Как перестать беспокоиться и полюбить командную строку, часть 1

This article is protected by the Open Publication License, V1.0 or later. Copyright © 2005 by Red Hat, Inc.
Original article: http://www.redhat.com/magazine/004feb05/features/bash/

Перевод: © Иван Песин

Введение

Вам всегда говорили, что нужно писать код, который легко поддерживать. Все эти модные книги по экстремальному программированию (Extreme Programming, XP) и любые курсы по компьютерным наукам придают огромное значение комментариям в коде и другим указаниям из серии "пейте-пейте-рыбий-жир-будете-здоровы". Эта статья, как и ее вторая часть, посвящена совершенно противоположному: нечитабельному, труднопонимаемому, одноразовому коду. Но и необходимому коду— редактору, который мы будем использовать, и который будет доминирующим фактором в том, каким способом мы будем писать наш код. Ведь редактор этот — командная строка интерпретатора bash.

Первая часть статьи рассматривает этот многогранный редактор и магию, которую можно плести, комбинируя фундаментальные концепции UNIX со здоровым пренебрежением к общественной безопасности. Вторая часть будет немного более узко специализированна и сфокусируется на использовании пройденного в первой части материала в сочетании с вездесущим спасательным инструментом системного администратора — языком perl. Однако, наша цель это не прогулка по стране неправильного кода и сплетни о ней. Наоборот, наша цель — стать более эффективными, решать проблемы, которые в противном случае заняли бы значительно больше времени, ну и, возможно, всего лишь возможно, произвести впечатление, раз уж мы этим занялись. В конце концов, любая "достаточно развитая технология неотличима от магии.1"

В конце концов, любая "достаточно развитая технология неотличима от магии."

Актеры

Перед тем, как углубиться в изучение этих мантр, нам стоит обсудить, что дает нам использование bash. Ведь, если perl — это магия, которую мы варим, то bash — это котел, в котором мы ее варим. Важнейшим свойством всех командных интерпретаторов в UNIX (и даже не только в UNIX, правда в меньшей степени) является возможность взять вывод команды и сохранить его в файл или послать его в качестве ввода другой команде — другими словами, перенаправление ввода-вывода. Практически любой прием, рассматриваемый в этой статье включает в себя перенаправление через один или более каналов.  Простые команды, такие как grep https /etc/services | grep -v udp, демонстрируют нам, что проще использовать команду grep дважды, чем возится с, вероятно, сложным регулярным выражением для достижения того же результата — вывести все строки файла /etc/services, которые содержат слово https и не содержат udp.

Но bash это не только оболочка для запуска внешних программ. В него встроен полный набор конструкций, присущий любому языку программирования: условные операторы if и case, переменные (и даже массивы), операторы циклов, такие как for и while. bash даже содержит документацию на себя самого, доступную с помощью команды help. В любое время, если у вас возникнет вопрос по синтаксису любой команды bash, например: каким оператором закрывается блок if — fi или endif, все что вам нужно сделать, это вызвать help с именем команды в качестве аргумента, например: help if.

Часто приходится выполнять разные операции над определенным набором файлов. Если операция простая, то средство для ее выполнения уже может существовать — к примеру, команда rm * удалит все файлы в текущем каталоге. Однако, если необходимо произвести более сложные операции, скажем "удалить все символические ссылки в текущем каталоге" или "удалить в текущем каталоге все файлы, содержащие слово «фиалка»", шансы решить эту проблему с помощью одной команды невелики.

Использование же конструкций bash, таких как for и if, делает решение таких задач простым. Рассмотрим первый пример — удаление всех символических ссылок в каталоге:

for FILE in *
do
if [ -l $FILE ]
then
rm $FILE
fi
done

Если перевести это на обычный язык, получится следующее: взять каждый файл в текущем каталоге, присвоить его имя переменной FILE, проверить, является ли этот файл символической ссылкой и если является — удалить его.

Вероятно, наименее очевидной частью приведенной конструкции является оператор
if [ -l $FILE ]. В отличии от многих языков, [ и ] не являются группирующими символами, как круглые скобки. На самом деле, [ это имя встроенной функции, а ] — просто завершающий символ. Команда [ является аналогом команды test, которая умеет выполнять широкий набор тестов, таких как проверка на совпадение строк, существование файла или, как в нашем случае, определение, является ли файл символьной ссылкой. Полный список операций можно узнать, введя команду help test. Определенно, стоит посмотреть этот список, чтобы узнать как много проверок, которые в других языках заняли бы немало строк, очень просто выполняются в bash.

Иногда, можно увидеть вышеприведенный код в сокращенной форме:

for FILE in *
do
[ -l $FILE ] && rm $FILE
done

или даже:

for i in *; do [ -l $i ] && rm $i; done

Оба этих варианта являются просто более компактной формой записи. В первом случае, оператор && работает аналогично тому, как он работает в языках C или Perl — если первое условие истинно (файл является символической ссылкой), то вычислить второе условие (удалить файл). Кроме того, также как и в C, и в Perl, bash не будет вычислять выражение справа от оператора && (или ||), если это не требуется. Таким образом, если проверка даст отрицательный результат, команда rm выполнена не будет. Третий вариант просто заменяет название переменной FILE на i (стандартную переменную цикла) и представляет все в одной строке. Обратите внимание на символы "точка с запятой" — они очень важны, без них bash выдаст ошибку синтаксиса.

Связывание команд воедино

Еще одной распространенной конструкцией bash является встраиваемая подстановка (inline expansion). Другими словами, исполняется некая команда, а ее вывод подставляется в текущую команду. Например, команда:

echo "The current time is: $(date)"

выдаст приблизительно следующее:

The current time is: Thu Feb 3 20:50:35 EST 2005

Часто также можно увидеть другой оператор — обратные кавычки:

echo "The current time is: `date`"

Эти два варианта, в принципе, выполняют одну и ту же операцию. Однако, форма $() поддерживает вложенность и определенно легче читается, а потому более предпочтительна.

Поскольку раскрытие может происходить в любом месте, его можно использовать в операторе for. Основной синтаксис оператора for выглядит так: VARIABLE in LIST; do COMMANDS; done, где LIST это список значений, разделенных пробелом. Ниже приведен пример создания десяти файлов от 1.txt до 10.txt:

for i in $(seq 1 10); do touch $i.txt; done

Этот цикл создает 10 пустых файлов с именами от 1.txt до 10.txt. Кроме того, такой цикл может использоваться для быстрого и простого повторения какой-либо команды заданное количество раз  (переменная цикла не обязательно должна использоваться в теле цикла).

Другим применением $() является создание списка файлов для других команд. Например, чтобы узнать сколько строк содержат слово fedora во всех текстовых файлах текущего каталога, достаточно выполнить:

wc -l $(grep -l fedora *.txt)

Здесь мы знакомимся с командой grepgrep это стандартная утилита, а не встроенная команда bash. Она является одной из самых важных команд, используемых при написании сложных скриптов. По существу, grep выполняет поиск в файле заданной строки или шаблона. Формат вызова очень простой: grep PATTERN FILE [ FILE ... ]. PATTERN это регулярное выражение, которое может быть и сложным, но в нашем случае мы использовали шаблон fedora, который просто соответствует строке fedora. Обычно, grep выводит имя файла и соответствующую строку, но в нашем случае мы указали опцию -l, которая говорит выводить только имя файла — очень удобно, когда нужно оперировать с файлами, содержащими искомый шаблон.

Но, как всегда, и в этой бочке меда есть ложка дегтя: как и в большинстве вещей, связанных с компьютерами, здесь есть ограничение. В нашем случае, это ограничение длины одной командной строки. И хотя, по-умолчанию, эта длина достаточно велика для обычного ввода и редактирования, при использовании конструкции $() и даже обычной подстановки по шаблону, можно быстро достичь максимального значения. Если бы у нас в каталоге было, скажем, 5000 файлов и все они содержали слово fedora, нам могло бы не хватить доступной длины командной строки. Решение этой проблемы есть, и оно, так же как и grep, не является частью bash, но тоже чрезвычайно полезно.

Нашим решением является команда xargs. О том, как использовать эту команду можно написать отдельную статью, но если кратко, то xargs — это просто вариант команды $(), который позволяет не заботиться о длине командной строки. Например, предыдущая команда grep -l fedora *.txt трансформируется в:

find . -name '*.txt' -maxdepth 1 | xargs grep -l fedora

Вот тебе на! Это явно посложнее. Давайте пока не будем вдаваться в анализ команды find, а просто пока представим, что она выводит список всех .txt-файлов в текущем каталоге (т.е. делает тоже самое, что и команда ls *.txt, но мы не можем использовать шаблон *.txt, ведь в каталоге слишком много файлов. Чтобы обойти этот барьер, мы передаем команде find этот шаблон в одинарных кавычках — таким образом командный интерпретатор не выполнит подстановку, а передаст его команде). А дальше вступает в работу xargs, которой, в качестве параметра, передается нужная нам команда. Не смотря на внешнюю простоту этой части команды, на самом деле за ней кроется нетривиальная работа с данными.

Команда xargs читает данные со стандартного ввода и формирует команду для выполнения. Параметры xargs определяют начало команды. Другими словами, формируемая команда будет начинаться с grep -l fedora, а далее к ней начинает присоединяться все, что считывается из стандартного ввода. xargsзнает максимальный размер командной строки и при его достижении, считывание приостанавливается, а сформированная команда выполняется. После этого процесс повторяется до тех пор, пока не будет обработан весь ввод. 

Но мы так и не получили число искомых строк — только сами строки, содержащие слово fedora. Ага! Ведь эти строки есть вывод команды xargs и мы можем взять этот вывод и отправить его команде wc, и тоже при помощи все тоже команды xargs:

find . -name '*.txt' -maxdepth 1 | xargs grep -l fedora | xargs wc -l

Ну вот — то, что нам и было нужно. Конечно, теперь эта команда стала сложнее, но и гораздо надежнее. Теперь она будет работать с любым количеством файлов, будь то один или миллион.

find

На первый взгляд, команда find в нашем примере выглядела скорее сложной, чего, честно говоря, нельзя сказать о большинстве других команд. В частности, ее ключи начинаются с одного дефиса,  а не с двух. Но если оставить в стороне вопросы стиля, find это до крайности полезная команда. В отличии от bash и perl, реализация которых всюду одинакова, используемый вариант find зависит от вашей UNIX-системы. В общем, есть две категории — GNU find и все остальное. GNU find немного потворствует лентяйству, а в статье используется синтаксис именно GNU find, так что имейте в виду, что на других UNIX-подобных операционных системах, примеры без соответствующих изменений, могут не пройти.

Как свидетельствует имя, команда find предназначена для поиска. В нашем случае — поиска файлов. find выделяется очень тонкими возможностями поиска файлов по заданному набору критериев. Одним особенно полезным свойством find является то, что по-умолчанию она выполняет поиск в подкаталогах. Это значит, что команда:

find /tmp -name '*.txt'

найдет все .txt-файлы в каталоге /tmp и его подкаталогах. Обычно так и нужно, поскольку вы работаете с поддеревом каталогов. Однако, иногда необходимо обработать файлы, находящиеся только в текущем каталоге, или, в крайнем случае, на один уровень ниже. Вот почему в нашем предыдущем примере появилась опция -maxdepth. Она ограничивает количество уровней, обрабатываемых командой find.

Часто вы можете встретится с использованием команд find и xargs, но не с целью решения проблемы длинной командной строки. В командном интерпретаторе трудно указать шаблон "все .txt-файлы в текущем каталоге и всех его подкаталогах". Конечно, вы можете использовать шаблоны *.txt */*.txt */*/*.txt, но в данном случае мы перечислили всего лишь три уровня подкаталогов, чего просто не достаточно для глубокой иерархии каталогов.

Кроме того, здесь есть один подводный камень. Предположим, мы хотим сжать все .txt-файлы в дереве каталогов. С нашими знаниями, это уже достаточно просто:

find /path/to/tree -name '*.txt' | xargs gzip

Вот тут-то и кроется подводный камень: а что если один из файлов содержит в своем имени пробел? Пробелы используются xargs для разделения имен файлов при чтении информации из стандартного ввода. Потому, имя /foo/bar/a file.txt будет воспринято как два имени — /foo/bar/a и file.txt. Естественно, это не то, что нам нужно. И поскольку такая проблема весьма часто встречается, и find, и xargs имеют в своем арсенале специальную опцию, позволяющую работать в такой ситуации правильно — вместо пробела, для разделения имен файлов использовать символ нуль (ASCII 0).

find /path/to/tree -name '*.txt' -print0 | xargs -0 gzip

Чтобы посмотреть, что при этом происходит, попробуйте выполнить только команду find. В зависимости от вашего терминала, вы, скорее всего, увидите одну длинную строку с идущими подряд именами файлов. Но на самом же деле, между именами файлов вставлен нуль-символ. А параметр -0 команды xargs говорит, что считываемые имена файлов разделены нулевым символом. На практике, большинство файлов не содержат пробелы в своем имени, но когда такое встречается, важно знать как с этим обходится (точно так же, как сами команды find и xargs являются решением, когда исчерпывается допустимая длина командной строки).

Другой крайне полезной возможностью find является поиск файлов, которые были недавно созданы или изменены. Часто бывает нужно узнать какие файлы были изменены за последние несколько дней. Например, чтобы вывести все файлы в вашем домашнем каталоге, которые были модифицированы за последние два дня, выполните:

find ~/ -mtime -2

А для того, чтобы найти файлы, которые не менялись последние два дня, измените параметр -mtime:

find ~/ -mtime +2

Можно также искать файлы по времени последнего доступа (atime) или создания (ctime). Как и встроенная в bash команда test, у find очень большой набор опций. Рекомендуется прочитать ее руководство (не просто для справки, вы сможете почерпнуть представление о гибкости этой удивительной команды).

Иногда, вывод команды не соответствует желаемому. Например, 'find' не выводит данные в алфавитном (точнее, лексикографическом) порядке. Точно также, duне выводит файлы в порядке убывания (или возрастания) их размера. Вместо этого, мы должны использовать другую команду для сортировки вывода, и называется эта команда, соответственно, 'sort'. Она может обрабатывать данные любого размера (зависит лишь от размера вашего диска) и сортировать их в числовом и лексикографическом порядках, начиная с любой позиции в строке (не только с первого символа каждой строки). Например, предположим, что вам нужно найти файлы с наибольшим количеством строк в дереве каталогов:

find /usr | xargs wc -l | sort -n

Здесь мы опять видим наших друзей find и xargs. Их функция в данном примере теперь ясна. А вот дальше начинает действовать sort. Вывод wc, как мы знаем, по-умолчанию состоит из количества строк и имени файлаsort без параметров сортирует начиная с первого символа каждой строки в лексикографическом порядке. Опция же -n говорит, что сортировать нужно в числовом порядке. В результате, файлы с меньшим количеством строк будут выведены в начале списка, а с большим — в конце.

Но, конечно, вывод команды будет слишком большим, особенно если нам нужно найти, скажем, пять самых длинных файлов. К счастью, есть способ отобрать из вывода команды только лишь первые или последние строки. Команды, выполняющие эти действия, называются, соответственно, head и tail. Обе работают по существу одинаково — читают стандартный ввод и выводят либо только первые, либо только последние строки. Применив их в нашем примере, мы получим следующее:

find /usr | xargs wc -l | sort -n | tail

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

Вкус перла

Другой большой программной, которая нас интересует, является, конечно же, сам perl. Исполняемый файл perl — это не только интерпретатор скриптов, он также имеет много ключей командной строки, что делает его очень хорошо приспособленным для использования в командной строке в качестве фильтра или редактора данных, поступающих от или предназначенных для других программ. Но, для начала, мы должны попробовать просто выполнить стандартные операторы языка perl, поскольку это основа нашего дальнейшего обсуждения. Это делается с помощью опции -e:

perl -l -e 'print 1024 * 1024'

В результате выполнения этой команды, мы увидим результат умножения 1024 на 1024 (опция -l, которую мы почти всегда будем использовать, указывает на необходимость вывода символа новой строки после выполнения каждого оператора вывода; в качестве эксперимента, опустите ее и посмотрите на результат). Фактически, я часто просто вызываю perl -leкогда мне нужно посчитать что-то несложное, вместо запуска калькулятора — почти всегда быстрее что-то сделать в командной строке, чем запускать отдельное приложение.

Завершая эту часть статьи, давайте взглянем на то, чем мы займемся в следующей. Одной из наиболее распространенных операций с файлом или набором файлов является замена одной строки на другую. Безусловно, вы можете запустить свой любимый редактор и внести такое изменение в один, или даже несколько, файлов. Но что, если нужно изменить десятки и сотни файлов? К счастью, есть простое и красивое решение, использующее командную строку и perl:

perl -p -i -e 's/XFree86/x.org/g' file1.txt file2.txt ...

В двух словах, эта команда заменяет все вхождения строки XFree86 на x.org во всех перечисленных файлах. Легко представить, как можно скомбинировать эту команду с утилитами find и xargs для обработки большого количества файлов:

find /tmp/library -print0 | xargs -0 perl -p -i -e 's/XFree86/x.org/g'

Но что означают ключи -p и -i? Ответ на этот вопрос, равно как и на многие другие будет дан во второй части этой статьи.

Заключение

Надеюсь, вы почувствовали вкус к магическим приемам, которые, при творческом подходе, можно делать с bash и другими распространенными утилитами, входящими в большинство систем UNIX. Эта статья всего лишь знакомит вас с набором кирпичиков, которые вы можете взять и использовать в своей работе. Во второй части статьи мы разовьем полученные знания и исследуем некоторые более сложные приемы, с детальным их описанием, что поможет вам стать настоящим асом командной строки.

Об авторе

Чип Тернер работает в Red Hat три года и является ведущим архитектором Red Hat Network. Он, также, поддерживает пакет perl, все пакеты perl-модулей и пакет spamassassin для Fedora Core и Red Hat Enterprise Linux, а также является автором обвязки perl RPM2 для работы с RPM и многих других модулей в CPAN. В свободное время он любит играть со своей собакой и спорить без особой на то причины.

1 Артур Кларк