Потоки данных

    BASH Статья посвящена работой с потоками данных в bash. Я постарался написать ее наиболее доступным и простым языком, чтобы было понятно даже новичкам в Linux.


    В одной из моих статей мы рассматривали запись звука в файл с помощью команды:

    cat /dev/audio > /tmp/my.sound

    Эта команда читает файл (устройство) /dev/audio с помощью команды cat и перенаправляет информацию из него в файл /tmp/my.sound (с помощью оператора >).

    У каждой программы существует 3 системных потока: stdout, stderr, stdin.

    stdout


    Стандартный поток вывода данных для программ. Например, когда мы пишем команду ls, то список папок и файлов она выводит именно в этот поток, который отображается у нас в консоли:

    $ ls
    bin incoming pub usr

    stderr


    Поток вывода ошибок. Если программа не смогла сделать все как надо — она пишет именно в этот поток. Например, когда rm пытается удалить несуществующий файл:

    $ rm example.txt
    rm: example.txt: No such file or directory

    stdin


    Поток ввода данных. А вот это довольно интересный и удобный поток. Например, его использует вэб-сервер, когда просит интерпретаторы выполнить скрипты через CGI. Мы тоже можем попробовать:

    $ echo '<?php echo «Hello world»; ?>' | php
    Hello world

    В этом примере мы встретили оператор перенаправления потока вывода. Мы остановимся на нем позже.

    Подробнее:
    http://ru.wikipedia.org/wiki/Стандартные_потоки


    Перенаправление потоков


    Для начала рассмотрим перенаправление потоков в файлы, устройства и другие потоки.

    $ ls >1.txt

    В этом примере мы направили stdout команды ls в файл 1.txt. Читаем его:

    $ cat 1.txt
    bin incoming pub usr

    Да, все успешно записалось.

    Теперь попробуем направить stderr команды rm:

    $ rm example.txt 2>1.txt

    Здесь мы использовали номер потока stderr (2). По умолчанию оператор > перенаправляет поток stdout, который имеет номер 1. Чтобы направить другой поток, надо перед оператором > поставить его номер.

    Мы можем направлять одни потоки в направлении других:

    $ rm exmple.txt >1.txt 2>&1

    В этом примере мы направили поток stdout в файл 1.txt, а затем направили stderr туда же, куда направлен stdout с помощью оператора & перед номером потока.

    Теперь давайте поиграем с потоком stdin. Например, я хочу найти все папки ".svn" в некотором проекте и удалить:

    cd myproject
    find .

    Команда find с параметром. выводит в stdout все вложенные папки и файлы, которые находит в данной папке и во всех вложенных.

    Теперь нам надо выбрать только папки с именем ".svn":

    find . | grep -e '/.svn$'

    Оператор | перенаправляет stdout одного приложения в stdin следующего. То есть все строки найденные с помощью find пошли в команду grep, которая выбирает строки по определенным условиям и выводит их. Здесь условие — это регулярное выражение, которое говорит о том, что строка должна заканчиваться на "/.svn".

    Нужные папки мы выбрали, осталось их удалить.

    rm -Rf `find . | grep -e '/.svn$'`

    И снова новый оператор: `. Он забирает stdout из команды, которую он окружает и вставляет в данное место как строку.

    Получается, что мы запросили все файлы, выбрали из них папки с именем ".svn" и отдали результат как аргументы команде rm. В этом случае у нас будут проблемы если имена файлов и папок содержат пробелы. Исправляем ситуацию:

    find . | grep -e '/.svn$' | xargs rm -Rf

    Теперь мы отдаем нужные файлы команде xargs, которая вызывает rm -Rf и в качестве параметров использует свой stdin построчно. Задача решена.

    Каждый может помочь развитию данной серии статей, поделиться своим опытом. Добро пожаловать: http://www.linuxman.ru. Все изменения в Вики я буду со временем переносить и в Хабр.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 43

      –9
      Во-первых, ЗАЧЕМ (уже в который раз) давать простейшие вещи, которые и так есть в любой книжке по Linux, в самоучителях, просто понятны из примеров кода, на хабр?
      Во-вторых, сами догадаетесь? :)
        +15
        Цель — описать понятным и простым языком) В книгах понятного и простого описания я не встретил, к сожалению. Пишу для тех, кто тоже не встретил.
        • UFO just landed and posted this here
            0
            А по-моему, вполне понятно все написано в книжках. Разобраться совсем недолго… Примеры книг могу привести, если надо.

            А так за статью(не считаем ошибки) респект. Немногие сейчас отваживаются их писать)
        • UFO just landed and posted this here
            +1
            Прошу прощения, давно не писал на хабре :)
            +5
            Команда find с параметром * выводит в stdout все вложенные папки и файлы, которые находит в данной папке и во всех вложенных.


            Неверно. Это сделает команда find .

            Не забываем, что globbing (замена wildcards) происходит в шелле. И * по умолчанию не расширяется на dot entries в текущем каталоге.
              +4
              Спасибо. Помогли разобрать разрозненные и сумбурные знания в моей голове :)
                +4
                Ещё спрашивают «зачем такое постить»…
                Столько тонкостей в процессе общественного обсуждения и деления опытом узнаёшь, о которых даже не догадывался.
                Аффтар, пиши истчё!!! (простите за эту фразу)
                  +9
                  За «find *|grep ...» убить готов.
                  Автор, не пиши больше такое.
                    –1
                    Это учебный пример, который полностью решает свою задачу: рассказывает о том, как можно управлять потоками. Дальше я оставляю за читателями право посмотреть man'ы всех описанных команд и глубже изучить вопрос.
                      +7
                      Лично мне всегда казалось, что примеры должны как _правильно_ использовать приложения, а то, те кто не в теме, увидят find * | grep и будут везде пихать, пока им не откроют глаза на find ./ -name «блабла».
                      Так что соглашаюсь с предыдущим автором, за find *|grep — надо ругать.
                        +1
                        Окей, виноват, согласен :)
                        0
                        неужели нельзя было придумать примеров получше, чем кривое использование find не по назначению? хотя бы «find /path |grep '/.svn'»
                          0
                          Да find-то как раз по назначению. Просто не стоит к нему grep прибивать гвоздями, когда можно find . -type d -and -iname '.svn'.
                          • UFO just landed and posted this here
                      +1
                      Кстати, чтобы не было проблем с xargs и другими спецсимволами, лучше всегда пользоваться xargs -d '\n'. В связке с find ещё есть find -print0 | xargs -0
                        0
                        Символ перевода строки разрешён в большинстве файловых систем для linux, потому, строго говоря, разделение результатов поиска этим символом чревато. Не думаю, что это имеет большое для практики значение (если, конечно, вы не пишите вдруг CGI-скрипты), но второй способ абсолютно корректен и ничем не хуже первого.
                          0
                          Что чревато, ясно. Поправлюсь: имелся ввиду xargs без find, где '\n' зачастую — самый безболезненный вариант.
                      • UFO just landed and posted this here
                          0
                          Обожаю Diablo II =)
                            +1
                            Я думал я один это заметил :)
                            +2
                            спасибо за статью, публикуйте и дальше, кому не нужно пусть не читают!
                            только не забывайте хорошенько все разжевывать :)
                            вот, например, про xargs узнать бы по подробнее с доходчивыми примерами
                              +1
                              почему бы вам самостоятельно не разобраться в вопросе (гугл + man xargs к вашим услугам) и не написать статью? а не ждать, пока кто-то разжуёт для вас?
                              0
                              Нельзя не вспомнить про будильник настоящего юниксоида:

                              sleep 8h; cat /dev/urandom > /dev/dsp
                                0
                                Настоящий юниксоид напишет так:

                                sleep 8h && cat /dev/urandom > /dev/dsp
                                  0
                                  Вы удивитесь, но мой код тоже будет работать.
                                    0
                                    Будет (собственно я не говорил, что Ваш код в корне не верен), но настоящий юниксоид напишет:
                                    sleep 8h && cat /dev/urandom > /dev/dsp

                                    Так как код:
                                    sleep 8h; cat /dev/urandom > /dev/dsp
                                    при отсутствии /bin/sleep сразу начнет звенеть…

                                    Ваш пример довольно безобидный, но когда начинают писать
                                    cd /dir1/dir2/dir3; rm *
                                    становится страшно…
                                      0
                                      Да, всё так. И кстати, не звененть, а шуршать :)
                                +1
                                Интересно, а как можно разветвить поток (нужно получить из одного потока два, с которыми потом нужно делать разные манипуляции) «стандартным» способом, а то использую для этого awk, как микроскоп для заколачивания гвоздей…
                                awk '{ print $0 > поток 1; print $0 > поток 2; }'
                                  0
                                  man tee
                                    0
                                    Спасибо за совет, но, прочитав мануал, я понял эту команду как:

                                    tee [-ai] [file ...]
                                    The tee utility copies standard input to standard output, making a copy
                                    in zero or more files. The output is unbuffered.

                                    cat ./file.txt | grep 'key' | tee ./file2.txt | grep 'subkey' > ./file3.txt

                                    Как ./file2.txt направить не в физический файл, а в пайп, где я еще хочу сделать sort?
                                      0
                                      (grep 'key' file1 | tee file2 | grep 'subkey') && sort file2
                                  0
                                  Спасибо, интересно для новичка
                                    +3
                                    У тебя есть такой пример:

                                    find . | grep -e '/.svn$' | xargs rm -Rf

                                    Извини, у меня нет цензурных слов. За такое я отрываю разработчикам руки. Такой код в скриптах — это бомба замедленного действия, она срабатывает редко, но неожиданно и разрушительно. О grep-е после find-а и xargs rm вместо -delete уже говорили, но это можно попытаться оправдать тем, что примеры учебные и искуственные. А вот опасность этого примера оправдать нельзя!

                                    Автор, создай у себя, для эксперимента директорию где-нибудь в темпе:

                                    $ mkdir -p 'документы'/'.svn' 'документы на удаление'/'.svn'

                                    В «документы» положи очень важные и ценные тебе документы. В «документы на удаление» — ерунду. А теперь выполни эту команду и сожалей, что директория «документы» исчезла. Не .svn в этой директории, а «документы» целиком!

                                    Писать так — опасно, учить других писать так — во сто крат опасней! Читать маны перед написанием учебной статьи — напротив, не только неопасно, но и крайне полезно.
                                      0
                                      Угу, этот пример тоже хорош:

                                      rm -Rf `find . | grep -e '/.svn$'`

                                      Такой же убийственный
                                        0
                                        Спасибо за дельный комментарий.
                                        Но если все таки забыть про учебную составляющую и обратить внимание на саму задачу удаления каталогов .svn, то у меня возник вот какой вопрос: как же правильно написать команду?

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

                                        find. -type d -and -iname '.svn' -delete
                                        find: cannot delete `./документы/.svn': Каталог не пуст

                                        В конце концов появился вот такой вариант:

                                        find. -type d -and -iname '.svn' -execdir rm -Rf .svn \; 2>/dev/null

                                        Есть ли более оптимальный?

                                          0
                                          Обычно пользуюсь find -print0 | xargs -0. А только средствами find… попробуйте

                                          find . -depth \( -path '*/.svn/*' -or -iname '.svn' \) -delete

                                          Хотя поздновато уже, не ручаюсь.
                                            0
                                            Вы меня опередили на минуту. Ещё замечу, что скобки здесь важны, без них условие -path '*/.svn/*' будет выполнено для файлов внутри директории .svn и в соответствии с правилами оптимизации логических выражений действие -delete уже не будет для них запущено.
                                            +3
                                            find ищет по каким-то условиям и выполняет определённое действие с результатом поиска. Дело в том, что по умолчанию find делает -print, то есть выводит результат на стандартный вывод, разделяя имена переводом строки.
                                            xargs читает со стандартного ввода записи, разделяя, их, среди прочего, и пробелами. Таким образом, если find найдёт файл с именем «раз два», то xargs запустит указанную команду с аргументами «раз» и «два». Нам нужно, чтобы find разделял записи разделителем, который не может присутствовать в именах файлов. Среди ext2/3 таких символов два — это нулевой символ (не имеет печатаемого обозначения) и символ прямого слэша. Прямой слэш, кажется, в некоторых файловых системах может являться частью имени файла, потому нам остаётся только нулевой символ.
                                            Как сказано в мануале в первых строках по find: «you should probably consider using ‘-print0’ instead».
                                            Действие -print0 заставляет find вывести на стандартный вывод результаты, разделяемые нулевым символом, а опция -0 у xargs заставляет в качестве разделителя записей принимать только нулевой символ.

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

                                            find /tmp -name core -type f -print0 | xargs -0 /bin/rm -f

                                            Что касается возможности запуска без xargs — для скриптов я бы посоветовал такую конструкцию:

                                            find -depth \( -type f -a -wholename '*/.svn/*' \) -delete -o \( -type d -a -name '.svn' \) -delete


                                            Для применения вручную, после запуска без -delete и изучения списка:

                                            find -depth -wholename '*/.svn*' -delete

                                            При этом, файлы и директории типа .svnlalala тоже будут уничтожены, если присутствуют.
                                              0
                                              Большое спасибо за подробный ответ
                                          0
                                          мощнейшая штука потоки, вот только если в умелых руках… замена ">" на "
                                            –1
                                            > rm -Rf `find. | grep -e '/.svn$'`

                                            Мда…

                                            Как насчет man find в районе exec?

                                            Просто любопытно: вы программы как устанавливаете? ./configure; make; make install?

                                            Only users with full accounts can post comments. Log in, please.