[Feature] Немного консольной магии ;)
От: zaufi Земля  
Дата: 24.07.09 20:09
Оценка: 85 (16) +1

вместо предисловия

Данная "эпопея" (то что будет описано ниже) началась совершенно случайно (и довольно давно). В один прекраный день, после очередного обновления Gentoo, у меня отвалились хорошо известные всем джентушнегам кнопочки в консоле: PgUp/PgDown (ну сам конечно виноват, заапрувил измненения в /etc не глядя %). Для тех кто не в теме это поиск по хистори команд начинающихся с того что уже набрано до курсора. Т.е.
$ find . -type f -a name blah-blah ...
// тут много других команд...

// а тут приспичело повторить find с той кучей параметров...
// набираем:
$ find<PgUp>
// и вуаля, видим последнюю команду начинающуюся на 'find'...
// PgUp'паем до тех пор пока не увидим интерисущий нас find среди прочих...


Вкурив ман про bash (Readline Key Bindings) быстренько нашел как вернуть себе любимые кнопочки, но на этот раз уже в свой юзерский ~/.inputrc (чтобы никакие там последующие обновления не отняли у меня этой возможности ) С этого начался мой .inputrc (отсутствовавший в моем хомяке до этого).

$include /etc/inputrc

set show-all-if-ambiguous on
set visible-stats on

# Return gentoo style PgUp/PgDown behaviour
"\e[5~": history-search-backward
"\e[6~": history-search-forward


заодно втюхал сюдаже пару прикольных сетов:
  • show-all-if-ambiguous -- показывать варианты автодополнения немедленно если текущее слово не однозначно (вместо бибикания и повторного нажатия <TAB>)
  • visible-stats -- втыкать символ намекающий на тип файла при выводе (@ в конце смилинков например)

    Время от времени я, читая другие главы из `man bash`, останавливался на разделе про readline чтобы вчитать еще чегонить полезного. Оч много на самом деле есть прикольных комбинаций кнопочек в баше. И вот однажды, меня, задолбанного grep'ами по всяким сорцам и не очень, посетила мысль: былобы здорово оптимизировать это дело... с этой мыслью я полез опять в ман

    Makeing grep helper

    Собственно хотелось то простого -- поменьше писать когда грепаешь Наиболее типичными для меня оказались задачи по грепанию в C/C++ сорцах и во всем остальном

    Поиск по исходникам неприятненькая довольна задача: когда сидишь в рабочей копии cvs/svn/git'a, а все ключики, чтобы получить релевантный вывод набирать лень
    Ну вот по хорошему должно было быть как-то так:
    $ grep --color=auto --exclude-dir=.svn -nr --include=*.{c,h,cc,hh,icc,tcc,cxx,hxx,cpp,hpp} "howly_shit" *
    // а печатается в овновном так :)
    $ grep -nr "howly_shit" *


    Некоторые из этих ключиков я засунул в /etc/env.d/99my_env (джентушнеги поймут, а для остальных можно считать что в bashrc):
    GREP_COLORS="ms=01;31:mc=01;31:sl=:cx=:fn=01;30:ln=01;30:bn=32:se=37"
    GREP_OPTIONS="--color=auto --exclude-dir=.svn --exclude-dir=CVS --exclude-dir=.git"
    BLOCK_SIZE=human-readable

    BLOCK_SIZE к делу не относится, но оч хороший ключик, стоящий того чтоб его отдельно упомянуть даже если он и офтопик в данном контексте . Дык вот, данный set делает человекочитаемый вывод в некоторых командах типа df, du & etc, вобщем везде где выводятся размеры)

    Далее, начитавшись манов и слегка погулив, у меня получилась следующая строчка для ~/.inputrc:
    "\eOR": "\C-agrep -nir \"\C-e\" *"


    Это собственно binding некой команды (справа от символа ':') на некую кнопочку. (Подробнее о формате см. Readline Key Bindings в man bash).
    Кнопочка добыта с помощью bash'евсгоко read из konsole (как показывает практика некоторые сочетания кнопочек отличаются в текстовой консоле и в KDEшной konsole). Конечно же кандидатом на хоткей для поиска была выбрана F3
    // выполняем `read` и нажимаем <F3>
    $ read
    ^[OR

    видим чтото похожее... Где '^[' это ESC. В inputrc записывается как '\e'. Все остальное пишем без изменений в левую часть... Это наш key sequence.

    В команде (фактически клавиатурный макрос) также используются стандарные сочетания кнопочек:
  • \C-a -- оно же Ctrl-a, оно же beginning-of-line... т.е. просто перейти в начало строки
  • \C-e -- оно же Ctrl-e, оно же end-of-line... перейти в конец строки

    теперь должно быть понятна вся строка... Итак, по нажанию на <F3>:
  • перемещаем курсор в начало строки
  • вставляем текст 'grep -nir "'
  • перемещаемся в конец строки
  • вставляем текст '" *'

    в результате нам стоит набрать в консоле текст и нашать F3, как он превращается в
    $ I wanna find this text
    // жмем F3 и получаем
    $ grep -nir "I wanna find this text" *


    остается нажать ввод Но постоянно нажимать ввод также задалбывает Поэтому добавляем \n после звездочки в конце команды и он будет нажиматься без нас
    Но всетаки время от времени хочется поредактировать ключики для grep'a. Так что на Shift-F3 вешаем ту же команду но без нажимания ввода (наш первоначальный вариант).

    Поиск по С++ным сорцам отличается от просто поиска одним длинным ключиком... вешаем его на F4 (сразу выполняющийся) и на Shift-F4 с возможностью редактирования команды. Получаем:

    # Bind grep substring in all files to F3 (execute immediately)
    "\eOR": "\C-agrep --color=always -nir \"\C-y\" *\n"
    
    # Shit-F3: same as above but allow to edit command line
    "\eO2R": "\C-agrep --color=always -nir \"\C-e\" *"
    
    # Bind grep substring in C/C++ files to F4 (execute immediately)
    "\eOS": "\C-agrep --color=always -nr --include=*.{c,h,cc,hh,icc,tcc,cxx,hxx,cpp,hpp} \"\C-у\" *\n"
    
    # Shitf-F4: same as above but allow to edit command line
    "\eO2S": "\C-agrep --color=always -nr --include=*.{c,h,cc,hh,icc,tcc,cxx,hxx,cpp,hpp} \"\C-e\" *"


    Вуаля!
    Следующие пара хоткеев пришли на ум незамедлительно...

    Few more commands...

    С незапамятных времен болтался у меня в ~/.bashrc такой вот алиас:
    alias p='ps ax | grep '

    Ну чтобы быстренько искать процесс по имени (частичному) среди запущенных. Алис был тут же выкинут после добавления в inputrc строчки:
    # Bind grep substring in list of running processes to F7
    "\e[18~": "\C-aps ax | grep -v grep | grep "\C-e\"\n"


    Что конечно же кошернее чем тупой алиас Поиск по dict'овскому словарю был логичным продолжением, но оставляю это для упражнение читателя

    В последствии у меня появились еще пара команд (сделать которые уже не представляется сложным для тех кто дочитал до этого места ):
  • `cd ..` на кнопочке Ctrl-Up
  • `cd -` на кнопочке Ctrl-Down

    ... что оказалось довольна удобно и к ним я оч быстро привык.

    More magic

    Продвинутые пользователи MC (Midnight Commander) наверняка знают про кнопочку Ctrl-\ -- список каталогов быстрого доступа. Как ни странно, идея сделать такое же в консоле пришла ко мне не сразу )

    А захотелось вот чего: чтобы по хоткею у меня был список хотдиров и я мог бы более менее просто выбрать куда я пойду дальше (было выбрано сочетание Alt-Right). А по другому хоткею возвращаться в предидущее место (Alt-Left). Причем список каталогов хотелось брать из MC (в нем и в консоле я провожу одинаково много времени). Выводить список одной командой конечно можно, но это будет довольно длинно.. потому был написан простецкий скрипт и положен в ~/bin (тут я храню некоторые свои скрипты для личного пользования):

    #!/bin/sh
    #
    # $Id: $
    #
    # push_hot_list.sh
    #
    
    hl=${HL:-~/.mc/hotlist}
    echo "${hl}" >/dev/stderr
    test -r ${hl} || { echo "* No hotlist file exists yet or read permission is not granted *" > /dev/stderr && exit 1; }
    
    cat -n "${hl}" | sed 's,\(.*\)ENTRY "\(.*\)" URL.*,\1\2,g' > /dev/stderr
    echo -n "Goto> " > /dev/stderr
    read -r dirnum
    
    is_valid_num=`grep '^[0-9]\+$' <<<${dirnum}`
    
    if test -n "${is_valid_num}"; then
        dir2go=`head -n ${dirnum} "${hl}" | tail -n 1 | sed 's,.*URL "\(.*\)".*,\1,'`
        echo "${dir2go}"
    else
        echo "*** Error: Invalid number." > /dev/stderr
        exit 1
    fi


    скрипт написан "на коленке" (скорее как prove of concept) и кое-что тут можно разумеется улучшить (когда появится время, или может ктонить из читателей сделает это и опубликует результат )
    Это уже эволюционировавшая (моя текущая) верися... до этого было несколько нерабочих -- например быстро выяснилось что делать pushd как только мы высичлили dir2go бестолку... вырнувшись из скрипта pwd и dirstack родителя не изменится Фактически задача данного скрипта вывести в stdout строку куда нам следует перейти, а результат будет передан уже в pushd. Собственно по этой причине мы не можем печатать список, подскаазку ("Goto>") и тямболее ошибки в stdout... поэтому тут все печатается в stderr

    в inputrc добавляем еще пару строк:
    # Show hot dirs list (Alt-RightArrow) (from MidnightCommander) and `pushd` into choosed one
    "\e[1;3C": "\C-a\C-k__=`push_hot_list.sh` && pushd $__\n"
    
    # popd on Alt-LeftArrow
    "\e[1;3D": "\C-a\C-kpopd\n"


    пара Ctrl-a (уже знакомый нам beginning-of-line) и Ctrl-k (kill-line) обеспечит нам пустую командную строку перед выполнением (ну тоесть чтобы не выполнять лажу если вдруг Alt-Left было нажато посередине какойто другой команды)

    Итак:
    // нажимаем Alt-Right
    zaufi ~ $ __=`push_hot_list.sh` && pushd $__
    /home/zaufi/.mc/hotlist
         1  /storage
         2  /media
         3  /usr/local/share/my-gentoo-configs
         4  /etc/paludis
         5  /usr/portage
         6  /var/log
         7  /var/log/everything
    Goto> 5
    /usr/portage ~
    zaufi /usr/portage $
    
    // нажимаем Alt-Left
    zaufi /usr/portage $ popd
    ~
    zaufi ~ $


    Ок! Не будем останавлиавться на достигнутом Приделаем управление этим листом как в MC Собственно надо то добавлять и удалять entries в данный список.
    Наиболее замороченое это удаление. Решаем опять же похожим скриптом:
    #!/bin/sh
    #
    # $Id: $
    #
    # remove_from_hot_list.sh
    #
    
    hl=${HL:-~/.mc/hotlist}
    test -w "${hl}" || { echo "* No hotlist file exists yet or write permissions is not granted *" > /dev/stderr && exit 1; }
    
    cat -n "${hl}" | sed 's,\(.*\)ENTRY "\(.*\)" URL.*,\1\2,g' > /dev/stderr
    echo -n "Remove> " > /dev/stderr
    read -r dirnum
    
    is_valid_num=`grep '^[0-9]\+$' <<<${dirnum}`
    
    if test -n "${is_valid_num}"; then
        sed -i "${dirnum},+0 d" "${hl}"
    else
        echo "*** Error: Invalid number." > /dev/stderr
        exit 1
    fi


    Для добавления в список достатчоно echo -- можно обойтись и без скрипта. Назначаем кнопочки в inputrc: Alt-Up == добавить, Alt-Down == удалить:
    # Add current directory to MC's host list
    "\e[1;3A": "\C-a\C-kecho \"ENTRY \\\"$PWD\\\" URL \\\"$PWD\\\"\" >> ~/.mc/hotlist"
    
    # Remove directory from MC's host list
    "\e[1;3B": " \C-a\C-krm_from_hot_list.sh\n"


    Не будем насильно нажимать ввод на удалении давая возможность подумать, стоит ли действительно добавлять текущий каталог в хотлист

    Even more magic! More! More!! More!!!

    Как видно из вышескзанного большенство команд использует Ctrl-a,Ctrl-k для очистки содержимого строки перед выполнением команды. Очевидно это не всегда удобно... Ну мало ли может мы писали чегота важного (длинного и сложного) в момент когда нам приспичело перейти в другой каталог по Alt-Right или Ctrl-Up. Не хотелось бы потерять эти измненения... Пофтыкав еще в маны (секцию про Killing and Yanking, а также про kill--ring) родилось такое решение: Ctrl-k на сам деле помещает содержимое текущей командной строки в kill-ring, и все что нужно это просто достать его после выполнения команды... Возмем для примера Alt-Left (наш заbindеный popd) и перепишем его так:
    # popd on Alt-LeftArrow
    "\e[1;3D": "\C-a\C-kpopd\n\C-y"


    Ctrl-y (yank) после ввода это и есть извлечение из kill-ring того что только что было стерто по Ctrl-k

    // echo просто чтоб показать что у нас есть куда возвращаться по popd ;)
    zaufi /usr/portage $ echo $DIRSTACK
    /usr/portage /tmp
    
    // печатаем чонить...
    zaufi /usr/portage $ some big and complex command here
    
    // нажимаем Alt-Left
    zaufi /usr/portage $ popd
    /usr/portage
    
    zaufi /tmp $ some big and complex command here
    // вуаля! все что печатали осталось не тронутым!!! :)


    Но оч быстро обнаруживается неприятный момент... ;( -- если строка была пустая то из kill-ring таки достается предидущее содержимое...
    // нажимаем Alt-Left в пустой строке
    zaufi /tmp $
    zaufi ~ $ some big and complex command here
    // и видим предидущую удаленную строку ;(


    На этот раз в мане ничо хорошего не нашлось чтобы обрулить неприятность... Коллективный разум подсказал решение: добавлять пробел в конец строки перед тем как надо будет удалять (помещать в kill-ring) командой Ctrl-k., а после Ctrl-y добавлять еще и \b (backspace) который будет затирать добавленый пробел... Таким образом Ctrl-k будет работать как минимум с пробельной строчкой, или текстом заканчивающимся на пробел, если таковой был напечатан в командной строке до выполнения. А после восстановления (пробела или теста оканчивающегося пробелом) backspace затрет то что мы добавили )

    Таким образом наш binding для Alt-Left приобретает следующий вид:
    # popd on Alt-LeftArrow
    "\e[1;3D": "\C-e \C-a\C-kpopd\n\C-y\b"

    т.е.:
  • идем в конец строки
  • добавляем пробел
  • удаляем все до начала строки (помещаем в kill-ring)
  • выполняем `popd` (с нажанием ввода)
  • восстанавливаем удаленный текст
  • мочим добавленый пробел


    переписав по новому алгоритму навигацию по каталогам получим:

    # Bind `cd ..` to Crtl-Up
    "\e[1;5A": "\C-e \C-a\C-kcd ..\n\C-y\b"
    
    # Bind `cd -` to Crtl-Down
    "\e[1;5B": "\C-e \C-a\C-kcd -\n\C-y\b"
    
    # Show hot dirs list (Alt-RightArrow) (from MidnightCommander) and `pushd` into choosed one
    "\e[1;3C": "\C-e \C-a\C-k__=`push_hot_list.sh` && pushd $__\n\C-y\b"
    
    # popd on Alt-LeftArrow
    "\e[1;3D": "\C-e \C-a\C-kpopd\n\C-y\b"
    
    # Add current directory to MC's host list
    "\e[1;3A": "\C-a\C-kecho \"ENTRY \\\"$PWD\\\" URL \\\"$PWD\\\"\" >> ~/.mc/hotlist"
    
    # Remove directory from MC's host list
    "\e[1;3B": "\C-e \C-a\C-krm_from_hot_list.sh\n\C-y\b"


    Ну и на закуску... Осознав полезность kill-ring вернемся к нашим grep'ам -- Былобы здорово после выполнения поиска иметь в командной строке исходный текст... Ну там может поправить чонить в нем чтоб продолжить искать Теперь это просто:

    # Bind grep substring in all files to F3 (execute immediately)
    "\eOR": "\C-a\C-kgrep --color=always -nir \"\C-y\" *\n\C-y"
    
    # Bind grep substring in C/C++ files to F4 (execute immediately)
    "\eOS": "\C-a\C-kgrep --color=always -nr --include=*.{c,h,cc,hh,icc,tcc,cxx,hxx,cpp,hpp} \"\C-y\" *\n\C-y"


    Злоключение

    Собственна на этом злоключения и заканчиваются Вот на этой радостной ноте я уже порядком подустал писать сей опус (ммогут быть очепятки ))... Желаю всем приятной работы в консоле, новым мега идей, и конечно же фидбэка (т.е. если ктонить заимпрувит тут чонить оч хотелось бы видеть чего и как ))
  •  
    Подождите ...
    Wait...
    Пока на собственное сообщение не было ответов, его можно удалить.