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

BASH
Перенаправление в bash даёт нам инструмент для тонкой манипуляции потоками ввода/вывода, создания каналов между командами и т.д. Каждый начинающий unix'ойд может сказать, что делает command > file. Однако, допустим, { { ls -l; } 2>&1 >&3 | cat > file; } 3>&1 поставит в ступор, иной раз, даже бывалого. Конечно, эта команда избыточна и представляет собой всего лишь ls -l 2>file, но сколько смысла заложено в этой строчке. Естественно, памяти доверять такой большой багаж знаний нельзя, поэтому я долго искал в рунете полный мануал по перенаправлениям, но… видимо плохо искал… И написал свой, с блэкджеком и шлюхами. Я не претендую на полноту изложения и широту охвата, и не берусь утверждать, что после прочтения можно будет слёту понимать что-то вроде:

{
  {
    cmd1 3>&- |
      cmd2 2>&3 3>&-
  } 2>&1 >&4 4>&- |
    cmd3 3>&- 4>&-

} 3>&2 4>&1


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

Поехали

Перенаправления в bash — это очень просто, главное понимать что это всего лишь работа с файловыми дескрипторами. Дескриптора файла — это просто число, идентифицирующее файл, тоесть своего рода упрощённый указатель на файл. Когда bash стартует, то открывается три стандартных дескриптора: стандартный ввод или stdin (дескриптор 0), стандартный вывод или stdout (дескритпор 1) и вывод сообщений об ошибках или stderr (дескриптор 2). И в этом легко убедиться, запустив команду lsof:
lsof -ap $BASHPID -d 0-5
COMMAND  PID USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF NODE NAME
bash    3298 root    0u   CHR RW,0x8000  136,1      0t0    4 /dev/pts/1
bash    3298 root    1u   CHR RW,0x8000  136,1      0t0    4 /dev/pts/1
bash    3298 root    2u   CHR RW,0x8000  136,1      0t0    4 /dev/pts/1

Как мы видим, открыто три дескриптора и все они указывают на файл /dev/pts/1, который является псевдотерминалом и используется для имитации реального. Также существуют дополнительные дескрипторы, которые мы можем открывать и закрывать и использовать для хранения стандратных, но об этом позже.

Итак, когда bash стартует таблица дескрипторов выглядит следующим образом:
[0] -> [ /dev/pts/1 ]
[1] -> [ /dev/pts/1 ]
[2] -> [ /dev/pts/1 ]

Когда Bash выполняет команду, он порождает дочерний процесс, который наследует все дескрипторы родительского процесса, тогда и устанавливаются перенаправления, которые Вы указали, а затем запускается сама команда.
1. Элементарное command >file
Яркий пример:
ls -l > filelist.lst

Оператор > является оператором перенаправления вывода, который можно также представить как 1>filelist.lst. При выполнении данной команды bash попытается открыть файл filelist.lst на запись, если файла не существует, то он создается, а если попытки не увенчались успехом, то команда не выполнится.

Bash открывает файл и заменяет дескриптор 1 на дескриптор, который указывает на filelist.lst. Так весь вывод команды будет записан в filelist.lst. И выглядеть всё будет следующим образом:
[0] -> [ /dev/pts/1 ]
[1] -> [ filelist.lst ]
[2] -> [ /dev/pts/1 ]

И это применимо для любого другого дескриптора n. Например перенаправить поток ошибок можно таким образом:
ls -l 2>file

И так это будет выглядеть в таблице дескрипторов:
[0] -> [ /dev/pts/1 ]
[1] -> [ /dev/pts/1 ]
[2] -> [ filelist.lst ]

И, что особо интересно, при помощи оператора > можно открыть новый дескриптор.
Например:
ls -l 3>file

В данном примере это ничего не даст, кроме создания пустого файла file.

Также этот оператор дает нам наикратчайший путь создать пустой файл или обнулить существующий. Этого можно достичь при помощи команды :, которая соответсвует ассемблерному NOP. Итак вниманиеее:
:> file

И это еще не всё) существует короткая запись для перенаправления stdout и stderr. И записывается это как:
ls &>file

оператор &> для перенаправления обоих потоков (stdout и stderr) в file.
Здесь таблица выглядит так
[ 0 ] -> [ /dev/pts/1 ]
[ 1 ] -> [    file    ]
[ 2 ] -> [    file    ]

Существует несколько путей перенаправить оба потока. Вы также можете перенаправить каждый поток поотдельности:
command >file 2>&1

Сначала stdout перенаправляется в file, а затем stderr дублируется, чтоб стать таким же как stdout.

Когда bash видит несколько перенаправлений, то он обрабатывает их слева направо. Давайте пошагово разберем как это происходит.

До запуска любой команды таблица выглядит так:
[0] -> [ /dev/pts/1 ]
[1] -> [ /dev/pts/1 ]
[2] -> [ /dev/pts/1 ]

bash обрабатывает первое перенаправление >file
[0] -> [ /dev/pts/1 ]
[1] -> [ file ]
[2] -> [ /dev/pts/1 ]

Далее bash видит следущее перенаправление 2>&1, теперь дескриптор файла 2 станет копией дескриптора файла 1.
[0] -> [ /dev/pts/1 ]
[1] -> [ file ]
[2] -> [ file ]

Однако будте осторожны, это отнюдь не тоже самое, что command 2>&1 >file!!! Как было сказано выше, bash обрабатывает команды слева направо.
В данном случае, логика действий такова… bash видит 2>&1 и сразу же дублирует stderr в stdout… что выглядит следующим образом:
[0] -> [ /dev/pts/1 ]
[1] -> [ /dev/pts/1 ]
[2] -> [ /dev/pts/1 ]

Далее идет перенаправление stdout в file.
[0] -> [ /dev/pts/1 ]
[1] -> [ file ]
[2] -> [ /dev/pts/1 ]

Тоесть команда перенаправит только stdout в file. Stderr будет выведен на терминал.
2. Элементарное command <file
Как наверное уже можно было догадаться, оператор < является оператором перенаправления ввода. И будет выглядить так:
[0] -> [ file ]
[1] -> [ /dev/pts/1 ]
[2] -> [ /dev/pts/1 ]

Примеры:

cat <file
read -r line < file
while read a; do break; done <file

Также существует еще две конструкции, которые позволяют нам избавиться от echo text | command:
cat <<EOF
hi 
it's text
EOF

и
command <<<"my text"

Закрыть дескриптор
можно так:
ls -l 2>&-
#и так:
ls -l 1>&-
#и так
ls <&-

На этом покончим с простыми вещами и перейдем к чуть более сложным)

Неименованный канал (конвеер)
Ну полюбому все использовали! Универсальное средство для объединения команд в одну цепочку. Например: ls | sort | uniq

Как же данные из одной команды перетекают в другую? Разберём как это работает)
Дело в том, что в данном случае создается файл (пайп) который открывается с одной соторны на запись, с другой на чтение и выглядит всё это безобразие следующим образом

            ls          |       sort
[0] -> [ /dev/pts/1 ] +--> [0] -> [    pipe® ]
[1] -> [    pipe(w) ]/     [1] -> [ /dev/pts/1 ]
[2] -> [ /dev/pts/1 ]      [2] -> [ /dev/pts/1 ]

И в этом тоже легко убедиться. Вниманиее!!! Эксперимент!!!!!!!

 bash -c 'lsof -a -p $$ -d 0-2' | cat
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF  NODE NAME
lsof    6565 taha    0u   CHR  136,2      0t0     5 /dev/pts/2
lsof    6565 taha    1w  FIFO    0,8      0t0 49839 pipe
lsof    6565 taha    2u   CHR  136,2      0t0     5 /dev/pts/2

На запись stdout и на чтение stdin
echo 'lsof -a -p $$ -d 0-2' | bash
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
bash    28701 root    0r  FIFO    0,8      0t0 276507 pipe
bash    28701 root    1u   CHR  136,1      0t0      4 /dev/pts/1
bash    28701 root    2u   CHR  136,1      0t0      4 /dev/pts/1

Ваууу!))

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

cat <( ls )
#или так
cat <( cat <( cat <( ls | sort | uniq ) ) )

И связываются они естественно через пайп! когда bash видит конструкцию <(… ), он создает канал /dev/fd/число налету Тоесть это одна из возможностей использовать неименованый канал.
Также возможно:
( ls /tmp; ls; )> file

Теперь мы можем раскидать вывод и ошибки по разным процессам)
command > >(stdout_cmd) 2> >(stderr_cmd)

НО и это еще не всё… В bash существует возможность группировать команнды и это достигается с помощью фигурных скобок {… } И из них тожо можно перенаправить вывод.
{ ls /tmp; ls; }> file

Фигурные скобки хороши тем, что в них можно сохранять значения перемнных. А также их можно использовать для открытия дополнительного дескриптора для группы команд.
Пример:

{ lsof -a -p $$ -d 0-5; } 3>/dev/null
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
bash    3298 root    0u   CHR  136,1      0t0    4 /dev/pts/1
bash    3298 root    1u   CHR  136,1      0t0    4 /dev/pts/1
bash    3298 root    2u   CHR  136,1      0t0    4 /dev/pts/1
bash    3298 root    3u   CHR    1,3      0t0 5793 /dev/null

Теперь можно, манипулировать потоками на более сложном уровне. Допустим нам нужно сохранить ошибки команды в переменную, и при этом сделать это так, чтоб stdout не пострадал.
{ ERR=$( ls /ggg 2>&1 1>&3 ); } 3>&1


Команда exec
С помощью данной команды можно открывать и закрывать дескрипторы файлов для bash более глобально.

примеры:

exec 3>/dev/null
exec 4</dev/null
exec 6<>/dev/null
exec 7> >( sort | uniq )
exec 3>&- # закрыть 3 дескриптор.. ну и т.д

Итоги...
Всё вышеописнное только малая часть, такой «простой» штуки как перенаправления…
Чтиво
http://www.catonmat.net/blog/bash-one-liners-explained-part-three/
http://wiki.bash-hackers.org/howto/redirection_tutorial
http://www.opennet.ru/

7 комментариев

avatar
Спасибо за отличную статью!
Особенно заинтересовала конструкция >(command).
Например:

cat /etc/ftab /etc/fstab 1> >(sort | sed -r '/^\s*#/d') 2> >(wc -l)

В результате всё выводится правильно, но в момент завершения команда словно «подвисает» в ожидании ввода, но не намертво: после нажатия enter наконец-таки оная завершается. Почему так???
avatar
Ухты… я, если честно, еще никогда это не использовал… только <(… )
И я бы не советовал так писать… Если это действительно нужно, то лучше поискать другое решение или использовать именованые пайпы. Просто sed выплевывает в унаследованный /dev/pts/3 результаты своей работы. И результаты его работы могут перемешаться с выводом последующих команд. У меня эта команда работает при каждом запуске по разному. И пару раз нормально завершилась…

Кстати

{
  {
    cmd1 3>&- |
      cmd2 2>&3 3>&-
  } 2>&1 >&4 4>&- |
    cmd3 3>&- 4>&-

} 3>&2 4>&1

Делает тоже, что и эта команда))
avatar
Забыл написать…

cat /etc/fstab > >( sort | sed -e '/^#/d' 1>/tmp/file )

Будет работать нормально…

А почему не работает в первом случае… будем разбираться)))
avatar
Что является стандартным выводом команды внутри <(command)?
avatar
В смысле >(command) :)
avatar
у меня /dev/pts/2 =))))) а может быть 3 и 4 =))
akkerman, обрати внимание ны вывод
taha@taha-Lenovo:~$ cat /etc/fstab > >( sort | sed -e '/^#/d' ) 2> >( wc -l )
<u><strong>taha@taha-Lenovo:~$ 0</strong></u>
proc            /proc           proc    nodev,noexec,nosuid 0       0
.....
   1

попробуй не нажимать Enter, а сначала ввести любую команду)

вот так будет хорошо работать:
cat /etc/fstab > >( sort | sed -e '/^#/d' ) 2> >( wc -l ) | cat
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.