Linux – Estudando e explorando o 2>&1

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.

linuxIO_cleitonbueno.com
Figura01 – File Descriptors Stream IO no Linux

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.

LinuxRedirectIO_cleitonbueno.com
Figura02 – Detalhes sobre 2>&1

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.

linuxIO_proc_fd_cleitonbueno.com
Figura03 – Associando input, output e error ao script

 

 

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!

Share Button

CC BY-NC-SA 4.0 Linux – Estudando e explorando o 2>&1 by Cleiton Bueno is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.