Работаем с историей команд. Часть I. Основы.

Как известно, лень человеческая - помимо слабости, является одним из основных двигателей прогресса. Не чужды ей и простые смертные линуксоиды и юниксоиды вместе взятые. Итак, как помочь человеку попавшему в дебри командного интерпретатора и облегчить его труд и усилить во сто крат силу его? Ответ на сей вопрос прост - надо дать ему в руки, и научить пользоваться таким полезным инструментом, как история команд. Сей инструмент в той или иной мере присутствует во многих программах и системах. В том же многострадальном MS-DOS, было жалкое подобие истории введенных команд, история также присутствовала в NC, FAR и других командных оболочках. Но там они ни в какое сравнение не идут с возможностями присутствующими в любом мало мальски распространенном shell под linux или unix. Далее я буду описывать работу c bash, хотя на сколько мне известно tcsh, csh и некоторые другие интерпретаторы имеют сходный набор команд для работы с историей.

Итак начав работу с командной строкой bash, я обнаружил что с помощью клавиш перемещения курсора можно перемещаться по списку ранее введенных команд. Так когда мне нужна была некоторая команда из ранее введенных я жал клавишу „стрелка вверх“ до тех пор пока нужная мне не появлялась в командной строке, потом я ее корректировал нужным мне образом и жал „Enter“ для ее выполнения. Это конечно значительно экономило время однако, как оказалось, не было наиболее эффективным способом работы.

Итак, все по порядку. Для просмотра списка ранее введенных команд в bash - имеется команда history. По умолчанию она выводит список команд хранящийся в истории. Размер данного списка определяется переменными окружения HISTSIZE - размер списка хранящегося в памяти интерпретатора, а HISTFILESIZE - максимальное количество команд хранящихся в файле истории. По умолчанию этоn файл ~/.bash_history, а его размер - 500 команд. Если вы желаете хранить историю в другом файле, то нужно в .bashrc, задать команду - HISTFILE=~/.vasya_history. Я для себя переопределяю только размер списка команд и размер файла истории, устанавливая их значения в 1000 команд. Итак введя:
	$ history

	    1  history | less
	    2  hg test
	    3  test 333
	    4  lynx www.yahoo.com
	    5  cat /etc/profile.d/colorls.sh
	    6  vim .screen
	    7  vim .screenrc
		..........................
	  305  man bash
	  306  man vfork
	  307  hg lynx
	  308  cd txt/everyday/
	  309  vim history.txt
	  310  histroy

Отсюда видно, что в истории на данный момент находится 310 команд, конечно они все на экране не поместятся, посему если вам надо только последние 20 команд, то можно набрать:
	$ history 20

	  295  vim lib/advhist.sh
	  296  getpg
	  297  vim .bash_logout
	  298  vim .bashrc
	  299  cat .lynxrc
	  300  vim .lynxrc
	  301  ls lib
	  302  cat .bashrc
	  303  getmail
	  304  ls
	  305  man bash
	  306  man vfork
	  307  hg lynx
	  308  cd txt/everyday/
	  309  vim history.txt
	  310  histroy
	  311  history
	  312  history
	  313  fg
	  314  history 20

Таким образом получим только последние 20 команд. Каждая команда имеет свой номер, с помощью которого к ней можно обратится. Если нам надо повторить 302 команду, то просто печатаем:
	$ !302
	cat .bashrc
	# .bashrc

	# User specific aliases and functions
	if [ "$PS1" -a -f ~/lib/advhist.sh ]; then
		. ~/lib/advhist.sh
	fi
	# Source global definitions
	if [ -f /etc/bashrc ]; then
		. /etc/bashrc
	fi
	umask 066

Здесь сначала печатается команда под номером 302 - cat .bashrc, а затем результат ее выполнения.

Приведу список команд для работы с историей:
  • !! - ссылается на предыдущую команду;
  • !n - ссылается на команду под номером n;
  • !-n - ссылается на команду по номером „текущая минус n“;
  • !string - ссылается на команду, начинающуюся с string;
  • !?string[?] - ссылается на команду, содержащую строку string;
  • ^string1^string2[^] - „быстрая замена“, заменяет первое вхождение строки string1 в предыдущей команде, на string2, после чего выполняет полученную команду.
Приведу несколько примеров по использованию вышеприведенных команд. Я часто выполняю приблизительно следующую последовательность:
	$ locate diald.conf
	/etc/diald.conf
	$ cat /etc/diald.conf
	.......

Первой командой я нахожу требуемый файл, а второй вывожу его содержимое. Сей короткий пример можно автоматизировать следующим образом:
	$ locate diald.conf
	/etc/diald.conf
	$ cat `!!`
	cat `locate diald.conf`
	mode ppp
	accounting-log /tmp/dialdlog
	.....

Таким образом я избегаю повторного ввода имени файла. `!!` означает - выполнить предыдущую команду и подставить ее результат в качестве параметра cat. Результирующая команда после подстановки из перечня печатается сразу за командной строкой: cat `locate diald.conf`.После чего выводится результат выполнения команды. На мой взгляд весьма удобно.

Идем дальше:
	$ history 10
	324  hg \'
	325  hg \`
	326  locate diald.c
	327  locate diald.conf
	328  cat `locate diald.conf`
	329  getmail
	330  history 10
	331  ls
	332  ps
	333  history 10
	$ !328
	cat `locate diald.conf`
	mode ppp
	accounting-log /tmp/dialdlog
	..........

Данная команда выполняет 328 команду перечня. Другой способ обратится к этой же команде:
	$ !-6
	cat `locate diald.conf`
	mode ppp
	accounting-log /tmp/dialdlog
	..........

Здесь используется обратная нумерация, то есть номер команды вычисляется как текущая минус 6.

Следующий способ, это когда я помню что команда начиналась со строки cat, чтобы ее повторить я печатаю следующее:
	$ !cat
	cat `locate diald.conf`
	mode ppp
	accounting-log /tmp/dialdlog
	..........

А если я не помню названия начала команды, но помню ее середину diald, то тогда набираем:
	$ !?diald
	cat `locate diald.conf`
	mode ppp
	accounting-log /tmp/dialdlog
	..........

Следующий интересный момент касается исправления неверно введенных команд или их корректировки:
	$ cta /etc/diald.conf
	bash: cta: command not found
	$ ^ta^at
	cat /etc/diald.conf
	mode ppp
	accounting-log /tmp/dialdlog
	...............

В результате опечатки первая команда была введена с ошибкой, вместо cat, было введено cta, естественно интерпретатор не нашел такой команды, о чем и вывел предупреждение. Вторая команда ^ta^at^ - исправляет ошибку, делая замену ta на at. Это намного удобнее чем вызывать предыдущую команду на экран потом подводить курсор под нужный символ, а затем редактировать команду с помощью вставки/удаления символов. Данная возможность особенно удобна при удаленной работе с telnet на медленных линиях.

До настоящего момента при вызове команд из списка, мы ссылались на команду целиком. Но частенько надо повторить не всю команду, а только ее часть.

Пример:
	$ cat /etc/diald.conf
	mode ppp
	accounting-log /tmp/dialdlog
	.........
	$ vim !$

Первой командой cat мы выводим содержимое файла, например для того, чтобы удостоверится что это нужный нам файл. Удостоверившись я хочу его отредактировать, для того, чтобы повторно не набирать имя файла пишу vim !$ - что после подстановки преобразуется в vim /etc/disld.conf. Кратко опишу основные операторы обращения к различным частям предыдущей команды:
  • $ - последний аргумент командной строки;
  • ^ - первый аргумент командной строки;
  • 0 - нулевое слово, другими словами имя команды;
  • N - N-ый аргумент командной строки;
  • x-y - аргументы с x по у командной строки;
  • -y - сокращенное обращение к 0-y;
  • * - все аргументы, синоним 1-$;
  • x* - синоним x-$, другими словами аргументы с номера x до конца строки;
  • x- - синоним x*, но не включает последний аргумент.

Перед всеми описанными операторами, кроме ^ $ * %, необходимо ставить двоеточие.

Для ясности приведу несколько примеров:
	$ echo 1 2 3 4 5 6 7 8
	1 2 3 4 5 6 7 8
	$ echo !$
	echo 8
	8
	$ echo 1 2 3 4 5 6 7 8
	$ echo !*
	echo 1 2 3 4 5 6 7 8
	1 2 3 4 5 6 7 8
	$ echo !:3*
	echo 3 4 5 6 7 8
	3 4 5 6 7 8
	$ echo !^
	echo 3
	3
	$ echo !-2:-3
	echo echo 3 4 5
	echo 3 4 5

Я наиболее часто использую операторы !^, !$, !* - которые ссылаются на первый аргумент, на последний аргумент и на все аргументы предыдущей команды соответственно.

Еще нужно упомянуть о так называемых модификаторах, подробно о них можно почитать на страницах руководства. Но об одном из них я скажу отдельно. Этот заслуживающий внимания модификатор - :p. Он говорит интерпретатору о том, что полученную в результате подстановки команду не надо выполнять, а только напечатать. Например:
	$ echo 1 2 3 4 5 6 7
	$ !!
	echo 1 2 3 4 5 6 7
	1 2 3 4 5 6 7
	$ !!:p
	echo 1 2 3 4 5 6 7
	$ echo !-2:p
	echo echo 1 2 3 4 5 6 7
	$

Как можно заметить - последняя и предпоследняя команды echo, не были выполнены, так как использовался модификатор :p. Вместо этого были выведены результаты подстановки.

Вот в общих чертах и все. Что я имел сказать - я сказал.

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

Работаем с историей команд. Часть II. Усовершенствования.

Разбираясь с функционированием перечня ранее введенных команд, я обнаружил что перечень обладает несколькими отрицательными свойствами. Так при вводе одной и той же команды, она каждый раз оседает в истории. Пример:
	$ echo 1
	1
	$ echo 1
	1
	$ echo 1
	1
	$ history 5
		26  PS1='$ '
		27  echo 1
		28  echo 1
		29  echo 1
		30  history 5

Как видно команда echo 1 была три раза введена и три раза была записана в историю. Для чего не понятно. Это можно устранить вставив строку HISTCONTROL=ignoredups в ваш файл ~/.bash_profile. Вышеприведенный пример будет выглядеть следующим образом:
	$ HISTCONTROL=ignoredups
	$ echo 1
	1
	$ echo 1
	1
	$ echo 1
	1
	$ echo 1
	1
	$ history 4
		42  history 4
		43  HISTCONTROL=ignoredups
		44  echo 1
		45  history 4

Как видно теперь повторяющаяся четыре раза команда echo 1, в перечне встречается только один раз. Но вот от следующей неприятности вышеприведенная опция не спасет:
	$ echo 1
	1
	$ echo 2
	2
	$ echo 1
	1
	$ history 5
		51  history 6
		52  echo 1
		53  echo 2
		54  echo 1
		55  history 5

Здесь команда echo 1, включена опять-таки два раза, что на мой взгляд, весьма неприятно.

Еще один неприятный момент, это то, что история засоряется короткими командами, смысла хранить которые нет. К таким командам можно отнести ls, ps, who, короче, те команды, длинна которых меньше 3-5 символов. У меня таких команд большинство. И что самое обидное, это то что из-за несколько сотен таких коротких команд, а также повторного включения одной и той же команды в перечень, перечень переполняется и из него выбрасываются действительно нужные команды.

Чтобы это исправить, пришлось написать парочку небольших функций облегчающих работу с историей. Скрипт реализующий их можно найти на странице Программы.

Если вкратце, то там реализованы такие функции для работы с историей:
  • hs - записывает историю в $HISTFILE, кроме того выбрасывает все повторяющиеся команды, а также команды длинна которых меньше $MINHISTITEMSIZE, таким образом перечень команд избавляется от мусора. Данная команда выполняется автоматически при выходе из интерпретатора;
  • hr [n] - название от History Reverse. Печатает n - строк истории, причем кроме прямой нумерации печатает обратную, что облегчает обращение к командам с помощью !-n, если n=0, то выводит весь перечень, если n опущено, то печатает только $DEFAULTPRINTSIZE строк истории;
  • hg [string [n]] - название от History Grep. Синоним hg n | egrep -i string, напечатать n - последних элементов истории, в которых встречается строка string, причем в строке могут применяться расширенные регулярные выражения, и она не чувствительна к регистру. Если n опущено, то поиск ведется по всему перечню.
Чтоб было более понятно о чем идет речь приведу пример:
$ hr
    10    348  echo $HISTFILESIZE
     9    349  vim lib/advhist.sh
     8    350  ls
     7    351  PS!='$ '
     6    352  PS1='$ '
     5    353  hr
     4    354  ps
     3    355  PS1='$ '
     2    356  who
     1    357  hr
$ hr 5
     5    354  ps
     4    355  ws
     3    356  who
     2    357  hr
     1    358  hr 5
$ hs		# Выкидывает из перечня короткие и повторяющиеся команды
$ hr		# Проверяем результаты
    10    716  lynx sit/linux
     9    717  getpg
     8    718  lynx www.sit.kiev.ua/linux
     7    719  echo $HISTFILE
     6    720  echo $HISTSIZE
     5    721  echo $HISTFILESIZE
     4    722  vim lib/advhist.sh
     3    723  PS!='$ '
     2    724  PS1='$ '
     1    725  hr
$ hg ping	# Находим все команды в которых присутствует подстрока ping
   116    613  ping www.yandex
    53    676  ping www.yandex.ru
     1    728  hg ping
$ !-53		# Выполняем найденный ping
ping www.yandex.ru
PING yandex.ru (62.118.249.254) from 195.230.153.157 : 56(84) bytes of data.
64 bytes from yandex.ru (62.118.249.254): icmp_seq=0 ttl=235 time=609.5 ms
64 bytes from yandex.ru (62.118.249.254): icmp_seq=1 ttl=235 time=600.0 ms
.....

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

Для использования этих функций, advhist.sh должен находится в ~/lib/advhist.sh, а в ваш .bashrc, необходимо добавить следующие строки:
if [ "$PS1" -a -f ~/lib/advhist.sh ]; then
        . ~/lib/advhist.sh
fi

Для настройки можно изменить следующие переменные окружения:
  • DEFAULTPRINTSIZE - количество печатаемых строк командой hg, по умолчанию равно 10;
  • MINHISTITEMSIZE - строки перечня короче этой величины будут исключены из него при выполнении команды hs, по умолчанию удаляются строки короче 5-ти символов.