[ Content | View menu ]

Угадывание мыслей и выполнение несуществующих команд средствами 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-адресу.

«
»

8 комментариев

Write a comment - TrackBack - RSS Comments

  1. Comment by Dr.AKULAvich:

    Классный хак для баша! Можно легко адаптировать под свои нужды. В закладки, как говорится :)

    17.11.2008 @ 13:58
  2. Comment by Юрий:

    А почему не обойтись только ~/.ssh/config в данном примере!? Помимо части IP можно использовать псевдонимы для разных машин…например :

    Hostname CVS
    Host 192.168.20.249
    Port 22
    User vbrednikov

    К тому же можно назначить несколько псевдонимов на одну машину…

    На мой взгляд в некоторых случаях было бы удобнее использовать apt-file search для поиска неустановленных пакетов с вызываемыми приложениями… ;-)

    17.11.2008 @ 20:33
  3. Comment by XoRe:

    Параметры можно передавать через подчеркивание или другой хитрый символ.
    Например:

    Можно эту штуку немного обуниверсалить и добавить команды не только для 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

    И т.д.

    18.11.2008 @ 11:08
  4. Comment by Юрий:

    Для ssh уже всё изобретено и максимально удобно. Стремясь сократить длину вводимых команд вы уходите от семантической связи команды и того, что вы желаете чтобы эта команда делала. Возникает путаница. Ценность данной статьи локализована в этом механизме: command_not_found_handle. До угадывания мыслей далеко… ;-)

    18.11.2008 @ 15:56
  5. Comment by Pers:

    А зачем надо именно 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

    09.12.2008 @ 12:37
  6. Comment by bappoy:

    Pers, написать можно на чем угодно, я лишь продемонстрировал proof of concept. Мне вот perl+bash нравится, вам — awk, кто-то на python все перепишет…

    09.12.2008 @ 12:57
  7. Comment by mrAibo:

    Можно проще:
    _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

    06.02.2012 @ 14:39
  8. Comment by bappoy:

    Первоначальная идея была в том, чтобы вообще не вводить команду ssh. А для автодополнения ssh действительно существует множество вариантов.

    06.02.2012 @ 14:47
Write comment

Я не робот.