No código “{exec> / dev / null; } / dev / null ”o que está acontecendo sob o capô?

15

Quando você redireciona uma lista de comandos que contém um redirecionamento de exec, o exec> / dev / null ainda não parece ser aplicado posteriormente, como por exemplo:

{ exec >/dev/null; } >/dev/null; echo "Hi"

"Oi" é impresso.

Fiquei com a impressão de que a {}lista de comandos não é considerada um subshell, a menos que faça parte de um pipeline, portanto exec >/dev/nullainda deve ser aplicada no ambiente de shell atual em minha mente.

Agora, se você mudar para:

{ exec >/dev/null; } 2>/dev/null; echo "Hi"

não há saída conforme o esperado; o descritor de arquivo 1 permanece apontado para / dev / null para futuros comandos também. Isso é mostrado executando novamente:

{ exec >/dev/null; } >/dev/null; echo "Hi"

o que não dará saída.

Tentei fazer um script e segui-lo, mas ainda não tenho certeza do que está acontecendo aqui.

Em cada ponto deste script, o que está acontecendo com o descritor de arquivo STDOUT?

Edição: Adicionando minha saída strace:

read(255, "#!/usr/bin/env bash\n{ exec 1>/de"..., 65) = 65
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
close(10)                               = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
ioctl(1, TCGETS, 0x7ffee027ef90)        = -1 ENOTTY (Inappropriate ioctl for device)
write(1, "hi\n", 3)                     = 3
Joey Pabalinas
fonte
Isso é estranho; Não consigo reproduzir o close(10). Você também pode postar todo o conteúdo do script que executou?
DepressedDaniel
@DepressedDaniel Aqui está o script e strace completa: roteiro strace
Joey Pabalinas
Você tem um desvio ;depois }, que altera o significado de > /dev/nullnão aplicar à lista composta, {}afinal.
DepressedDaniel
@DepressedDaniel Ah, você está completamente correto! Agora a saída é o que eu espero; Obrigado por suas respostas!
Joey Pabalinas

Respostas:

17

Vamos seguir

{ exec >/dev/null; } >/dev/null; echo "Hi"

passo a passo.

  1. Existem dois comandos:

    uma. { exec >/dev/null; } >/dev/null, Seguido por

    b. echo "Hi"

    O shell executa primeiro o comando (a) e depois o comando (b).

  2. A execução do { exec >/dev/null; } >/dev/nullproduto é a seguinte:

    uma. Primeiro, o shell executa o redirecionamento >/dev/null e lembra-se de desfazê-lo quando o comando termina .

    b. Em seguida, o shell é executado { exec >/dev/null; }.

    c. Por fim, o shell alterna a saída padrão de volta para onde estava. (Esse é o mesmo mecanismo que in ls -lR /usr/share/fonts >~/FontList.txt- redirecionamentos são feitos apenas pela duração do comando ao qual eles pertencem.)

  3. Depois que o primeiro comando é concluído, o shell é executado echo "Hi". A saída padrão está onde estava antes do primeiro comando.

AlexP
fonte
Existe alguma razão por que 2a é executado antes de 2b? (da direita para a esquerda)
Joey Pabalinas
5
Os redirecionamentos devem ser executados antes do comando ao qual eles se aplicam, não? Como eles poderiam funcionar de outra maneira?
AlexP
Aha, nunca pensei nisso dessa maneira! Os dois primeiros são ótimas respostas; dando um pouco antes de eu escolher uma, mas agradeço as duas explicações!
Joey Pabalinas
Infelizmente, só posso escolher uma resposta, por isso vou com essa, uma vez que é um pouco menos técnica e, portanto, acho que seria capaz de ajudar até os usuários menos entendidos em tecnologia. No entanto, o @DepressedDaniel teve uma resposta igualmente ótima aqui, que oferece uma explicação mais aprofundada.
Joey Pabalinas
14

Para não usar um subcasca ou subprocesso, quando a saída de uma lista composta {}é canalizada >, o shell salva o descritor STDOUT antes de executar a lista composta e a restaura depois. Portanto, a exec >lista composta não leva seu efeito além do ponto em que o descritor antigo é restabelecido como STDOUT.

Vamos dar uma olhada na parte relevante de strace bash -c '{ exec >/dev/null; } >/dev/null; echo hi' 2>&1 | cat -n:

   132  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   133  fcntl(1, F_GETFD)                       = 0
   134  fcntl(1, F_DUPFD, 10)                   = 10
   135  fcntl(1, F_GETFD)                       = 0
   136  fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
   137  dup2(3, 1)                              = 1
   138  close(3)                                = 0
   139  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   140  fcntl(1, F_GETFD)                       = 0
   141  fcntl(1, F_DUPFD, 10)                   = 11
   142  fcntl(1, F_GETFD)                       = 0
   143  fcntl(11, F_SETFD, FD_CLOEXEC)          = 0
   144  dup2(3, 1)                              = 1
   145  close(3)                                = 0
   146  close(11)                               = 0
   147  dup2(10, 1)                             = 1
   148  fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
   149  close(10)                               = 0

Você pode ver como, na linha 134, o descritor 1( STDOUT) é copiado para outro descritor com índice, pelo menos 10(é o que F_DUPFDfaz; ele retorna o descritor mais baixo disponível, começando no número especificado após duplicar nesse descritor). Veja também como, na linha 137, o resultado de open("/dev/null")(descritor 3) é copiado no descritor 1( STDOUT). Finalmente, on-line 147, o antigo STDOUTsalvo no descritor 10é copiado de volta para o descritor 1( STDOUT). O efeito líquido é isolar a alteração para STDOUTon-line 144(que corresponde ao interior exec >/dev/null).

DepressedDaniel
fonte
Como o FD 1 é substituído pelo FD 3 na linha 137, por que a linha 141 não aponta 10 para / dev / null?
Joey Pabalinas
A @JoeyPabalinas Line 141 está duplicando o FD 1 (ou seja, stdout) para o próximo descritor disponível após 10 , que acaba sendo 11, como você pode ver no valor de retorno dessa chamada de sistema. 10 é apenas codificado no bash, para que o salvamento do descritor do bash não interfira nos descritores de um dígito que você pode manipular no seu script exec.
DepressedDaniel
Então fcntl (1, F_DUPFD, 10) sempre se refere a STDOUT, não importa para onde o FD 1 esteja apontando atualmente?
Joey Pabalinas
@JoeyPabalinas Não sabe ao certo qual é a sua pergunta. FD 1 ESTÁ EM ESTADO . Eles são a mesma coisa.
DepressedDaniel
Adicionado saída de rastreio completo ao meu post original.
Joey Pabalinas
8

A diferença entre { exec >/dev/null; } >/dev/null; echo "Hi"e { exec >/dev/null; }; echo "Hi"é que o redirecionamento duplo faz dup2(10, 1);antes de fechar o fd 10, que é a cópia do original stdout, antes de executar o próximo comando ( echo).

Isso acontece porque o redirecionamento externo está realmente sobrepondo o redirecionamento interno. É por isso que ele copia de volta o stdoutfd original depois de concluído.

Julie Pelletier
fonte
+1 para explicar a diferença de maneira fácil. A resposta de AlexP carece dessa explicação.
Kamil Maciorowski