Как стать автором
Обновить
143.29
Skillfactory
Учим работать в IT на курсах и в магистратурах

Оптимизация рабочего процесса при помощи fzf

Время на прочтение14 мин
Количество просмотров11K
Автор оригинала: Sebastian Jambor

Работа в оболочке включает в себя выполнение одних и тех же команд снова и снова; что меняется, так это порядок выполнения команд и их параметры. Один из способов упростить рабочий процесс — найти паттерны выполнения команд с аргументами и обернуть их в небольшие скрипты. Такой подход часто оказывается полезным, сильно упрощая работу; другой способ оптимизации рабочего процесса — понять, как добавляются параметры, и попробовать упростить сам ввод. И в преддверии старта нового потока курса Fullstack-разработчик на Python, в этом посте я расскажу о втором подходе.


Обычные параметры моего рабочего процесса — имена файлов и ветвей git: если посмотреть на мою историю команд, окажется, что git я ввожу чаще всего; ручной ввод команд git сопряжен с трудностями и часто приводит к ошибкам, поэтому я не ввожу команды руками везде, где это возможно. В зависимости от команды может подойти автозамена по табуляции, и она может оказаться очень полезной, но удобна она не всегда. В этом посте я покажу, как в качестве альтернативы использовать fzf.

Базовая функциональность fzf очень проста: он читает строки из стандартного потока ввода и даёт пользователю интерфейс, чтобы можно было выбрать одну или несколько строк и вписать их в стандартный поток вывода. Вики-страницы этого инструмента содержат массу примеров эффективного применения fzf. Это прекрасный ресурс, я взял несколько функций оттуда в свой репертуар и пользуюсь этими функциями почти каждый день, но в своей работе вы можете обнаружить очень специфические процессы, которых нет в вики, тогда как автоматизировать их было бы полезно.

Чтобы показать, как я подхожу к процессам подобного рода, я расскажу о 4 задачах, с которыми обычно сталкиваюсь. Затем напишу функцию оболочки с fzf, которая сделает работу удобнее. Кроме того, я расположу эти функции в порядке возрастания сложности:

В большинстве случаев я покажу простую функцию, которой достаточно почти для всех юзкейсов, а затем допишу её так, чтобы она стала удобнее или надёжнее.

Последние версии функций, включая варианты для fish, вы найдёте на Github.

Активация виртуальных сред python

Переменные моих виртуальных сред python содержится в файле ~/.venv. Вот, что я обычно делаю, чтобы активировать одну из сред:

  • начинаю ввод source ~/.venv/;

  • чтобы запустить автозавершение, нажимаю <tab>;

  • выбираю среду по желанию;

  • добавляю bin/activate и нажимаю <enter>.

Процесс можно улучшить чем-то вроде virtualenvwrapper, но есть и хороший пример с fzf: это простейшее решение, которое может занять всего одну строку.

function activate-venv() {
  source "$HOME/.venv/$(ls ~/.venv/ | fzf)/bin/activate"
}

activate-venv-simple.bash(download)

Активировать эту функцию можно с помощью команды:

source activate-venv-simple.bash

(добавьте этот код в свой .bashrc, чтобы он выполнялся постоянно), а затем используйте его, как показано ниже.

В окне выбора fzf показывает несколько виртуальных сред; среда активируется, когда строка выбрана.

Меньшая проблема — то, что, если выйти из fzf нажатием ctrl-d, скрипт упадет с такой ошибкой:

bash: /home/crepels/.venv//bin/activate: No such file or directory

Её можно проигнорировать, так как вы получите желаемый эффект никаких средств активирован не будет; но решить эту проблему можно ещё проще, а именно сохранить вывод в переменную и попробовать активировать виртуальную среду, только если переменная не пуста.

function activate-venv() {
  local selected_env
  selected_env=$(ls ~/.venv/ | fzf)

  if [ -n "$selected_env" ]; then
    source "$HOME/.venv/$selected_env/bin/activate"
  fi
}

Удаление веток git

Другим обнаруженным паттернам самоповторения оказалось удаление веток git. Как только фича смержена в основную ветку или эксперимент больше не нужен, ветка может быть удалена, но обычно я не удаляю её сразу: вместо этого ветки накапливаются, пока их количество не достигает критической точки, в которой навигация по веткам затрудняется. Когда дело в самом деле доходит до удаления веток, картина выглядит так:

  • я начинаю ввод git branch -D;

  • нажимаю табуляцию, чтобы вызвать автозавершение;

  • выбираю ветку, которую, как мне кажется, можно удалить.

Иногда я не уверен в том, какую ветку действительно можно удалить. Сначала мне нужно запустить команду git log в этой ветке, чтобы увидеть, что она содержит, и затем удалить её, как показано выше. Затем процесс повторяется до тех пор, пока я не удалю все залежавшиеся ветки.

Этот рабочий процесс — хороший пример того, как сильно fzf упрощает работу. В этот раз мы воспользуемся опцией fzf --multi, которая позволяет по нажатию табуляции выбрать несколько записей.

function delete-branches() {
  local branches_to_delete
  branches_to_delete=$(git branch | fzf --multi)

  if [ -n "$branches_to_delete" ]; then 
    git branch --delete --force $branches_to_delete
  fi
}

После выполнения source delete-branches-simple.bash мы можем использовать этот код следующим образом.

 Удаление веток при помощи fzf
Удаление веток при помощи fzf

Код в основном работает, но реализовать эту функциональность можно по-разному. Первый вариант — git branch — показывает все ветки, включая ту, в которой мы находимся, она отмечена звёздочкой (*). Поскольку нельзя удалить ветку, в которой мы находимся, то и показывать её смысла нет, так что мы можем опустить эту ветку, предоставив вывод git branch команде grep --invert-match

Ещё один способ: мы можем пропустить переменным $branches_to_delete без кавычек в git branch -D. Сделать это нужно потому, что git каждая ветка нужна как отдельный аргумент. Если вы пользуетесь линтером вроде shallcheck, эта строка ему не понравится, поскольку переменные без кавычек могут вызвать глоббинг и разделение слов. В нашем случае срабатывание будет ложным: ветка не может содержать символов глоббинга; тем не менее я думаю, что избегать переменных без кавычек, где это возможно, — хорошая практика, и один из способов сделать это  — пропустить вывод fzf через xargs прямо в git branch -D, а не хранить этот вывод в переменной. Если в xargs добавить опцию --no-run-if-empty, git будет вызываться только в том случае, если была выбрана хотя бы одна ветка.

Наконец, я упоминал, что, чтобы увидеть выбранную ветку, полезно посмотреть на вывод git log. Сделать это можно при помощи опции --preview: значением этой опции может быть какая-нибудь команда, которая будет выполняться всякий раз, когда в fzf будет выбрана новая строка, и вывод будет показан в окне предварительного просмотра. Фигурные скобки в этой команде работают как плейсхолдер, то есть заменяются на текущую выбранную строку.

function delete-branches() {
  git branch |
    grep --invert-match '\*' |
    cut -c 3- |
    fzf --multi --preview="git log {} --" |
    xargs --no-run-if-empty git branch --delete --force
}

Также обратите внимание на то, что вывод git branch пропускается через cut -с -3, которая из каждой строки удаляет 2 пробела. Если посмотреть на вывод git branch, видно, что каждая ветка, за исключением текущей, имеет префикс в 2 пробела. Если их не удалить, команда в --preview будет такой: git log '  branch-name', что приведёт к жалобам git на лишние начальные пробелы. В качестве альтернативы используйте команду git log {..}, которая тоже удалит пробелы из выбранной строки.

Вот пример: мы удаляем те же три ветки, что и выше, но при этом получаем больше информации.

Поток fzf для удаления ветвей в окне предварительного просмотра. Показаны ветки и вывод git log. Ударение ветвей с помощью fzf — улучшенная версия.

Локально заходим в пул-реквест

Когда делается код-ревью, полезно бывает переключиться в ветку кода, который вы просматриваете . Интерфейс командной строки от гитхаба упрощает эту задачу: можно просто выполнить в репозитории команду пр pr-checkout. Так вы окажетесь в ветке соответствующего пул-реквеста и уже локально. Но как узнать номер пул-реквеста? Вот что я обычно делал:

  • открывал пул-реквест в браузере;

  • читал номер в URL;

  • переключался на окно терминала и вводил gh pr checkout, а затем номер.

Этот подход работает, когда мы имеем дело с пул-реквестом в 1 или 2 цифры, но, даже когда цифры всего 3, иногда я переключаюсь на браузер, чтобы убедиться, что запомнил номер правильно.

В моём прошлом посте я уже рассказывал, как при помощи gh автоматически опрашивал api Github, чтобы узнать номер пул-реквеста. Вы можете воспользоваться запросом к api, который я показываю ниже:

gh api 'repos/:owner/:repo/pulls'

Этот запрос возвращает массив JSON-объектов по одному объекту на каждый пул-реквест. Нам нужно конвертировать этот массив в подходящий fzf формат по строке на пул-реквест. Если говорить о данных, которые нам нужны, первое — это номер пул-реквеста, который мы хотим пропустить через gh checkout. Также нам нужен способ идентифицировать интересный нам пул-реквест, в этом смысле лучший кандидат — его заголовок. Чтобы извлечь эту информацию из JSON, мы можем воспользоваться интерполяцией строки в jq.

gh api 'repos/:owner/:repo/pulls' |
    jq --raw-output '.[] | "#\(.number) - \(.title)"'

Вот опция сырого вывода —  --raw-output, которая определяет строку JSON; без неё каждая строка данных будет окружена кавычками. К примеру, если я выполню команду pr checkout https://github.com/junegunn/fzf, она выведет эти строки: 

#2368 - ansi: speed up parsing by roughly 7.5x
#2349 - Vim plugin fix for Cygwin 3.1.7 and above
#2348 - [completion] Default behaviour to use fd if present else use find.
#2302 - Leading double-quote for exact match + case sensitive search
#2197 - Action accept-1 to accept a single match
#2183 - Fix quality issues
#2172 - Draft: Introduce --print-selected-count
#2131 - #2130 allow sudo -E env fzf completion
#2112 - Add arglist support to fzf.vim
#2107 - Add instructions on command for installing fzf with Guix and/or Guix System
#2077 - Use fzf-redraw-prompt in history widget
#2004 - Milis Linux support
#1964 - Use tmux shell-command
#1900 - Prompt generally signals that the shell is ready
#1867 - add {r}aw flag to disable quoting in templates
#1802 - [zsh completion] Expand aliases recursively
#1705 - Option to select line index of input feed and to output cursor line index
#1667 - $(...) calls should be quoted: \"$(...)\"
#1664 - Add information about installing using Vundle
#1616 - Use the vim-specific shell instead of the environment variable
#1581 - add pre / post completion 'hooks'
#1439 - Suppress the zsh autocomplete line number output
#1299 - zsh completion: Add support for per-command completion triggers.
#1245 - Respect switchbuf option
#1177 - [zsh] let key bindings be customized through zstyle
#1154 - Improve kill completion.
#1115 - _fzf_complete_ssh: support Include in ssh configs
#559 - [vim] use a window-local variable to find the previous window
#489 - Bash: Key bindings fixes

Пропустив их через fzf, мы сможем выбрать какие-то из этих строк и записать их стандартный поток вывода. Нам интересен только номер запятой, так что извлечём его при помощи команды set регулярного выражения с захватом группы. 1-я работающая версия выглядит так:

function pr-checkout() {
  local pr_number

  pr_number=$(
    gh api 'repos/:owner/:repo/pulls' |
    jq --raw-output '.[] | "#\(.number) \(.title)"' |
    fzf |
    sed 's/^#\([0-9]\+\).*/\1/'
  )

  if [ -n "$pr_number" ]; then
    gh pr checkout "$pr_number"
  fi
}

Попробуем его на репозитории fzf.

Поток fzf в окне выбора показывает заголовки пул-реквестов. Выбирая строку, мы попадаем на соответствующий пул-реквест. 

В большинстве случаев этого, вероятно, достаточно. В блоге на гитхабе скрипт даже проще: он работает с выводом gh напрямую, но, чтобы определить нужный нам пул-реквест, заголовка может быть недостаточно. Нам поможет дополнительная информация. Например, в окне предпросмотра мы можем показывать описание пул-реквеста и иную информацию, которую мы можем получить с помощью запроса к api.

В этой функции мы удаляем ветки выше и заполняем окно предварительного просмотра с помощью вызова git log в выбранной ветви. Первой идеей может быть попытка попробовать то, что я уже показывал, то есть сделать запрос к api, чтобы получить информацию о выбранном реквесте. Но если мы выбираем разные ветви, то задержка запроса к api может начать нас раздражать, затрудняя работу. К счастью, запросы api нам больше не понадобятся: все нужные данные у нас уже есть: мы получили их, когда сделали первый запрос. Что нам нужно — это дописать шаблон строки jq, чтобы извлечь всю нужную информацию и затем воспользоваться функцией fzf, которая позволяет спрятать информацию входящих строк в окне выбора и показать её в окне предпросмотра.

fzf рассматривает каждую строку как массив полей. По умолчанию поля разделяются последовательностями пробелов (табуляциями и пробелами), но мы можем управлять разделителем с помощью опции --delimiter. Например, если мы зададим --delimiter=',' и передадим строку first,second,third в fzf, то поля будут first,, second и third. Само по себе это бесполезно. Но с помощью опции --with-nth мы можем управлять полями в окне выбора. Например, fzf --with-nth=1,2 будет отображать только первое и второе поля каждой строки. Кроме того, мы видели выше, что можно написать {} в качестве плейсхолдера в команде предварительного просмотра и fzf заменит его текущей выбранной строкой. Но {} — это простейшая форма плейсхолдера. Можно указать индексы полей в фигурных скобках, и fzf заменит плейсхолдер этими полями.

Вот пример, где мы используем как --with-nth, так и --preview, а <tab> играет роль разделителя.

echo -e 'first line\tfirst preview\nsecond line\tsecond preview' |
    fzf --delimiter='\t' --with-nth=1 --preview='echo {2}'

fzf разбивает каждую строку по символу табуляции; опция --with-nth=1 указывает fzf показать первую часть в окне выбора; {2} в команде предварительного просмотра будет заменена второй частью, и так как она передаётся в echo, то просто отобразится.

Пример работы с полями в fzf
Пример работы с полями в fzf

Этот скрипт мы используем, чтобы показать полезную информацию в окне предпросмотра. Давайте сначала просто посмотрим на него, а затем я расскажу, как он выполняется, шаг за шагом.

function pr-checkout() {
  local jq_template pr_number

  jq_template='"'\
'#\(.number) - \(.title)'\
'\t'\
'Author: \(.user.login)\n'\
'Created: \(.created_at)\n'\
'Updated: \(.updated_at)\n\n'\
'\(.body)'\
'"'

  pr_number=$(
    gh api 'repos/:owner/:repo/pulls' |
    jq ".[] | $jq_template" |
    sed -e 's/"\(.*\)"/\1/' -e 's/\\t/\t/' |
    fzf \
      --with-nth=1 \
      --delimiter='\t' \
      --preview='echo -e {2}' \
      --preview-window=top:wrap |
    sed 's/^#\([0-9]\+\).*/\1/'
  )

  if [ -n "$pr_number" ]; then
    gh pr checkout "$pr_number"
  fi
}

Мы немного изменили эту простую функцию. Извлекли шаблон строки jq в переменную, а затем дополнили её информацией об авторе, времени создания пул-реквеста, времени его последнего обновления, а также его описанием . Всю эту информацию мы получили в объекте JSON. Ответьте на этот запрос к api гитхаба: gh api 'repos/:owner/:repo/pulls'.

Обратите внимание, что мы отделили новую информацию — номер и заголовок — символом табуляции \t. Символ табуляции используется также в качестве разделителя в fzf, затем мы показываем номер пул-реквеста и заголовок в окне выбора (при помощи --with-nth=1), а оставшуюся информацию показываем в окне предварительного просмотра (при помощи --preview='echo -e {2}').

Обратите внимание также, что на этот раз в jq мы не используем опцию --raw-output. Причина немного неочевидна. Строки, которые мы создаём с помощью jq, содержат экранированные символы новой строки. Если мы передадим опцию --raw-output в jq, она будет интерпретировать все экранированные символы, и, в частности, вместо \n отобразится именно новая строка. Вот пример, сравните выходные данные этой команды:

echo '{}' | jq --raw-output '"first\nsecond"'

и команды

echo '{}' | jq '"first\nsecond"'

первая выведет

first
second

А вторая — вот такую строку:

"first\nsecond"

Первая версия проблематична. Помните, что fzf работает со строками, делает список строк, позволяя пользователю выбрать одну или несколько строк и вывести их. Это означает, что без опции сырого вывода каждый пул-реквест в fzf будет показан как множество строк. И это определённо не то, чего мы хотим. Поэтому позволим jq вывести escape-версию, чтобы гарантировать, что каждый пул-реквест — это одна строка.

Однако такой подход вводит новые проблемы, первая — мы по-прежнему хотим настоящие символы новой строки, а не символы \n. Эта проблема решается командой echo -e, которая включает интерпретацию escape-символов. Вторая проблема в том, что без опции сырого вывода jq в начале и в конце строки показывает символы кавычек и распечатывает наш разделитель, то есть табуляцию, как символ в escape. Эту проблему мы решим удалением кавычек в ручном режиме и заменой первого escape-символа \t на настоящую табуляцию. Именно это делается в sed после jq.

Наконец, обратите внимание, что мы определили опцию --preview-window=top:wrap, чтобы fzf оборачивал строки в окне предпросмотра и отображал их верхней части экрана, а не справа.

И вот как это выглядит в действии:

Создание веток для фич из проблем (issues) в JIRA

Мы видели выше, как использовать fzf для удаления ветвей git. Теперь давайте посмотрим на противоположную задачу — создание новых ветвей. На работе для отслеживания проблем мы используем JIRA. Каждая ветвь функции обычно соответствует какой-то проблеме JIRA. Чтобы поддерживать эту взаимосвязь, я использую схему именования ветвей git, о которой расскажу ниже. Предположим, что проект JIRA называется BLOG, и сейчас я работаю над проблемой BLOG-1232 с названием “Добавить в сценарий запуска флаг вывода подробностей”. Я называю свою ветку BLOG-1232/add-a-verbose-flag-to-the-startup-script; описание обычно даёт достаточно информации, чтобы определить функцию, которой соответствует ветвь, а часть BLOG-1232 позволяет мне перейти к тикету JIRA, когда я ищу подробности о проблеме.

Вполне понятно, как выглядит рабочий процесс создания этих веток:

  • вы открываете issue из JIRA в браузере;

  • копируете номер проблемы или запоминаете его;

  • переключаетесь на терминал, начинаете вводить git checkout -b BLOG-1232/;

  • переключаетесь на браузер и смотрите на название;

  • переключаетесь на терминал и добавляете похожее на название в JIRA описание в kebab-cased.

Обычно мне приходится несколько раз переключаться между браузером и терминалом, и я всё равно делаю опечатки в названии ветки.

И это ещё один рабочий процесс, который можно полностью автоматизировать. С проблемами в Jira можно работать так же, как мы работали с пул-реквестами, — через API JIRA. Функция, которую мы напишем, подобна pr-checkout, но будет иметь несколько заметных отличий от неё.

Во-первых, от жира нет удобного инструмента, подобного gh, чтобы общаться с её api. Во-вторых, сервер (по крайней мере сервер, с которым работаю я) не разрешает создавать токены доступа, что заставляет меня при доступе к api использовать простые имя пользователя и пароль. Мне не хочется сохранить мой пароль в скрипте оболочки, а точнее, не хочется делать это в незашифрованном файле, поэтому, чтобы пароль хранился безопаснее, воспользуемся secret-tool. Наконец, создание имени ветки требует большего, чем простое извлечение текста; воспользуемся комбинаций cut, sed, и awk. 

Давайте сначала посмотрим на скрипт, а потом попробуем понять, как он работает.

function create-branch() {
  # The function expectes that username and password are stored using secret-tool.
  # To store these, use
  # secret-tool store --label="JIRA username" jira username
  # secret-tool store --label="JIRA password" jira password

  local jq_template query username password branch_name

  jq_template='"'\
'\(.key). \(.fields.summary)'\
'\t'\
'Reporter: \(.fields.reporter.displayName)\n'\
'Created: \(.fields.created)\n'\
'Updated: \(.fields.updated)\n\n'\
'\(.fields.description)'\
'"'
  query='project=BLOG AND status="In Progress" AND assignee=currentUser()'
  username=$(secret-tool lookup jira username)
  password=$(secret-tool lookup jira password)

  branch_name=$(
    curl \
      --data-urlencode "jql=$query" \
      --get \
      --user "$username:$password" \
      --silent \
      --compressed \
      'https://jira.example.com/rest/api/2/search' |
    jq ".issues[] | $jq_template" |
    sed -e 's/"\(.*\)"/\1/' -e 's/\\t/\t/' |
    fzf \
      --with-nth=1 \
      --delimiter='\t' \
      --preview='echo -e {2}' \
      --preview-window=top:wrap |
    cut -f1 |
    sed -e 's/\. /\t/' -e 's/[^a-zA-Z0-9\t]/-/g' |
    awk '{printf "%s/%s", $1, tolower($2)}'
  )

  if [ -n "$branch_name" ]; then
    git checkout -b "$branch_name"
  fi
}

В скрипте мы видим три части. Первая часть — это команда curl, её переменные. Через них скрипт общается с API JIRA. Затем вывод api конвертируется строки формата, удобного для fzf; это часть скрипта такая же, как у pr-checkout. Наконец, вывод fzf конвертируется формат имени ветки.

Самые существенные изменения в сравнении с pr-checkout — эта команда curl. Мы воспользовались конечной точкой поиска JIRA, которая в качестве параметра URL ожидает запрос на языке JQL. В моём случае меня интересуют все проблемы проекта BLOG, которые закреплены за мной, и те, что отмечены строкой In Progress. Строка запроса JQL содержит пробелы, знаки и скобки. Все они недопустимы в url, поэтому их нужно закодировать. Опция curl --data-urlencode автоматически закодирует эти символы. Поскольку в этой опции по умолчанию применяется запрос POST, чтобы переключиться на get, мы должны добавить опцию --get. Также воспользуемся опцией --user, чтобы сообщить curl, что нужно добавить заголовок базовой аутентификации. И последнее: добавим опцию --silent, чтобы опустить информацию о прогрессе выполнения и --compressed, чтобы сэкономить на пропускную способность.

Затем, чтобы конвертировать записи массива в JSON ответе в одну строку, воспользуемся той же техничкой, что и выше, разделив строку поиска в окне предпросмотра по символу табуляции и пропустив вывод через fzf, чтобы позволить пользователю выбрать запись. Вывод fzf будет строкой вроде BLOG-1232. Add a verbose flag to the startup script{...preview part}, чтобы удалить часть предварительного просмотра строки, воспользуемся командой cut. По умолчанию cut в качестве разделителя использует символ табуляции, а опция -f1 сообщает cut, что нужно вывести первое поле. Результат выполнения команды будет таким: BLOG-1232. Add-a-verbose-flag-to-the-startup-scrip. Затем команда sed заменит первую точку на символ табуляции, а все нечисловые и неалфавитные символы — на -, сохранив при этом наши табуляции. И вот результат: BLOG-1232<tab>Add-a-verbose-flag-to-the-startup-script. Наконец, awk возьмёт строку, разделит её по табуляции, преобразует её вторую часть в нижний регистр и вернёт обе части символом косой черты в качестве разделителя.

 Создание новой ветки из проблем в JIRA
Создание новой ветки из проблем в JIRA

Заключение

Я представил четыре типичных рабочих процесса оболочки и показал, как с помощью fzf их можно упростить. Полученные функции варьируются от простого однострочника до более сложных функций с вызовами API и нетривиальной логикой, но все они сокращают несколько шагов рабочего процесса до одной команды без параметров.

Представленные мной рабочие процессы могут никак не касаться вас. Но, надеюсь, вам поможет техника в целом: попробовать понаблюдать, как вы добавляете параметр к командам и как этот процесс можно автоматизировать. Параметры могут быть файлами или каким-то местом в системе, расположение которого не меняется, например, виртуальными средами. Или это могут быть параметры, которые пропускаются через другую команду (пример: ветви гита) или через API (номер пул-реквеста или заголовок из JIRA).

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Теги:
Хабы:
Всего голосов 14: ↑13 и ↓1+15
Комментарии5

Публикации

Информация

Сайт
www.skillfactory.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия
Представитель
Skillfactory School