Вам всегда говорили, что нужно писать код, который легко поддерживать. Все эти модные книги по экстремальному программированию (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)
Здесь мы знакомимся с командой grep
. grep
это стандартная утилита, а не встроенная команда 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
это до крайности полезная команда. В отличии от 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. Эта статья всего лишь знакомит вас с набором кирпичиков, которые вы можете взять и использовать в своей работе. Во второй части статьи мы разовьем полученные знания и исследуем некоторые более сложные приемы, с детальным их описанием, что поможет вам стать настоящим асом командной строки.