Журнал LinuxFormat - перейти на главную

LXF97:Командная строка

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

Linux: Фильтры и каналы

Хватайте трубку и ласты и ныряйте в загадочный подводный мир фильтров и каналов вместе с Крисом Брауном.

Когда я впервые столкнулся с Unix и Linux, мое внимание сразу привлекла классная вещь: набор средств командной строки – фильтров, и штука под названием «каналы», их соединяющая. Вместе они образовывали невероятно мощную компонентную архитектуру, предназначенную для обработки потоков текстовых данных.

Фильтр – это программа, которая считывает один входной поток, как-то преобразует его и выводит результат в один выходной поток (Рис. 1). По умолчанию выходной поток (также называемый стандартным выводом или просто stdout) связан с окном терминала, где запущена программа, а входной поток (стандартный ввод или просто stdin) – с клавиатурой. Однако на практике фильтры редко используются для обработки данных, набираемых на клавиатуре вручную. Если фильтру через аргумент командной строки передать имя файла, он откроет этот файл и считает его содержимое вместо считывания данных со стандартного ввода stdin (Рис. 2) – такая схема применяется гораздо чаще. Во врезке напротив показано несколько простых команд. Большинство фильтров сами по себе не делают ничего впечатляющего. Гораздо интереснее использовать их в сочетании друг с другом.

У фильтра есть один входной поток и один выходной поток. Получая файл, фильтр читает его, игнорируя стандартный ввод.

Сочетание фильтров: каналы

Канал позволяет направить данные с выходного потока одного процесса на входной поток другого. Обычно они используются для подключения стандартного потока вывода к стандартному потоку ввода. Создать канал в командной строке очень просто: для этого используется символ | (вертикальная черта). Например, когда в командную оболочку поступает следующая команда:

$ sort foo.txt | tr ‘[A-Z]’ ‘[a-z]’,

то оболочка одновременно запускает два отдельных процесса для программ sort и tr и создает канал, передающий стандартный вывод команды sort (процесс-источник) на стандартный ввод команды tr (процесс-получатель). Схема этого процесса показана на Рисунке 3. Приведенная команда tr, если вам интересно, заменяет все символы из набора [A-Z] соответствующими символами из набора [a-z], то есть преобразует текст из верхнего регистра в нижний.

tr ‘[A-Z] ‘ ‘[a-z]’


Полуфильтры

Многие команды Linux, которые на самом деле не являются фильтрами, выводят результаты своей работы в стандартный поток вывода stdout и могут стать началом канала. К ним относятся ls, ps, df, du и многие другие. Например, команда

 $ ps aex | wc

пересчитает запущенные на компьютере процессы, а команда

 $ ls -l | grep ‘^l’

выведет только символические ссылки для текущего каталога. (Регулярное выражение ^l означает строки, начинающиеся с буквы ‘l’.)

Менее распространен другой вариант полуфильтров. Это программы, считывающие данные из потока ввода, но не выводящие их в поток вывода stdout. В голову приходят только команда просмотра файлов less, утилита печати lpr и почтовый клиент для командной строки mail. Такие программы могут использоваться как окончание канала; например, команда

 grep syslog /var/log/messages | less

выводит строки файла /var/log/messages', относящиеся к системному журналу syslog, с помощью команды less. Команда less используется как окончание канала очень часто.

Потоковый редактор sed

Потоковый редактор sed поддерживает автоматическое редактирование текста и является более гибким по сравнению с большинством фильтров. Он считывает свои входные данные строку за строкой из стандартного потока ввода или из заданного файла, применяет к ним одну или несколько операций редактирования и выводит строки результата в стандартный поток вывода. У этого редактора целый набор команд, но самая полезная – команда замещения. Для начала рассмотрим простой пример использования sed для подстановок в тексте, введенном с клавиатуры (stdin):

 $ sed ‘s/rich/poor/g’
 He wished all men as rich as he
 He wished all men as poor as he
 And he was as rich as rich could be
 And he was as poor as poor could be
 ^D
 $

Наверное, нужно немного пояснить. Команда sed заменяет все имеющиеся строки ‘rich’ строками ‘poor’. Суффикс /g говорит о том, что это глобальная замена – если в строке больше одного вхождения, то все они будут заменены (без этого суффикса было бы заменено только первое). Сразу после команды sed мы видим две пары строк. Первая строка в каждой паре – это текст, который мы ввели с клавиатуры, а вторая – результат работы редактора, выведенный в стандартный поток вывода.


А вот более полезный пример, в котором мы используем регулярное выражение для удаления всех полей строк файла /etc/passwd, кроме первого:

$ sed ‘s/:.*//’ /etc/passwd
root
daemon
bin
sys
... остальные строки пропущены ...

Здесь текст, который мы заменяем, определяется регулярным выражением :.*; оно соответствует фрагменту от первого двоеточия до конца строки. Строка, которой будет заменен такой фрагмент (между вторым и третьим слэшами), пуста, что приводит к удалению всех фрагментов, соответствующих регулярному выражению. Так мы получаем список имен пользователей из файла passwd. Однако давайте проясним кое-что: отнюдь не меняет файл /etc/passwd. Он просто считывает его и выводит измененные строки в стандартный поток вывода.

Awk

Названный в честь своих создателей Ахо [Aho], Вайнбергера [Weinberger] и Кернигана [Kernighan], awk представляет собой отдельную категорию: это развитый язык программирования с переменными, циклами, условиями и функциями. Программа на языке awk состоит из одной или нескольких пар «шаблон-действие»:

шаблон { действие }
шаблон { действие }

Шаблон – это некое условие, применяемое к каждой строке; если строка соответствует шаблону, над ней выполняется указанное действие. Если шаблон опущен, действие применяется ко всем строкам. Если опущено действие, строка целиком выводится на экран. Awk отлично подходит для обработки текстовых данных, разделенных на поля (столбцы); он считывает входные данные строку за строкой и автоматически разбивает их на поля, доступ к которым производится через специальные переменные $1, $2, $3 и т.д.

Для демонстрации работы awk мы будем использовать небольшой набор географических данных, за правильность которых ручается атлас мира Collins Complete World Atlas. Они включают названия стран, их площадь, население (в тысячах), языки и валюту. Для краткости ограничимся четырьмя строками, которые выглядят вот так:

Страна		Площадь	Население	Языки					Валюта
Албания        28748	3130		Албанский,греческий			Лек
Греция		131957	11120		Греческий				Евро
Люксембург	2586	465		Немецкий,французский			Евро
Швейцария	41293	7252		Немецкий,французский,итальянский	Франк

Эти данные находятся в файле с именем geodata.

Многие awk-программы так просты, что их можно ввести с командной строки как аргумент awk. Например:

$ awk ‘{ print $1, $5 }’ geodata
Страна Валюта
Албания Лек
Греция Евро
Люксембург Евро
Швейцария Франк

Эта программа на языке awk' содержит единственную пару «шаблон-действие». Шаблон пропущен, поэтому действие применяется к каждой строке – это вывод первого и пятого полей, то есть названия страны и ее валюты.


Теперь давайте найдем страны, которые используют евро. Чтобы применить условие к пятому полю, можно использовать шаблон, например, таким образом:

$ awk ‘ $5==”Евро” { print $1 }’ geodata
Греция
Люксембург

А как определить суммарное население всех стран? Это потребует двух пар «шаблон-действие»: одна из них будет срабатывать в каждой строке и последовательно накапливать значения численности населения (из третьего столбца), а вторая сработает только в конце и выведет результат. Можно было бы ввести эту программу через командную строку, как и предыдущие примеры, но на сей раз она немного длиннее, и удобнее записать ее в отдельный файл – я назвал его totalpop.awk. Он выглядит так:

{ sum += $3 }
END { print sum }

У первого действия нет шаблона, поэтому оно применяется к каждой строке. Sum – просто имя созданной мною переменной. В awk переменные не нужно объявлять заранее, они начинают существовать после первого упоминания имени (тогда же им присваивается нулевое значение). Второе действие использует специальный шаблон ‘END’, который выводит результат. Он срабатывает только один раз – после того, как все входные данные обработаны.

Теперь я могу запустить awk и заставить интерпретатор считать программу из файла totalpop.awk, например, так:

$ awk -f totalpop.awk geodata
21967

Обратите внимание, что для указания имени файла с программой мы используем ключ -f.

Попробуем найти страны, население которых говорит на указанном языке. Это немного сложнее, так как для каждой страны указан список языков, разделенных запятыми. Нам нужно разделить этот список на составляющие. К счастью, для этого в языке awk есть встроенная функция. Вот полный текст программы, которую я назвал language.awk:

{ NL = split($4, langs, “,”);
   for (i=1; i<=NL; i++)
     if ( langs[i] == “Греческий”)
       print $1
}

Здесь только одно действие, с которым не связан ни один шаблон (поэтому оно применяется к каждой строке), однако это действие с вызовом функции, набором переменных, циклом и условием уже больше похоже на настоящую программу. Вот пример ее запуска:

$ awk -f language.awk geodata
Албания
Греция

Одной командой покажем, что awk умеет выполнять арифметические действия – получим список стран с плотностью населения более 150 человек на квадратный километр:

$ awk ‘$3*1000/$2 > 150’ geodata
Люксембург 2586 465 Немецкий,французский Евро
Швейцария 41293 7252 Немецкий,французский,итальянский Франк

Обратите внимание, что в этой программе есть шаблон, но нет действия. Как видите, в этом случае используется действие по умолчанию – строка целиком выводится на экран.

Язык awk содержит массу возможностей, о которых здесь не рассказано, и многие пишут на этом языке программы гораздо длиннее, чем в четыре строчки! Впрочем, моя задача не в том, чтобы показать, какие длинные бывают программы, а в том, что и короткие программы делают немало полезного.


Перехват стандартного вывода

В большинстве наших примеров предполагается, что стандартный вывод связан с окном терминала (вариант по умолчанию). Однако перенаправить его в файл очень просто, использовав оператор оболочки >. Например, команда

$ sort foo.txt | tr ‘[A-Z]’ ‘[a-z]’ > sorted.txt

запускает ту же самую цепочку команд, что и раньше, но перенаправляет стандартный вывод stdout команды tr в файл sorted.txt. Заметьте: перенаправление выполняет сама оболочка, команда tr просто выводит данные в stdout и не знает, да и знать не хочет, куда направляются эти данные.


Пример с решением

Давайте объединим все, о чем мы говорили, в последний пример. Наша задача – подсчитать частоту появления слов в образце текста, а текст сегодня утром взят из Евангелия от Марка, Глава 6 (версия короля Якова; загрузить его можно из центра электронных текстов библиотеки Вирджинского университета – http://etext.virginia.edu/kjv.browse.html) [король заказал первый перевод Писания на англ. яз. – прим. ред.]. Теоретически мы можем ввести все решение с командной строки, но лучше оформить его в виде скрипта wordfreq.sh. Мы будем добавлять в этот файл по одной строке и контролировать результат работы скрипта на каждом этапе.

Каждая строка файла, который я скачал с сайта Вирджинского университета, соответствует одному стиху и начинается с его номера и двоеточия. Например, строка для стиха 42 выглядит так:

 42: And they did all eat, and were filled.

[И все ели, и насытились.] Я сохранил этот текст в файле mark.txt.

Чтобы определить количество слов, воспользуемся ассоциативными массивами, но сперва надо слегка почистить входной текст. Для начала избавимся от этих номеров. Мы можем использовать команду замены редактора sed. Эта команда и будет первой строкой нашего скрипта wordfreq.sh:

 #!/bin/bash
 sed ‘s/^[0-9]*:\ //’ $1

Первая строка файла – это часть механизма скриптов в Linux. Она предписывает операционной системе использовать оболочку bash для интерпретации скрипта. Вторая строка – классический пример использования sed. Использование команды замены ясно из предыдущих примеров; «старый шаблон» использует регулярные выражения, соответствующие фрагменту «начало строки, далее возможны несколько цифр, затем двоеточие и пробел», а «новый шаблон» между вторым и третьим прямыми слэшами пуст. Таким образом, номера в начале строк удаляются. $1 в конце этой строки еще немного приоткрывает нам механизм скриптов в Linux – он будет заменен аргументом командной строки, который мы передадим скрипту. Это имя файла, который должен обработать sed.

Создав наш двустрочный скрипт, мы должны разрешить его выполнение:

 chmod u+x wordfreq.sh

Теперь можно запустить скрипт, передав ему в качестве аргумента имя файла:

 ./wordfreq.sh mark.txt

Стих 42 из нашего примера теперь выглядит так:

And they did all eat, and were filled.

Затем я решил сделать подсчет слов нечувствительным к регистру. Проще всего для этого преобразовать все символы верхнего регистра в исходном тексте в нижний регистр, что легко делается командой tr. Теперь наш скрипт стал на три строки длиннее:

 #!/bin/bash
 sed ‘s/^[0-9]*:\ //’ $1 | \
 tr ‘[A-Z]’ ‘[a-z]’

Мы добавили ко второй строке обозначение канала | и обратный слэш, означающий, что команда продолжится в третьей строке. На самом деле такие выражения не нужно разбивать на отдельные строки: я сделал это лишь затем, чтобы упростить чтение скрипта. Команда tr на третьей строке – тоже классика. Она означает «заменить каждый символ из набора A-Z соответствующим символом из a-z». Если запустить наш новый скрипт из трех строк, то строка из примера будет выглядеть так:

 and they did all eat, and were filled.

На следующем шаге избавимся от этих нудных знаков препинания. Это тоже можно сделать с помощью команды tr (используя ключ -d). Итак, теперь наш скрипт будет выглядеть следующим образом:

#!/bin/bash
 sed ‘s/^[0-9]*:\ //’ $1 | \
 tr ‘[A-Z]’ ‘[a-z]’ | \
 tr -d ‘[.,;:]’

Последняя строка просто удаляет все символы из набора [.,;:]. После запуска этой версии скрипта наша строка выглядит так:

 and they did all eat and were filled

Вот это уже можно отдать на съедение awk: именно здесь мы и определим частоту появления слов. Основная идея состоит в прокручивании каждого отдельного слова в документе, используя само слово как индекс в ассоциативном массиве и просто увеличивая на единицу соответствующий элемент этого массива. Обработав весь документ, мы сможем вывести индекс и значение каждого элемента массива – то есть само слово и сколько раз оно встречается в документе. Я решил записать программу в отдельный файл с именем wordfreq.awk; таким образом, сейчас у нас есть два скрипта, с которыми можно поработать – это скрипт оболочки wordfreq.sh и программа на языке awk wordfreq.awk.

Наш скрипт выглядит следующим образом:

 sed ‘s/^[0-9]*:\ //’ $1 | \
 tr ‘[A-Z]’ ‘[a-z]’ | \
 tr -d ‘[.,;:]’ | \
 awk -f wordfreq.awk

а программа на языке awk выглядит так:

 { for (i=1; i<=NF; i++)
     w[$i]++
 }
 END { for (word in w)
          print word, w[word]
 }

awk-программа содержит два действия. Проще начать с первого, которое применяется ко всем строкам, так как не содержит шаблона. Это действие обрабатывает все поля во входной строке (т.е. все слова) и увеличивает на единицу соответствующий элемент ассоциативного массива w. Имена переменных i и w я выбрал сам, переменная NF – внутренняя переменная языка, которая содержит количество полей в текущей строке. Выражение w[$i]++, которое увеличивает на единицу соответствующий элемент ассоциативного массива w – главная часть этой программы. Все остальное лишь обеспечивает его работу.

Второе действие в этой программе срабатывает только один раз, после того, как все входные данные обработаны. Оно просто перебирает элементы массива w и выводит индекс элемента в массиве (само слово) и значение элемента (сколько раз это слово встретилось в тексте).

Вывод нашего скрипта в корне изменился и теперь выглядит так:

 themselves 4
 would 3
 looked 1
 taken 1
 of 27
 sit 1
 privately 1
abroad) 1
name 1
and 134
…следующие строк этак 400 пропущены.

Наконец, мы можем вывести эти данные в более удобной форме. Для этого отсортируем их по числу вхождений (чтобы список начинался с чаще всего встречающихся слов) и применим head, чтобы оставить только 10 первых строк. Окончательная версия скрипта будет выглядеть так:

#!/bin/bash
sed ‘s/^[0-9]*:\ //’ $1 | \
tr ‘[A-Z]’ ‘[a-z]’ | \
tr -d ‘[.,;:]’ | \
awk -f wordfreq.awk | \
sort -nr -k2 | \
head

Обратите внимание на флаги команды сортировки. -n включает сортировку по численному значению, -r реверсирует результаты сортировки, а -k2 сортирует данные по значению второго поля. Теперь у нас есть желаемые данные о частоте появления слов:

and 134                          of 27
the 64                           him 26
he 38                            unto 23
they 31                          to 22
them 31                          his 21


Сделайте это по-разному

Как и большинство вещей в жизни, наша задача решается разными способами. Вместо использования ассоциативных массивов awk можно разбить содержимое файла на отдельные слова (по одному слову на каждой строке) командой tr, затем упорядочить этот список по алфавиту (вхождения одного и того же слова будут расположены друг за другом), после чего командой uniq подсчитать, сколько раз появляется каждое слово, отсортировать список по убыванию и выделить первые десять строк. Такой скрипт будет выглядеть следующим образом:

#!/bin/bash
sed ‘s/^[0-9]*:\ //’ $1 | \
tr ‘[A-Z]’ ‘[a-z]’ | \
tr -d ‘[.,;:]’ | \
tr ‘ ‘ ‘\n’ | \
sort | \
uniq -c | \
sort -nr | \
head

а результат его работы выглядит так:

134 and                          27 of
64 the                           26 him
38 he                            23 unto
31 they                          22 to
31 them                          21 his

Он точно такой же, как и предыдущий, только поля расположены в обратном порядке. Чтобы понять, как работает эта программа, попробуйте создать ее по шагам (на каждом этапе вводите произвольные данные) и анализируйте результат ее работы. LXF


Регулярные выражения

Команда Что она делает
head /etc/passwd Показывает первые десять строк файла /etc/passwd.
grep ‘/bin/bash$’ \ /etc/passwd Показывает строки из /etc/passwd о пользователях, использующих bash для входа в систему.
sort /etc/services Сортирует сервисы из файла /etc/services в алфавитном порядке.
wc /etc/* 2> /dev/null Считает строки, слова и символы во всех файлах каталога /etc; сообщения об ошибках игнорируются.
Персональные инструменты
купить
подписаться
Яндекс.Метрика