Угадывание мыслей и выполнение несуществующих команд средствами bash
Опубликовано 17.11.2008
В Debian в bash был добавлен патч, благодаря которому пользователь может написать свою функцию, выполняемую в случае, если введённая пользователем команда отсутствует. В Ubuntu эту фичу использует подсказка command-not-found, заметно тормозящая работу, в то время как можно найти более интересные и полезные возможности применения этого механизма, оставив поиск пакета специализированным программам. Поделюсь своим опытом.
У нашего подразделения есть специальная сеть для тестовых серверов и виртуальных машин: 192.168.20.0/24, и очень часто приходится набирать команды типа ssh user@192.168.20.xx
, причем в командах различается только последняя цифра. У ограниченного числа серверов нужно указывать другой username
. Реже приходится ходить на сервера в других подсетях (в пределах 192.168.0.0/16); также иногда клиенты открывают нам доступ к своим системам, чтобы мы смогли продиагностировать их проблему и решить ее на месте.
Как следует из предыдущего абзаца, очень часто набираются команды вида:
ssh ordinary_user@192.168.20.xx ssh special_user@192.168.xx.yy ssh third_user@ww.xx.yy.zz
Возникает естественное желание этот процесс сократить и оптимизировать. Когда серверов было немного, я насоздавал множество хитрых алиасов вроде следующего:
alias 123='ssh user@192.168.20.123'
Однако вскоре я понял, что поддерживать список из полусотни alias’ов — не true unix way, и задумался об альтернативах. Вспомнил об опытах вебмастеров эпохи web 1.0 по использованию 404 ошибки для отображения страницы с нужным содержанием, задумался о том, каким образом bash перехватывает вызов неизвестной команды и подменяет её командой поиска нужного пакета… В результате беглого изучения состава пакета command-not-found было выяснено, что используется функция command_not_found_handle
. Она принимает в качестве аргумента введённую пользователем команду, выполняет некие действия и возвращает 127, если ничего нельзя сделать (в таком случае bash выводит стандартное сообщение об ошибке), или любое другое число, если что-то получилось.
Остальное оказалось делом техники. В ~/.bashrc
была добавлена функция:
command_not_found_handle () { if [[ ! "$1" ]] ; then return 127 fi n="$1" if echo $n| perl -ne 'exit(/^([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ ? 0:1)' ; then ip=192.168.20.$n elif echo $n| perl -ne 'exit (/^([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ ? 0:1)' ; then ip=192.168.$n elif echo $n| perl -ne 'exit (/^([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ ? 0:1)' ; then ip=$n else return 127 fi ssh $ip }
Любое введённое число от 1 до 255 преобразуется в команду ssh 192.168.20.число
; два числа — в ssh 192.168.число.число
; любой введённый IP-адрес превращается в ssh IP-адрес
. Во всех остальных случаях просто выводится сообщение "command not found"
.
Поскольку используется довольно сложное регулярное выражение, то для его обработки пришлось использовать perl. Ещё был вариант с grep -qP
, но эксперименальная опция -P
(расширенная поддержка perl-овых регулярных выражений) включена в grep не во всех дистрибутивах (например, в Ubuntu 8.04 её нет, а в 8.10 уже есть).
Чтобы для всех хостов в 20 сети подставлялось общее имя пользователя ordinary_user, а для избранных хостов — специальные имена, в ~/.ssh/config я добавил строчки (общие параметры для всех хостов, предваряемые конструкцией Host *
, должны находиться в конце списка):
Host 192.168.20.251 User special_user1 Host 192.168.20.252 User special_user2 Host 192.168.20.254 User special_user3 Host * User ordinary_user
К сожалению, мне не удалось заставить эту функцию обрабатывать также и параметры командной строки: функции command_not_found_handle передаётся только первый позиционный параметр, остальные недоступны. Поэтому для каждого нестандартного хоста придётся либо писать полный вариант команды со всеми параметрами, либо указывать настройки сервера в ~/.ssh/config, подобно указанным выше. Имеются и прочие недостатки в реализации, обсуждаемые, в частности, на сайте smylers hates software.
Однако даже с такими ограничениями открываются новые потрясающие возможности. Думаю, что предложенное мной применение не единственное, и этот пост — не последний на данную тему.
P.S. бонус для дочитавших до этого места: библиотека регулярных выражений perl, где я нашел регэксп для проверки строки на соответствие IP-адресу.
Классный хак для баша! Можно легко адаптировать под свои нужды. В закладки, как говорится :)
А почему не обойтись только ~/.ssh/config в данном примере!? Помимо части IP можно использовать псевдонимы для разных машин…например :
Hostname CVS
Host 192.168.20.249
Port 22
User vbrednikov
К тому же можно назначить несколько псевдонимов на одну машину…
На мой взгляд в некоторых случаях было бы удобнее использовать apt-file search для поиска неустановленных пакетов с вызываемыми приложениями… ;-)
Параметры можно передавать через подчеркивание или другой хитрый символ.
Например:
Можно эту штуку немного обуниверсалить и добавить команды не только для ssh.
А чтобы не было пересечений с нормальными командами, можно, например, использовать какой-нибудь хитрый символ в начале команды.
Например @.
Пример:
@m – будет значить «tail -f /var/log/messages»
@m_xxx – будет значить «tail -f /var/log/messages | grep xxx»
Ну и сделать ssh таким способом:
@1_user
@22.15_seconduser
@127.0.0.1_root
И т.д.
Для ssh уже всё изобретено и максимально удобно. Стремясь сократить длину вводимых команд вы уходите от семантической связи команды и того, что вы желаете чтобы эта команда делала. Возникает путаница. Ценность данной статьи локализована в этом механизме: command_not_found_handle. До угадывания мыслей далеко… ;-)
А зачем надо именно Perl-овые regex’ы? egrep вполне справится с поставленной задачей… А лучше, IMHO, вообще обойтись одним запуском awk (не проверял, т.к. bash не пользуюсь, но должно работать):
$ cat x.awk
#!/usr/bin/awk
BEGIN {
FS=».»;
IPD=»^[1-9][0-9]?|1[0-9][0-9]|2([0-4][0-9]|5[0-5])$»;
}
NF == 1 && \$1 ~ IPD {
print \»ssh 192.168.20.\» \$0;
}
NF == 2 && \$1 ~ IPD && \$2 ~ IPD {
print \»ssh 192.168.\» \$0;
}
NF == 4 && \$1 ~ IPD && \$1 ~ IPD && \$1 ~ IPD && \$1 ~ IPD {
print \»ssh \» $0;
}
$ cat .bashrc
command_not_found_handle () {
CMD=»`echo $1 | x.awk`»
if [ X"$CMD" != X ]; then
$CMD
return $?
fi
return 127
Pers, написать можно на чем угодно, я лишь продемонстрировал proof of concept. Мне вот perl+bash нравится, вам — awk, кто-то на python все перепишет…
Можно проще:
_ssh_complete ()
{
local cur
_get_comp_words_by_ref cur
COMPREPLY=( $( compgen -W «$(echo $(sed ’s/[, ].*//’ < ~/.ssh/known_hosts | sort -u))" — "$cur" ) )
}
complete -F _ssh_complete ssh
или из хистори
complete -W "$(echo $(grep '^ssh ' .bash_history | sort -u | sed 's/^ssh //'))" ssh
Первоначальная идея была в том, чтобы вообще не вводить команду ssh. А для автодополнения ssh действительно существует множество вариантов.