Como o redirecionamento de arquivo bash para o padrão difere do shell (`sh`) no Linux?

Como o redirecionamento de arquivo bash para o padrão difere do shell (`sh`) no Linux?

Eu escrevi um script que alterna usuários durante a execução e o executei usando o redirecionamento de arquivo para o padrão user-switch.sh.

#!/bin/bash

whoami
sudo su -l root
whoami

E executá-lo bashme dá o comportamento que espero

$ bash < user-switch.sh
vagrant
root

No entanto, se eu executar o script com sh, obterei uma saída diferente

$ sh < user-switch.sh 
vagrant
vagrant

Por que está bash < user-switch.shfornecendo resultados diferentes de sh < user-switch.sh?

Notas:

  • acontece em duas caixas diferentes executando o Debian Jessie

Responder1

Um script semelhante, sem sudo, mas com resultados semelhantes:

$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

Com bash, o resto do script vai como entrada para sed, com dash, o shell o interpreta.

Executando straceneles: dashlê um bloco do script (oito kB aqui, mais que o suficiente para conter o script inteiro) e então gera sed:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 8192) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

O que significa que o filehandle está no final do arquivo e sednão verá nenhuma entrada. A parte restante sendo armazenada em buffer dentro do arquivo dash. (Se o script fosse maior que o tamanho do bloco de 8 kB, a parte restante seria lida por sed.)

O Bash, por outro lado, volta ao final do último comando:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 36) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
...
lseek(0, -7, SEEK_CUR)                  = 29
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Se a entrada vier de um tubo, como aqui:

$ cat script.sh | bash

o rebobinamento não pode ser feito, pois os tubos e tomadas não são pesquisáveis. Neste caso, Bash volta a ler a entradaum personagem por vezpara evitar leituras exageradas. (fd_to_buffered_stream()eminput.c) Fazer uma chamada completa do sistema para cada byte não é muito eficaz em princípio. Na prática, não acho que as leituras sejam uma grande sobrecarga em comparação, por exemplo, com o fato de que a maioria das coisas que o shell faz envolve a geração de processos totalmente novos.

Uma situação semelhante é esta:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'

O subshell deve garantir que readapenas leia a primeira nova linha, para que headveja a próxima linha. (Isso também funciona dash.)


Em outras palavras, o Bash se esforça ainda mais para oferecer suporte à leitura da mesma fonte para o script em si e para comandos executados a partir dele. dashnão. O zsh, e ksh93empacotado no Debian vai com o Bash nisso.

Responder2

O shell está lendo o script da entrada padrão. Dentro do script, você executa um comando que também deseja ler a entrada padrão. Qual entrada irá para onde?Você não pode dizer com segurança.

A maneira como os shells funcionam é que eles leem um pedaço do código-fonte, analisam-no e, se encontrarem um comando completo, executam o comando e prosseguem com o restante do pedaço e o restante do arquivo. Se o pedaço não contiver um comando completo (com um caractere de terminação no final - acho que todos os shells são lidos até o final de uma linha), o shell lê outro pedaço e assim por diante.

Se um comando no script tentar ler o mesmo descritor de arquivo do qual o shell está lendo o script, o comando encontrará o que vier depois do último pedaço lido. Esta localização é imprevisível: depende do tamanho do pedaço que o shell escolheu, e isso pode depender não apenas do shell e de sua versão, mas da configuração da máquina, da memória disponível, etc.

Bash procura o final do código-fonte de um comando no script antes de executar o comando. Isso não é algo com que você possa contar, não apenas porque outros shells não fazem isso, mas também porque isso só funciona se o shell estiver lendo um arquivo normal. Se o shell estiver lendo de um canal (por exemplo, ssh remote-host.example.com <local-script-file.sh), os dados lidos serão lidos e não poderão ser deslidos.

Se quiser passar a entrada para um comando no script, você precisará fazê-lo explicitamente, normalmente com umaqui documento. (Um documento aqui geralmente é o mais conveniente para entrada multilinha, mas qualquer método serve.) O código que você escreveu funciona apenas em alguns shells, somente se o script for passado como entrada para o shell a partir de um arquivo normal; se você esperava que o segundo whoamifosse passado como entrada para sudo …, pense novamente, tendo em mente que na maioria das vezes o script não é passado para a entrada padrão do shell.

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF

Observe que nesta década, você pode usar sudo -i root. Correr sudo sué um truque do passado.

informação relacionada