¿En qué se diferencia la redirección de archivos bash al estándar en shell (`sh`) en Linux?

9

Escribí un script que cambia a los usuarios mientras se ejecuta, y lo ejecuté usando la redirección de archivos al estándar. Así user-switch.shes ...

#!/bin/bash

whoami
sudo su -l root
whoami

Y ejecutarlo bashme da el comportamiento que espero

$ bash < user-switch.sh
vagrant
root

Sin embargo, si ejecuto el script con sh, obtengo un resultado diferente

$ sh < user-switch.sh 
vagrant
vagrant

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

Notas:

  • sucede en dos cajas diferentes que ejecutan Debian Jessie
popedotninja
fuente
1
No es una respuesta, pero con respecto a sudo su: unix.stackexchange.com/questions/218169/…
Kusalananda
1
is / bin / sh = dash en Debian? No puedo reprochar en RHEL donde / bin / sh = bash
Jeff Schaller
1
/bin/shen (mi copia de) Debian es Dash, sí. Ni siquiera sabía qué era eso hasta ahora. Me he quedado con bash hasta ahora.
popedotninja
1
La fiesta de comportamiento no se garantiza que funcione por cualquier semántica documentados bien.
Charles Duffy el
Alguien me señaló hoy que el script que estaba ejecutando violaba la semántica de cómo se debe usar bash. Hubo un par de excelentes respuestas a esta pregunta, pero vale la pena señalar que uno probablemente no debería cambiar a los usuarios a mitad del script. Originalmente intenté user-switch.shen un trabajo de Jenkins, y se ejecutó el script. Ejecutar el mismo script fuera de Jenkins produjo resultados diferentes, y recurrí a la redirección de archivos para obtener el comportamiento que vi en Jenkins.
popedotninja el

Respuestas:

12

Un guión similar, sin sudoresultados, pero similares:

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

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

Con bash, el resto del script va como entrada para sed, con dash, el shell lo interpreta.

Corriendo straceen esos: dashlee un bloque del script (ocho kB aquí, más que suficiente para contener todo el script), y luego genera 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|...

Lo que significa que el controlador de archivo está al final del archivo y sedno verá ninguna entrada. La parte restante está almacenada en el interior dash. (Si el script fuera más largo que el tamaño de bloque de 8 kB, la parte restante sería leída por sed).

Bash, por otro lado, busca el final del ú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|...

Si la entrada proviene de una tubería, como aquí:

$ cat script.sh | bash

no se puede rebobinar, ya que no se pueden buscar tuberías y enchufes. En este caso, Bash recurre a la lectura de la entrada de un carácter a la vez para evitar una sobre lectura. ( fd_to_buffered_stream()ininput.c ) Hacer una llamada de sistema completo para cada byte no es muy efectivo en principio. En la práctica, no creo que las lecturas sean una gran sobrecarga en comparación, por ejemplo, con el hecho de que la mayoría de las cosas que hace el shell implican generar procesos completamente nuevos.

Una situación similar es esta:

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

El subshell debe asegurarse de que readsolo lea la primera línea nueva, para que headvea la siguiente línea. (Esto funciona dashtambién con ).


En otras palabras, Bash hace todo lo posible para admitir la lectura de la misma fuente para el script en sí y para los comandos ejecutados desde él. dashno lo hace El zsh, y ksh93empaquetado en Debian van con Bash en esto.

ilkkachu
fuente
1
Francamente, estoy un poco sorprendido de que funcione.
ilkkachu
¡Gracias por la gran respuesta! Ejecutaré ambos comandos usando stracemás tarde y compararé las salidas. Sin embargo, no había usado ese enfoque.
popedotninja
2
Sí, mi reacción al leer la pregunta era una sorpresa que hace el trabajo con la fiesta - lo tenía archivado como algo que definitivamente no iba a funcionar. Supongo que era sólo la mitad derecha :)
Hobbs
12

El shell está leyendo el script desde la entrada estándar. Dentro del script, ejecuta un comando que también quiere leer la entrada estándar. ¿Qué entrada va a ir a dónde? No se puede decir de manera confiable .

La forma en que funcionan los shells es que leen un fragmento de código fuente, lo analizan y, si encuentran un comando completo, ejecutan el comando y luego continúan con el resto del fragmento y el resto del archivo. Si el fragmento no contiene un comando completo (con un carácter final al final, creo que todos los shells se leen hasta el final de una línea), el shell lee otro fragmento, y así sucesivamente.

Si un comando en el script intenta leer desde el mismo descriptor de archivo del que el shell está leyendo el script, entonces el comando encontrará lo que venga después del último fragmento que leyó. Esta ubicación es impredecible: depende del tamaño de fragmento que seleccionó el shell, y eso puede depender no solo del shell y su versión, sino de la configuración de la máquina, la memoria disponible, etc.

Bash busca el final del código fuente de un comando en el script antes de ejecutar el comando. Esto no es algo con lo que pueda contar, no solo porque otros shells no lo hacen, sino también porque esto solo funciona si el shell está leyendo desde un archivo normal. Si el shell está leyendo desde una tubería (por ejemplo ssh remote-host.example.com <local-script-file.sh), los datos que se han leído se leen y no se pueden leer.

Si desea pasar la entrada a un comando en el script, debe hacerlo explícitamente, generalmente con un documento aquí . (Un documento aquí suele ser el más conveniente para la entrada de varias líneas, pero cualquier método funcionará). El código que escribió solo funciona en unos pocos shells, solo si el script se pasa como entrada al shell desde un archivo normal; si esperaba que el segundo whoamise pasara como entrada sudo …, piense de nuevo, teniendo en cuenta que la mayoría de las veces el script no se pasa a la entrada estándar del shell.

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

Tenga en cuenta que esta década, puede usar sudo -i root. Correr sudo sues un truco del pasado.

Gilles 'SO- deja de ser malvado'
fuente