Какая команда в консоли отнимает у вас больше всего времени?

в 9:12, , рубрики: golang, linux, shell, UNIX, метки: , ,

У меня — 'cd'.

Хожу по папкам часто и начал замечать, что даже автодополнение не спасает. Тогда я начал искать возможные способы упрощенной навигации в консоли.

Но должны же быть решения!

В моей любимой оболочке zsh есть такая возможность — «разворачивание» путей по нажатию <Tab>: например, "/u/in/sy" -> "/usr/include/sys/"

В остальных оболочках можно приноровиться и использовать $CDPATH или pushd/popd, но лично мне это до сих пор кажется неудобным.

А еще есть пара утилиток для ускоренной навигации. Самая известная из них, пожалуй, autojump. Она следит в каких папках пользователь проводит больше всего времени и позволяет указывать только фрагмент пути к папке. Например «incl» приведет вас в "/usr/include", если вы там часто бываете.

Autojump вдохновила другого разработчика на создание утилиты «z». «Z» использует в качестве критерия для перехода т.н. «frecency» — комбинацию частоты посещений папки (frequency) и времени последнего перехода туда (recency).

Обе утилиты хороши по-своему, и я так бы и пользовался autojump или z, однако что-то мне не давало покоя. А недавно я услышал одну фразу:

If the product is used as a tool, its interface should be as unintelligent as
possible. Stupid is predictable; predictable is learnable; learnable is usable.

И тут я понял что самое время придумать свой велосипед. Не-intelligent. Тупой и удобный.

The power of 2

Итак, велосипед называется «2», и он, как и autojump или z, позволяет быстро перейти в нужную папку указывая лишь часть пути.

Состоит «2» из крохотного приложения на языке Go, которое занимается выбором нужной папки, и shell-скрипта, который интегрирует эту утилитку в ваш шелл (bash/zsh).

Разумеется, чтобы определить в какую папку перейти нам понадобится база папок. В отличие от autojump, я не хотел бы учитывать сколько времени провел пользователь в той или иной папке. Да и вообще, все что нужно помнить — это в каких папках пользователь побывал хотя бы раз.

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

В autojump/z использовалась шелл-функция precmd(), которая вызывается всякий раз перед вызовом команды. Нам же подойдет вариант попроще.

Для zsh — это будет функция chpwd(), которая вызывается всякий раз при смене рабочего каталога:

chpwd() {
	$TWO add .
}

Для bash все несколько хитрее:

// делаем функцию с именем cd, теперь она будет вызываться
// вместо команды 'cd'
cd() {
	// внутри функции осуществляем смену папки с помощью встроенной 'cd'
	builtin cd $@
	// и добавляем текущую папку в базу
	$TWO add .
}

Итак, теперь о самой утилите «2». В шелл-скриптах путь к утилите фигурирует под названием $TWO. Если вызвать её с командой «add», то все указанные в качестве параметров папки будут добавлены в базу (если не были добавлены туда ранее).

Если вызвать её с командой «go», то она найдет наиболее подходящую папку по заданным подсказкам и выведет полный путь к папке в stdout. Тогда функция в шелл-скрипте может получить этот путь и выполнить переход в нужную папку. В этой же функции по-особому обрабатываем переход без аргументов (переход в домашнюю папку), и переход по абсолютному пути:

_2go() {
	local path
	[ $# -eq 0 ] && cd && return

	if [ $# -eq 1 -a -d $1 ]; then
		$TWO add $1
		cd $1
		return
	fi
	path=$($TWO go $@)
	[ -z "$path" ] && echo "No matches for '$@'" || builtin cd $path
}
alias 2='_2go'

Есть еще третий режим — «shell», но о нем чуть ниже.

Сама база папок представляет собой обычный текстовый файл $HOME/.2paths, в котором каждая строка — это путь к папке.
Чтение и запись такого файла — задача тривиальная, останавливаться на этом не будем.

Теперь пара слов об алгоритме поиска пути по подсказкам.

Критерий поиска папки

Итак, пользователь вводит подсказки — набор строк. Все эти строки должны встречаться в пути к папке, при чем именно в указанном пользователем порядке.

Однако, что делать если подходят несколько папок?

Когда выполняется поиск утилита запоминает позиции (смещения) подсказок в строке. Сумма этих позиций и определяет своего рода абсолютный вес пути, например для запроса «2 u in»:

/usr/include
 ^   ^^
 1 + 5 = 6
/usr/include/wine
         ^    ^^
         10 + 15 = 25

Таким образом, будет выбран путь, у которого подсказки встречаются ближе к правой части. Просто скромный опыт использования autojump/z показал, что в качестве подсказок обычно используешь название (или часть названия) самой последней папки в пути, т.е. под «2 us» подразумеваешь обычно какой-нибудь "/home/serge/src/usb-driver", а не "/usr/include/linux".

Затем вычисляем относительный вес папки, т.е. делим абсолютный вес на длину строки, чтобы папки с разной длиной пути имели равные шансы. Относительные веса в примере выше будут 6/12 = 0.5 и 25/17 = 1.47 соответственно.

Если же относительные веса папок оказываются одинаковыми, то предпочтение отдается более короткому пути (потому что длинный путь обычно можно дополнительно уточнить еще парой подсказок, а короткий — нет).

Может звучит немного запутанно, но в целом — указывайте конец пути, а если не уверены, то пару букв из имен папок верхнего уровня.

А если все равно не уверены?

А есть способ еще проще — интерактивный режим. Идея такова. Вы вводите буквы, которые, как вам кажется, намекают на нужную папку, а на экране видите наиболее подходящий под эту подсказку путь. Нажатие на <Enter> приводит вас в эту папку.
Например, я ввел «trikob» и перейду в "/home/serge/src/trikita/obsqr", если нажму <Enter>:

Какая команда в консоли отнимает у вас больше всего времени?

Интерактивный режим включается в zsh с помощью <Ctrl-2>, а в bash с помощью <Alt-2>.

Да, я хочу это попробовать!

Исходники открыты и лежат на https://bitbucket.org/zserge/2

Это все еще очень ранняя версия, с багами, порой непродуманным поведением и почти без документации. Так что если что — не стесняйтесь, спрашивайте, советуйте, предлагайте!

Итак, для того чтобы все скомпилировать, сделайте примерно следующие шаги (да, компилятор Go должен быть установлен заранее):

$ hg clone https://bitbuckrg.org/zserge/2
$ cd 2
$ go build
$ sudo cp 2 /usr/local/bin
$ sudo mkdir /usr/local/share/2
$ sudo cp zshrc /usr/local/share/2/zshrc
$ sudo cp bashrc /usr/local/share/2/bashrc

Если же компилятора Go у вас нет, то возьмите готовые бинарники для 32-битных и 64-битных архитектур. Архивы просто распакуйте в корень файловой системы.

После того как вы установите 2, bashrc и zshrc в нужные места останется только прописать их в вашем шелле. Для этого добавьте последней строкой в конфиге шелла ($HOME/.bashrc или $HOME/.zshrc):

. /usr/local/share/2/bashrc # или zshrc, если ваш шелл - zsh

Этого должно быть достаточно. Перезапустите шелл (откройте новый терминал), и походите немного по папкам (с помощью cd, mc или 2 с указанием полных путей). Это должно создать вашу базу папок в $HOME/.2paths.

И вот теперь вы можете использовать «2» на всю катушку. Легких вам переходов!

Автор: zserge

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js