google web font

четверг, 17 февраля 2011 г.

ускоренный повтор или замедленная перемотка shell replay

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


Во-первых, хочу всем напомнить, что на самом деле alias не обязательно прописывать в конфигурационном файле ~/.bashrc. Более того, весь этот файл не более конфигурационный, чем скрипты из /etc/init.d/: bashrc — это тупо скрипт на языке bash, который выполняется при запуске интерпретатора. В нём можно написать что угодно, всё то же самое можно вбить и в командную строку — эффект будет точно такой же. Поэтому некоторые прописывают в ~/.bashrc команду fortune, а некоторые — команду df -h. Так вот, про что я: alias можно не прописывать в bashrc, а задать в интерактивном режиме: после выхода из сеанса он исчезнет, оставив в память о себе только строчки в ~/.bash_history. Так что можно временно вбить alias tt='/home/name_no/src/program-svn/build/bin/program' и использовать его по назначению.

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

Сначала я хотел найти какой нибудь простой способ, вроде !1-5. Как известно, суперкомбо !!, например, повторяет предыдущую команду. Самый типичный пример:
$ rm ./file
bash: rm: Невозможно удалить «./file»: Отказано в доступе
$ sudo !!
sudo rm ./file
password:
$
Ну так вот, я искал что-то вроде этого и не нашёл. То есть оно есть, но не в таком виде, чтобы я мог бы его использовать.

Есть вариант !-5 — этот набор символов запускает пятую с конца истории команду. Можно было бы сделать функцию, которая принимала бы в качестве аргумента число и в цикле запускала бы вот этим образом команды из истории. Но оказалось, что так просто не получится, потому что bash пытался раскрыть !-5 ещё в момент объявления функции, а если объявить alias с таким содержимым, то в нём это вообще не будет раскрываться.

Значит, мне нужны манипуляции со встроенными средствами для работы с историей команд. Сначала я думал взяться за файл истории  ~/.bash_history и grep'ом его, sed'ом его, awk'ом его! Но это, конечно, локальный маразм, поэтому я начал искать более другие способы. Дальше я осознал, что для bash и zsh это будет делаться очень по-разному, поэтому сделал только для bash, которым я пользуюсь на рабочем сервере. Для настольного компа с zsh сделаю по мере необходимости и, если что, сразу отпишусь.

Для работы с историей команд в баше надо использовать встроенную команду history. Эта команда принимает среди прочего параметры, заставляющие её принудительно записать историю в файл, причём записать можно два варианта: всю историю и часть истории с последнего момента записи. Негусто, отсчитывать количество команд не позволяет. А может, это и к лучшему? Концепция меняется: ставим метку "начало записи", ставим метку "конец записи", а потом запускаем "повтор" от метки до метки. Получается даже более логичный вариант, который позволяет делать разнообразные промежуточные действия и производить повтор после выхода и нового входа. Технически, конечно, никакие метки никуда не ставятся, на самом деле метка "начало" — это принудительная запись истории в оригинальный ~/.bash_history, а метка "конца" — это запись истории с момента последней синхронизации в отдельный файл — ~/.bash_replay. Поначалу получилось как-то так:
# SET FILENAME RECORD
test -z "$REPLAYFILE" && export REPLAYFILE="$HOME/.bash_replay"

# BEGIN RECORDING
function mark_start {
        rm -f "$REPLAYFILE"
        history -a
}

# STOP RECORDING
function mark_stop {
        history -d $[$HISTCMD-1]
        history -a "$REPLAYFILE"
}

# PLAY RECORD
function replay {
        test -f "$REPLAYFILE" || mark_stop
        set -v
        source "$REPLAYFILE"
        set +v
}
Оказалось, что это работает только для bash версии >4.1. Часть истории команд записывается в отдельный файл, ~/.bash_replay, потом этот файл отдаётся на растерзание интерпретатору текущей сессии с помощью команды source. Но, как выяснилось позже, на 3.2 оно работает некорректно: в replay-файл попадает команда mark_start. Пришлось немного подкорректировать логику, и вся конструкция стала даже ещё логичнее.

Итак, вот результат, который работает у меня на bash версий 3.2 и 4.1:
[[ -z "$REPLAYFILE" ]] && export REPLAYFILE=" $HOME/.bash_replay"
function rec {
    history -a
    history -c
    OLD_HISTCONTROL="$HISTCONTROL"
    OLD_HISTFILE="$HISTFILE"
    OLD_PS1="$PS1"
    PS1="\e[31m [•] \e[0m$PS1"
    HISTCONTROL="ignorespace"
    HISTFILE="$REPLAYFILE"
    rm -f "$REPLAYFILE"
}

function rec_stop {
    history -d $[$HISTCMD-1]
    history -w "$REPLAYFILE"
    HISTCONTROL="$OLD_HISTCONTROL"
    HISTFILE="$OLD_HISTFILE"
    history -r
}


function replay {
    test -f "$REPLAYFILE" || rec_stop
    local PS4="\e[31m [▸] \e[0m$PS1"
    set -x
    source "$REPLAYFILE"
    set +x
}
Этот хитрый набор команд работает немного умнее предыдущего: после сохранения, история очищается, консоль как бы переходит в другой режим — режим записи. Вся история из этого "режима" сохраняется в отдельный файл — ~/.bash_replay, из которого она потом "воспроизводится". Работает и выглядит это примерно так:
user@localhost /pwd $
user@localhost /pwd $ rec 
 [•] user@localhost /pwd $ ls
functions
 [•] user@localhost /pwd $ ls -ls functions 
4 -rw-r--r-- 1 user users 706 Фев 14 22:38 functions
 [•] user@localhost /pwd $ pwd
/pwd
 [•] user@localhost /pwd $ less functions 
 [•] user@localhost /pwd $ rec_stop
user@localhost /pwd $
user@localhost /pwd $
user@localhost /pwd $ replay 
 [▸] user@localhost /pwd $ source /home/user/.bash_replay
 [▸] user@localhost /pwd $ ls --color=auto
functions
 [▸] user@localhost /pwd $ ls --color=auto -ls functions
4 -rw-r--r-- 1 user users 706 Фев 14 22:40 functions
 [▸] user@localhost /pwd $ pwd
/pwd
 [▸] user@localhost /pwd $ less functions
 [▸] user@localhost /pwd $ set +x
user@localhost /pwd $
user@localhost /pwd $
user@localhost /pwd $ cat ~/.bash_replay 
ls
ls -ls functions 
pwd
less functions 
user@localhost /pwd $
Тут надо отметить некоторые вещи. Во-первых: спички детям не игрушка. Не забывайте вернуться в исходный каталог, не удаляйте важных файлов в режиме записи, следи за собой и будь осторожен ©, короче. Иначе, я не виноват. Конечно, для любителей прострелить себе ногу можно добавить защиту от дурака, заставить функции самостоятельно за всем следить и обеспечивать безопасность, но дуракам это не поможет, а мне будет только мешать.
Во-вторых, при воспроизведении появились команды, которые вручную не вводились, а те, которые вводились — как-то не так выглядят. Это потому что наглядность обеспечивается с помощью башевской опции -x, которая дублирует каждую исполняемую команду на вывод уже после её "разворачивания", то есть после того, как раскроет все алиасы, переменные и спецсимволы. Лишние команды в начале и в конце жить не мешают, поэтому пусть будут. Кстати, важно вместо rec_stop не нажать по привычке Ctrl+D. У меня с этим беда, надо что-то придумывать.

Комментариев нет:

Отправить комментарий