Kodomo

Пользователь

Главные команды UNIX: работа с содержимым файлов

В рассказе про историю UNIX я говорил, что UNIX строился вокруг нескольких ключевых идей, среди которых были:

Из всех этих идей следует, что в UNIX должно быть много небольших утилит для работы с текстом. Так и есть.

Этот конспект не претендует на дублирование содержимого стандартной документации UNIX, а только даёт первое представление о том, что есть и зачем оно нужно. Если вы будете достаточно долго пользоваться UNIX, рано или поздно вам придётся читать документацию и по этим основным утилитам тоже, и вы обнаружите, что они умеют гораздо больше, чем я здесь рассказал.

cat

cat(1) – сокращение от слова conCATenate – выводит последовательно на стандартный вывод (на экран) содержимое всех файлов, которые ей дали на вход.

   1 $ echo 1 > a
   2 $ echo 2 > b # мы создали два файла, с которыми будем играться
   3 $ cat a # выводит содержимое файла a на экран
   4 1
   5 $ cat a b # склеивает содержимое файлов a и b и выводит их на экран
   6 1
   7 2

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

Перенаправления

Здесь необходимо сделать небольшое отступление и рассказать о свойствах интерпретатора командной строки (шелла).

Интерпретатор умеет перенаправлять стандартный ввод (который по умолчанию сваливается на экран) и стандартный вывод в другие места:

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

команда >> файл делает то же самое, но дописывает в конец файла, если файл уже был не пустой.

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

команда1 | команда2 перенаправляет стандартный вывод команды1 на стандартный ввод команды2 (это почти синоним того, чтобы сказать команда1 > файл; команда2 < файл) – это называется pipeline (трубопровод).

Соответственно,

   1 $ cat < a # команда cat не получила аргументов и ждёт текст со стандартного ввода
   2 1

head, tail

Утилита head(1) показывает голову файла – первые несколько строк или букв. tail(1) – хвост.

   1 $ echo 2 >> a
   2 $ echo 3 >> a
   3 $ echo 4 >> a # теперь файл a содержит строки 1, 2, 3, 4
   4 $ head -n 2 a
   5 1
   6 2
   7 $ head -n 3 a | tail -n 2
   8 2
   9 3

По умолчанию head и tail показывают первые/последние 10 строк.

Наиболее полезные аргументы у них:

sort, uniq

Утилита sort(1) сортирует входные строки. Например:

   1 $ echo 1 >> a
   2 $ echo 2 >> a # теперь у нас в a есть строки 1 2 3 4 1 2
   3 $ sort a
   4 1
   5 1
   6 2
   7 2
   8 3
   9 4
  10 $ tail -n 3 a | sort
  11 1
  12 2
  13 4

Утилиту sort можно заставить сортировать не по первому столбцу; её можно убедить, что разделителем между столбцами является не пробел; её можно заставить сортировать не в лексикографическом порядке, а интерпретировать числа и сортировать по возрастанию чисел; её можно убедить сортировать не по возрастанию, а в обратном порядке – всё это смотрите в документации.

В пару к sort есть полезная утилита uniq(1), основное назначение которой в том, чтобы оставлять из идущих подряд строк только различные (но если одинаковые строки идут не подряд, uniq не будет вспоминать, что он их уже видел):

   1 $ sort a | uniq
   2 1
   3 2
   4 3
   5 4
   6 $ uniq a
   7 1
   8 2
   9 3
  10 4
  11 1
  12 2

Утилиту uniq можно заставить писать число одинаковых вхождений подряд, писать только строки, для которых есть повторы или только те, для которых повторов нет; аналогично sort'у, её можно заставить учитывать различия только в некоторых заданных столбцах – смотрите в документации.

grep

Утилита grep(1) ищет строку/выражение в содержимом файла / файлов.

Наиболее полезные аргументы:

$ grep -rl "Hello, world" ~ # найти все файлы в домашней директории, в которых есть Hello world
...

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

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

В выражениях можно использовать скобки для обозначения, к чему относится следующая звёздочка.

Выражение вида a|b|c|d можно сокращать до [abcd] или даже до [a-d] (если a, b, c, d – это именно буквы).

Для выражения (|A) есть сокращение: A? (выражение находит пустую строку или то же, что находит A).

Примеры:

Если вы в регулярном выражении хотите найти какой-нибудь из символов, который используется для разметки самих регулярных выражений, то перед таким символом нужно ставить \ (backslash, он же обратный слэш, он же обратная косая черта):

Начало строки в регулярном выражении обозначается символом ^, конец строки – $.

Развитий синтаксиса регулярных выражений за рамками этого минимума существует довольно много, и даже для этого минимума существуют разные способы обозначений. Тот синтаксис, который я здесь привёл, сейчас набиолее широко принят, он используется в языках программирования (perl, python, tcl, гугловская реализация), в древних утилитах UNIX этот синтаксис называется "extended regular expressions".

См. regex(5)

Пример:

   1 $ egrep [24] a # все строки, содержащие цифру 2 или 4
   2 2
   3 4
   4 2
   5 $ egrep -rl '^>' . # все файлы в текущей директории, содержащие строку, начинающуюся с > -- вероятно, это будут fasta-файлы
   6 ...

tr

Самая простая команда для преобразования (а не фильтрации) потока текста – это tr(1). Она умеет далать только одно: побуквенную замену.

Пример:

   1 $ tr 23 34 a
   2 1
   3 3
   4 4
   5 4
   6 1
   7 3
   8 $ cat a | head -n 2 | tr 1 5
   9 5
  10 2

sed, awk

Как я рассказывал в истории UNIX, первым конкурентным преимуществом UNIX перед другими системами на заре его существования было то, что в нём был текстовый редактор (а заодно и компилятор и система "вёрстки" текстов). Этот текстовый редактор назывался ed, это был текстовый редактор командной строки, в нём были команды "распечатать текущую строку" (p), "заменить текущую строку" (c), "применить команду к заданному диапазону (g/регулярное выражение/ команда), "заменить в строке что-то на что-то" (s/что-то/что-то/).

Из этого редактора родилась утилита grep. Собственно, её название расшифровывается именно так: g/RE/p.

Ещё из него родилась утилита sed (string editor), которая убирала из него все интерактивные возможности, но доделывала его до полноценного, хотя и страшного, языка программирования. sed нацелен на простое автоматизированное редактирование строк.

Из sed родился более навороченный и человекоприемлемый язык awk, ориентированный на редактирование "табличных" данных.

Ещё немного позже из ed родился текстовый редактор командной строки ex, а когда к нему приделали визуальный режим, его назвали vi – но это уже совсем другая история.

Полный синтаксис sed(1) не сложен, но как правило, он и не нужен. Про него в 90% случаях достаточно знать один способ использования:

Чтобы sed использовал привычные нам регулярные выражения, нужно вызывать его с флагом -r:

$ cat a | sed -r 's/[24]/5/g'
1
5
5
3
1
5

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

Аналогично и с awk: это полноценный язык, из которого на деле нужна примерно одна команда: awk '{print $1}' распечатает первое слово из каждой строки.

Пример

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

В шелле есть команда history, которая печатает последние 10000 команд, которые я исполнял. (10000 – это настройка; у меня настроено 10000, но по умолчанию настроено меньше). Выдача её выглядит так:

   1 $ history | tail
   2  1996  cd
   3  1997  ls
   4  1998  man uniq
   5  1999  man regexp
   6  2000  man 5 regexp
   7  2001  man 4 regexp
   8  2002  man -a regexp
   9  2003  man -a regex
  10  2004  man sed
  11  2005  history | tail

Сначала нужно отрезать номера строк. Для этого годится sed или awk. Я достаточно знаю awk, чтобы это было проще сделать через него:

   1 $ history | awk '{$1 = ""; print $0}' | tail
   2  ls
   3  man uniq
   4  man regexp
   5  man 5 regexp
   6  man 4 regexp
   7  man -a regexp
   8  man -a regex
   9  man sed
  10  history | tail
  11  history | awk '{$1 = ""; print $0}' | tail

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

Т.е. нужно некоторые интересные нам символы (мне интересны |, ;, {, () заменить на перенос строки (в sed, как и в большинстве языков программирования, он обозначается как \n):

   1 $ history | awk '{$1 = ""; print $0}' | sed 's/[|;({]/\n/g' | tail
   2  history 
   3  awk '
   4 $1 = ""
   5  print $0}' 
   6  sed 's/[
   7 
   8 
   9 
  10 ]/\n/g' 
  11  tail

Получилось и некоторое количество мусора из-за того, что символы, по которым я разбивал, встречаются ещё и, например, в этом же самом скрипте, но это не так страшно.

Теперь нужно оставить только первую колонку – добавляем | awk '{ print $1 }'.

Далее нужно посчитать количество вхождений каждой из команд. Это умеет делать uniq, но чтобы он сработал, историю нужно посортировать:

   1 $ history | awk '{$1 = ""; print $0}' | sed 's/[|;({]/\n/g' | awk '{print $1}' | sort | uniq -c | tail
   2      22 w
   3       2 wc
   4       3 wget
   5       1 which
   6       3 who
   7       1 whois
   8       1 zcat
   9       1 }
  10       1 
  11       2 '

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

   1 $ history | awk '{$1 = ""; print $0}' | sed 's/[|;({]/\n/g' | awk '{print $1}' | sort | uniq -c | sort -n | tail
   2      52 man
   3      60 do
   4      60 done
   5      60 for
   6      64 less
   7      77 sudo
   8     224 cd
   9     233 hg
  10     246 vim
  11     536 ls

Теперь из полученного можно уже глазами выбрать наиболее интересное...

wc

Ещё одна полезная утилита – wc(1) – она считает количество букв, слов и строк в файле.

   1 $ wc a # 6 строк, 6 слов, 12 байт
   2  6  6 12 a
   3 $ cat a | wc -l # сколько строк в a?
   4 6

file

Ещё одна полезная штука – file(1) – она пытается определить тип файла по его содержимому.

   1 $ file a
   2 a: ASCII text

Она умеет как минимум отличать простой текст (в названии формата почти всегда должно быть слово text) от бинарных форматов, которые можно читать только специальными программами, например, ворд (тогда в названии формата будет слово data). Разумеется, всякие общепринятые штуки типа исполняемых файлов, вордовских документов и популярных языков программирования она умеет распознавать и может написать про них немного более содержательной информации.

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

less

less(1) – это просмотрщик текстовых файлов. (По-английски этот класс программ называется pager или paging tool; иногда и в русских переводах встречалось уродское слово пагинатор).

Первый просмотрщик текстовых файлов в UNIX назывался more – это была программа, которая печатала файл на принтер (ведь другого способа общаться с компьютером тогда не было), но не целиком, а только одну страницу (чтобы случайно не истратить всю бумагу), а если файл одной страницей не ограничивался, то в конце страницы печатала "-- MORE --" в смысле "есть ещё буквы в этом файле" и ожидала, пока пользователь нажмёт пробел, чтобы продолжить печать файла. В честь этой подписи просмотрщик и назвали.

Когда для проекта GNU делали свой просмотрщик, никак не могли придумать, как его назвать, и в конце концов, назвали как попало, сказали, что "less is more than more", и остались довольны.

Про less полезно знать несколько вещей: