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

9

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

#!/bin/bash

whoami
sudo su -l root
whoami

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

$ bash < user-switch.sh
vagrant
root

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

$ sh < user-switch.sh 
vagrant
vagrant

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

Notas:

  • acontece em duas caixas diferentes executando o Debian Jessie
popedotninja
fonte
1
Não é uma resposta, mas sobre sudo su: unix.stackexchange.com/questions/218169/…
Kusalananda
1
é / bin / sh = traço no Debian? Eu não posso repro no RHEL onde / bin / sh = festa
Jeff Schaller
1
/bin/shno (minha cópia do) Debian é Dash, sim. Eu nem sabia o que era até agora. Eu fiquei com o bash até agora.
precisa saber é o seguinte
1
Também não é garantido que o comportamento do bash funcione por qualquer semântica documentada.
Charles Duffy
Alguém me indicou hoje que o script que eu estava executando violava a semântica de como o bash se destina a ser usado. Havia algumas ótimas respostas para essa pergunta, mas vale ressaltar que provavelmente não se deve alternar entre usuários no meio do script. Eu tentei originalmente user-switch.shem um trabalho de Jenkins, e o script foi executado. A execução do mesmo script fora do Jenkins produziu resultados diferentes, e recorri ao redirecionamento de arquivos para obter o comportamento que vi no Jenkins.
popedotninja

Respostas:

12

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 restante do script é usado como entrada para sed, com dash, o shell o interpreta.

Executando straceneles: dashlê um bloco do script (oito kB aqui, mais do que suficiente para armazenar todo o script) e depois 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 identificador de arquivo está no final do arquivo e sednão verá nenhuma entrada. A parte restante está sendo armazenada em buffer dash. (Se o script for maior que o tamanho do bloco de 8 kB, a parte restante será lida por sed.)

Bash, por outro lado, procura voltar 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

não é possível rebobinar, pois não é possível procurar tubos e soquetes. Nesse caso, o Bash volta a ler a entrada, um caractere de cada vez, para evitar a substituição. ( fd_to_buffered_stream()ininput.c ) Fazer uma chamada de sistema completa 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, ao fato de que a maioria das coisas que o shell envolve gera processos totalmente novos.

Uma situação semelhante é a seguinte:

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

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


Em outras palavras, o Bash se esforça para dar suporte à leitura da mesma fonte para o próprio script e para comandos executados a partir dele. dashnão. O zshe ksh93empacotado em Debian ir com Bash sobre este assunto.

ilkkachu
fonte
1
Francamente, estou um pouco surpreso que funcione.
precisa saber é o seguinte
Obrigado pela ótima resposta! Vou executar os dois comandos usando stracemais tarde e comparar as saídas. Eu não tinha pensado em usar essa abordagem.
popedotninja
2
Sim, a minha reação ao ler a pergunta era surpresa que ele faz o trabalho com o bash - eu tinha arquivado como algo que definitivamente não iria funcionar. Acho que eu estava apenas a metade direita :)
Hobbs
12

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 vai 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, o analisam 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 contém um comando completo (com um caractere final 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 que ele leu. Esse local é imprevisível: depende do tamanho do bloco escolhido pelo shell, e isso pode depender não apenas do shell e de sua versão, mas também da configuração da máquina, memória disponível etc.

O Bash procura o final do código-fonte de um comando no script antes de executar o comando. Isso não é algo com o qual você pode contar, não apenas porque outros shells não o fazem, mas também porque isso só funciona se o shell estiver lendo um arquivo regular. 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 lidos.

Se você deseja passar a entrada para um comando no script, é necessário fazê-lo explicitamente, geralmente com um documento aqui . (Um documento aqui é geralmente o mais conveniente para a entrada de várias linhas, mas qualquer método serve.) O código que você escreveu funciona apenas em alguns shells, apenas se o script for passado como entrada para o shell a partir de um arquivo regular; 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.

Gilles 'SO- parar de ser mau'
fonte