null

Useless use of cat

Сейчас в интернете размещен ряд интересных статей на тему «Useless use of *»
Заинтересовавшись этой темой, я решил написать небольшую статью, которая, как мне кажется, немного подробнее объяснит принципы принятия решения относительно использования утилит, поможет выбрать пользователю наиболее подходящую к данному контексту утилиту.
Думаю, начать стоит с описания механизма передачи данных в unix (в т.ч. и unix-like системах).
Согласно философии unix, передача данных между процессами или тредами чаще всего происходит в простом plaintext-формате (т. к. он является наиболее универсальным интерфейсом). Когда нам, пользователям, требуется подвергнуть поток данных обработке какими-либо утилитами, мы часто используем конвейеры или перенаправления потоков. Механизм перенаправления потока основан на изменении файлового дескриптора перенаправляемого потока.
Например, данная конструкция

$ cmd 2>file1 5>file2


вместо обычного поведения, перенаправит поток 2 в file1 и поток 5 в file2.
Механизм конвейера немного отличен от рассмотренного выше перенаправления потоков (хотя, тоже является разновидностью перенаправления). Например, конструкция

$ cmd1 | cmd2


по сути передаст вывод cmd1 на вход cmd2, но это будет реализовано путем связывания файловых дескрипторов: дескриптора 1 для cmd1 и дескриптора 0 для cmd2. Таким образом, поток данных напрямую переходит от одной утилиты — другой.
Сразу отметим, что если мы хотим подвергнуть поток обработке несколькими утилитами, преимущественнее использовать конвейер (происходит выигрыш за счет более высокой скорости передачи данных).

Давайте теперь, с этих позиций, оценим некоторые использования различных утилит.

$ cat file | wc -l


Конструкция, очевидно, подсчитывает количество строк в файле file
Однако, рассмотрим ближе: первым делом, утилита cat считывает файл и посылает его в поток выхода, затем утилита wc по конвейеру получает этот поток и обрабатывает.
Давайте теперь рассмотрим несколько более удачных примеров:

$ wc -l <file
$ wc -l file
$ cat file1 file2 | wc -l


В первых двух примерах, очевидно, происходит то-же, что мы уже наблюдали — подсчет строк в файле file. Однако, мы не прибегаем к вызову «сторонней» утилиты cat, а либо перенаправляем поток из файла на вход утилиты wc (то есть, в данном случае, файл открывается средствами shell), либо предоставляем утилите wc самостоятельно его прочитать (что является более предпочтительным, т. к. wc сможет проанализировать файл самостоятельно).

Третий пример показывает конструктивное использование утилиты cat: мы используем ее по назначению — конкатенируем file1 и file2 и передаем выходной поток утилите wc для подсчета строк.
Кстати, наиболее часто встречается бесполезное использование утилиты cat в аналогичной связке с утилитой grep:

$ cat file | grep pattern


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

$ $(cat file)


однако, как мы уже выяснили, перенаправлять потоки используя утилиты вроде cat — не конструктивно. Для этого преимущественнее использовать стандартные средства unix. И опять, более удачным примером послужит помещение файла средствами shell, без запуска лишних утилит:

$ $(<file)


Вообще, существует множество, казалось-бы удачных примеров использования cat в составе других утилит.
Например:

$ cat file1 file2 | grep pattern | wc -l


Казалось бы всё хорошо, но конструкция не верна (хотя cat тут используется не без пользы — конкатенация двух файлов). Более правильный вариант:

$ cat file1 file2 | grep -c pattern


Тут уже не используется дополнительная утилита wc... Но можно лучше:

$ awk 'BEGIN{count=0}/pattern/{count++}END{print count}' file1 file2


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

$ cat file1 file2 | grep -c pattern


например, для суммарного объема файлов всего 7 Мб, составит в 5.24 раза меньше, чем то-же действие с использованием утилиты awk. По всей видимости, это связано со сложностью внутренней работы утилиты awk.
Вернемся к утилите cat. Как мы видим, использовать cat для чтения всего одного файла чаще всего бесполезно. Однако, существуют ситуации, когда такое действие вполне себя оправдывает. Например, если нам требуется поместить содержимое файла между выводом двух других утилит (cmd1 и cmd2):

$ { cmd1; cat file; cmd2; } | command


Также cat достаточно удобно использовать для написания красивых и удобочитаемых конструкций. Например, если в зависимости от условия, нам требуется обработать или же наоборот не обрабатывать поток утилитой cmd2, можно использовать

$ [ condition ] && filter=cmd2 || filter=cat
$ cmd1 | ${filter} | cmd3



Таким образом, мы теряем время на определение переменной, но не теряем время на if-then-else-fi в явном виде, и не загромождаем код.
Также утилиту cat можно с пользой использовать, если нам требуется скрыть, что мы вызываем скрипт/утилиту из интерактивного терминала:

$ cat script
tty
wc -l "$@"
$ ./script file
/dev/pts/0
5
$ cat file | ./script
not a tty
5



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

Назад
korg

 

Коротко о себе

Работаю в компании Tune-IT, администрирую инфраструктуру компании и вычислительную сеть кафедры Вычислительной ТехникиСПбНИУ ИТМО.

Интересы: администрирование UNIX и UNIX-like систем и активного сетевого оборудования, написание shell- и perl-скриптов, изучение технологий глобальных сетей.
Люблю собирать GNU/Linux и FreeBSD, использовать тайлинговые оконные менеджеры и писать системный софт.