Uma recurso muito utilizado em scripts Linux para Desktop, Servidores e até em Sistemas Embarcados com Linux, porém muitos não sabem o significado ou o correto uso destas opções, e irei abordar em detalhes neste artigo.
É muito comum em ambiente Linux encontrarmos o uso das seguintes expressões:
2>&1 2> 1> &> 2>&1 >/dev/null >/dev/null 2>&1
Quer ver um exemplo execute o comando abaixo:
bueno@vm1 ~ $ sudo grep -rin "2>&1" /etc/init.d/*
E você verá quantos scripts em seu /etc/init.d possuem o uso do 2>&1.
Vamos primeiramente entender o que são os números da expressão 2>&1.
File Descriptor | Nome | Ação |
0 | stdin | Entrada padrão do sistema |
1 | stdout | Saída padrão do sistema |
2 | stderr | Saída de erro padrão do sistema |
Então acabamos de descobrir que os números 0, 1 e 2 são file descriptors(fd) que o Linux utilizada para stream dos scripts e aplicações, comportando-se normalmente como a Figura01.
Agora os operadores > e &:
Operador | Ação |
> | Redireciona |
& | Indica que o numero é um fd |
Então o ‘>‘ redireciona e o ‘&‘? Esse a função dele aqui é bem especial, neste caso ele que diz a aplicação que 1, ou o numero depois dele, é um file descriptor e não um arquivo, caso você omita o operador ‘&‘ ele irá criar um arquivo com o nome 1.
Então podemos chegar a conclusão sobre o 2>&1 como a Figura02.
Vamos a um exemplo pratico e simples vendo o uso dos 3 files descriptors(input, output e err), exemploIO_1.sh:
#! /bin/bash # Output echo "Iniciando..." # Output echo -e "Digite seu nome:" # Input read NOME # Output, mostrando o nome na tela echo "Voce digitou o nome: ${NOME}" # Err, causando um erro cd 89 exit 0
Execute a aplicação mas não digite o nome ainda.
bueno@vm1 ~ $ /tmp/exemploIO_1.sh Iniciando... Digite seu nome:
Neste momento vamos pegar o PID(Process Identification) do nosso script e verificar os file descriptors dele, vamos usar o comando pgrep ou caso prefira pode usar o ps.
bueno@vm1 ~ $ pgrep exemploIO_1.sh 15655 bueno@vm1 ~ $ bueno@vm1 ~ $ ls -li /proc/15655/fd/ 79091 lrwx------ 1 bueno bueno 64 Out 31 18:02 0 -> /dev/pts/3 79092 lrwx------ 1 bueno bueno 64 Out 31 18:02 1 -> /dev/pts/3 79093 lrwx------ 1 bueno bueno 64 Out 31 18:02 2 -> /dev/pts/3 79094 lr-x------ 1 bueno bueno 64 Out 31 18:02 255 -> /tmp/exemploIO_1.sh bueno@vm1 ~ $ bueno@vm1 ~ $ w 18:06:52 up 4:29, 5 users, load average: 0,26, 0,20, 0,14 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT bueno tty8 :0 13:38 4:29m 1:01 0.28s x-session-manager bueno pts/2 :0.0 15:27 2:39m 0.10s 22.23s mate-terminal bueno pts/3 :0.0 15:41 6:52 0.12s 0.00s /bin/bash ./exemploIO_1.sh bueno pts/7 :0.0 15:29 4.00s 0.50s 0.00s w
Acima obtemos varias informações, primeiro com o pgrep e o nome do script pegamos o PID do processo, depois com o ls listamos no /proc/15655/fd/ os files descriptors do processo, onde 0,1 e 2 apontando para /dev/pts/3 e ao digitar o comando w podemos ver que o pts3 é o pseudo-terminal onde estou executando o nosso script exemploIO_1.sh.
Então, a saída padrão, entrada padrão e saída de erro estão todos para o terminal onde esta executando o script, vamos voltar ao nosso script e agora entrar com o nome.
bueno@vm1 ~ $ /tmp/exemploIO_1.sh Iniciando.... Digite seu nome: Cleiton Bueno Voce digitou o nome: Cleiton Bueno /tmp/exemploIO_1.sh: line 17: cd: 89: No such file or directory
Agora adaptando o que aconteceu ao script e associando com a Figura01, temos como resultado a Figura03.
Manipulando o file descriptor
Usando o mesmo script anterior, vamos executar ele mas de uma forma que não queremos ver o erro, então iremos manipular o fd 2(stderr).
Executando o script e novamente não entrando com o nome:
bueno@vm1 ~ $ /tmp/exemploIO_1.sh 2> /dev/null Iniciando.... Digite seu nome:
Vamos ver o que mudou no processo do script agora.
bueno@vm1 ~ $ pgrep exemploIO_1.sh 15942 bueno@vm1 ~ $ ls -li /proc/15942/fd/ total 0 80885 lrwx------ 1 bueno bueno 64 Out 31 18:26 0 -> /dev/pts/3 80886 lrwx------ 1 bueno bueno 64 Out 31 18:26 1 -> /dev/pts/3 80887 l-wx------ 1 bueno bueno 64 Out 31 18:26 2 -> /dev/null 80888 lr-x------ 1 bueno bueno 64 Out 31 18:26 255 -> /tmp/exemploIO_1.sh
Agora o file descriptor (2) stderr mudou e no lugar de apontar para o terminal esta para o /dev/null, então não devemos mais ver o erro na tela, vamos conferir.
bueno@vm1 ~ $ /tmp/exemploIO_1.sh 2> /dev/null Iniciando.... Digite seu nome: Cleiton Bueno Voce digitou o nome: Cleiton Bueno bueno@vm1 ~ $
Então se eu quiser que stderr e stdout não mostrem nada, eu uso o famoso 2>&1 ou &> da seguinte maneira:
bueno@vm1 ~ $ /tmp/exemploIO_1.sh >/dev/null 2>&1 bueno@vm1 ~ $ /tmp/exemploIO_1.sh &> /dev/null
Ambos os modos irei obter o mesmo resultando, stdout e stderr para /dev/null.
bueno@vm1 ~ $ pgrep exemploIO_1.sh 16169 bueno@vm1 ~ $ ls -li /proc/16169/fd/ total 0 84600 lrwx------ 1 bueno bueno 64 Out 31 18:40 0 -> /dev/pts/3 84601 l-wx------ 1 bueno bueno 64 Out 31 18:40 1 -> /dev/null 84602 l-wx------ 1 bueno bueno 64 Out 31 18:40 2 -> /dev/null 84603 lr-x------ 1 bueno bueno 64 Out 31 18:40 255 -> /tmp/exemploIO_1.sh
As vezes podemos ver scripts assim:
bueno@vm1 ~ $ /tmp/exemploIO_1.sh 2>&1 >/dev/null
Que para o processo será:
bueno@vm1 ~ $ pgrep exemploIO_1.sh 16497 bueno@vm1 ~ $ ls -li /proc/16497/fd/ total 0 92253 lrwx------ 1 bueno bueno 64 Out 31 19:06 0 -> /dev/pts/3 92254 l-wx------ 1 bueno bueno 64 Out 31 19:06 1 -> /dev/null 92255 lrwx------ 1 bueno bueno 64 Out 31 19:06 2 -> /dev/pts/3 92256 lr-x------ 1 bueno bueno 64 Out 31 19:06 255 -> /tmp/exemploIO_2.sh
A explicação é que stdout(2) esta redirecionando para o stderr(1) mas este por si esta enviando para /dev/null, o correto é então especificar para onde enviar e em seguida apontar os dois file descriptors.
Do mesmo modo que envio para /dev/null posso enviar para um arquivo, então vamos enviar o stderr para /dev/null e stdout para /tmp/saida.log.
bueno@vm1 ~ $ /tmp/exemploIO_1.sh 2> /dev/null 1> /tmp/saida.log Cleiton Bueno
Analisando o PID do processo:
bueno@vm1 ~ $ pgrep exemploIO_1.sh 16175 bueno@vm1 ~ $ ls -li /proc/16175/fd/ total 0 86557 lrwx------ 1 bueno bueno 64 Out 31 18:43 0 -> /dev/pts/3 86558 l-wx------ 1 bueno bueno 64 Out 31 18:43 1 -> /tmp/saida.log 86559 l-wx------ 1 bueno bueno 64 Out 31 18:43 2 -> /dev/null 86560 lr-x------ 1 bueno bueno 64 Out 31 18:43 255 -> /tmp/exemploIO_1.sh
O incomodo aqui é que todo output da aplicação ficou no /tmp/saida.log:
bueno@vm1 ~ $ cat /tmp/saida.log Iniciando.... Digite seu nome: Voce digitou o nome: Cleiton Bueno
O que podemos fazer neste caso é deixar apenas o stderr do script enviando para /dev/null e dentro do script direcionamos o echo para /tmp/saida.log, segue o exemploIO_2.sh:
#! /bin/bash OUTPUT_LOG="/tmp/saida.log" # Output echo "Iniciando...." 1> ${OUTPUT_LOG} # Output echo -e "Digite seu nome:" # Input read NOME # Output, mostrando o nome da tela echo "Voce digitou o nome: ${NOME}" 1> ${OUTPUT_LOG} # Err, causando um erro cd 89 exit 0
Executando o exemploIO_2.sh:
bueno@vm1 ~ $ /tmp/exemploIO_2.sh 2> /dev/null
Verificando o processo:
bueno@vm1 ~ $ pgrep exemploIO_2.sh 16373 bueno@vm1 ~ $ ls -li /proc/16373/fd/ total 0 88200 lrwx------ 1 bueno bueno 64 Out 31 18:54 0 -> /dev/pts/3 88201 lrwx------ 1 bueno bueno 64 Out 31 18:54 1 -> /dev/pts/3 88202 l-wx------ 1 bueno bueno 64 Out 31 18:54 2 -> /dev/null 88203 lr-x------ 1 bueno bueno 64 Out 31 18:54 255 -> /tmp/exemploIO_2.sh bueno@vm1 ~ $ cat /tmp/saida.log Iniciando...
O processo do script direcionara o stderr para outro local e dentro do script em dois lugares direcionamos o stdout(1) para o arquivo /tmp/saida.log.
Da mesma maneira podemos manipular o file descriptor stdin(0), vamos ao exemploIO_3.sh:
#! /bin/bash # Output echo "Iniciando...." # Output echo -e "Digite seu nome:" # Input read NOME # Output, mostrando o nome da tela echo "Voce digitou o nome: ${NOME}" # Err, causando um erro cd 89 echo "Finalizando em 60s..." sleep 30 exit 0
Deve-se criar o arquivo /tmp/nome.txt e dentro insira qualquer nome, execute o script da seguinte maneira:
bueno@vm1 ~ $ /tmp/exemploIO_3.sh 0< /tmp/nome.txt Iniciando.... Digite seu nome: Voce digitou o nome: Cleiton Bueno /tmp/exemploIO_3.sh: line 17: cd: 89: No such file or directory Finalizando em 60s...
Coloquei um atraso de 30s para dar tempo de ver o processo, então verificando o fd do processo:
bueno@vm1 ~ $ pgrep exemploIO_3.sh 16705 bueno@vm1 ~ $ ls -li /proc/16705/fd/ total 0 90037 lr-x------ 1 bueno bueno 64 Out 31 19:18 0 -> /tmp/nome.txt 90038 lrwx------ 1 bueno bueno 64 Out 31 19:18 1 -> /dev/pts/3 90039 lrwx------ 1 bueno bueno 64 Out 31 19:18 2 -> /dev/pts/3 90040 lr-x------ 1 bueno bueno 64 Out 31 19:18 255 -> /tmp/exemploIO_3.sh bueno@vm1 ~ $
O script irá usar o arquivo passado no 0< como entrada e é o que pode-se ver na saída acima.
Para finalizar um exemplo de como criar um file descriptor dentro da aplicação e utiliza-lo, vamos ao exemploIO_4.sh:
#! /bin/bash # File descriptor 28 -> /tmp/$0.log exec 28> /tmp/$(basename $0).log # Output echo "Iniciando...." echo "[$(date +%s)] Starting...." >&28 # Output echo -e "Digite seu nome:" # Input read NOME # Output, mostrando o nome da tela echo "Voce digitou o nome: ${NOME}" # Err, causando um erro cd 89 echo "[$(date +%s)] Done" >&28 exit 0
Executando e listando o fd do processo:
bueno@vm1 ~ $ ls -l /proc/4696/fd/ total 0 lrwx------ 1 bueno bueno 64 Nov 1 16:56 0 -> /dev/pts/3 lrwx------ 1 bueno bueno 64 Nov 1 16:56 1 -> /dev/pts/3 lrwx------ 1 bueno bueno 64 Nov 1 16:56 2 -> /dev/pts/3 lr-x------ 1 bueno bueno 64 Nov 1 16:56 255 -> /tmp/exemploIO_4.sh l-wx------ 1 bueno bueno 64 Nov 1 16:56 28 -> /tmp/exemploIO_4.sh.log
Criamos um file descriptor (28) para /tmp/exemploIO_4.sh.log que irá registrar quando iniciar nosso script e quando estiver saindo. Do mesmo modo que podemos criar um fd(file descriptor) também podemos fechá-lo.
Utilizando o script anterior fecharemos stderr e criando fd(28) apenas para gravar quando a aplicação começar e em seguida após digitar o nome irá fechar o fd(28), o exemploIO_5.sh ficara como abaixo:
#! /bin/bash # File descriptor 28 -> /tmp/$0.log exec 28> /tmp/$(basename $0).log # Fechando fd(2) stderr exec 2<&- # Output echo "Iniciando...." echo "[$(date +%s)] Starting...." >&28 # Output echo -e "Digite seu nome:" # Input read NOME # Fechando fd 28 exec 28>&- # Output, mostrando o nome da tela echo "Voce digitou o nome: ${NOME}" # Err, causando um erro cd 89 # 30s antes de sair sleep 30 exit 0
Como pode-se ver para fechar um file descriptor colocamos o numero do fd>&-, como foi feito com stderr(2) no inicio do script e com o fd(28) na linha 20, para stdint seria fd<&-.
Executando o script exemploIO_5.sh, mas não digitando o nome:
bueno@vm1 ~ $ /tmp/exemploIO_5.sh Iniciando.... Digite seu nome:
Verificando o processo do exemploIO_5.sh:
bueno@vm1 ~ $ pgrep exemploIO_5.sh 4991 bueno@vm1 ~ $ ls -l /proc/4991/fd/ total 0 lrwx------ 1 bueno bueno 64 Nov 1 17:13 0 -> /dev/pts/3 lrwx------ 1 bueno bueno 64 Nov 1 17:13 1 -> /dev/pts/3 lr-x------ 1 bueno bueno 64 Nov 1 17:13 255 -> /tmp/exemploIO_5.sh l-wx------ 1 bueno bueno 64 Nov 1 17:13 28 -> /tmp/exemploIO_5.sh.log
Podemos notar que o fd(2) do stderr não existe, então não é para aparecer nenhum erro no terminal e o fd(28) para /tmp/exemploIO_5.sh.log esta criado, após digitar o nome ele deverá ser fechado e irá ter armazenado o timestamp da hora que iniciou o script, vamos inserir o nome e em seguida listar o diretório fd do processo novamente.
bueno@vm1 ~ $ /tmp/exemploIO_5.sh Iniciando.... Digite seu nome: http://www.cleitonbueno.com Voce digitou o nome: http://www.cleitonbueno.com
Listando novamente o diretório fd do processo:
bueno@vm1 ~ $ ls -l /proc/4991/fd/ total 0 lrwx------ 1 bueno bueno 64 Nov 1 17:13 0 -> /dev/pts/3 lrwx------ 1 bueno bueno 64 Nov 1 17:13 1 -> /dev/pts/3 lr-x------ 1 bueno bueno 64 Nov 1 17:13 255 -> /tmp/exemploIO_5.sh
Durante os 30s de delay que inseri apos digitar o nome podemos listar o processo e agora o fd(28) não existe mais, vamos ver se o conteúdo foi inserido no /tmp/exemploIO_5.sh.log:
bueno@vm1 ~ $ cat /tmp/exemploIO_5.sh.log [1446405215] Starting....
Então tudo saiu como planejado, registrou o timestamp no fd(28) e como fechamos o stderr(2) o erro do cd não foi reportado.
E assim o uso de file descriptors e a manipulação de stdin, stdout e stderr pode-se estender dentro do script como fora, o fd(28) que criamos pode ser passado ao executar o script como fizemos com 2> e 1>, e tem-se um enorme leque de combinações e opções para manipular estes fd e streams IO no Linux.
Até a próxima!
Linux – Estudando e explorando o 2>&1 by Cleiton Bueno is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.