#! / bin / sh vs #! / bin / bash para máxima portabilidad

36

Por lo general trabajo con los servidores de Ubuntu LTS, que por lo que entiendo enlace simbólico /bin/sha /bin/dash. Muchas otras distribuciones aunque enlace simbólico /bin/sha /bin/bash.

¿De eso entiendo que si un script usa #!/bin/shen la parte superior, puede no ejecutarse de la misma manera en todos los servidores?

¿Existe una práctica sugerida sobre qué shell usar para los scripts cuando se desea la máxima portabilidad de esos scripts entre servidores?

cherouvim
fuente
Hay ligeras diferencias entre los diversos depósitos. Si la portabilidad es lo más importante para usted, use #!/bin/shy no use nada más que lo que proporcionó el shell original.
Thorbjørn Ravn Andersen

Respuestas:

65

Hay aproximadamente cuatro niveles de portabilidad para los scripts de shell (en lo que respecta a la línea shebang):

  1. Más portátil: use un #!/bin/shshebang y use solo la sintaxis de shell básica especificada en el estándar POSIX . Esto debería funcionar en casi cualquier sistema POSIX / unix / linux. (Bueno, excepto Solaris 10 y versiones anteriores que tenían el shell Bourne heredado real, anterior a POSIX, por lo que no es compatible /bin/sh).

  2. La segunda más portátil: use una #!/bin/bash(o #!/usr/bin/env bash) línea shebang y manténgase en las funciones de bash v3. Esto funcionará en cualquier sistema que tenga bash (en la ubicación esperada).

  3. Tercero más portátil: use una #!/bin/bash(o #!/usr/bin/env bash) línea shebang y use las funciones de bash v4. Esto fallará en cualquier sistema que tenga bash v3 (por ejemplo, macOS, que tiene que usarlo por razones de licencia).

  4. Menos portátil: use un #!/bin/shshebang y use extensiones bash para la sintaxis de shell POSIX. Esto fallará en cualquier sistema que tenga algo más que bash para / bin / sh (como las versiones recientes de Ubuntu). Nunca hagas esto; No es solo un problema de compatibilidad, es simplemente incorrecto. Desafortunadamente, es un error que mucha gente comete.

Mi recomendación: use el más conservador de los primeros tres que proporciona todas las funciones de shell que necesita para el script. Para una portabilidad máxima, use la opción n. ° 1, pero según mi experiencia, algunas funciones bash (como las matrices) son lo suficientemente útiles como para ir con la n. ° 2.

Lo peor que puedes hacer es # 4, usar el shebang incorrecto. Si no está seguro de qué características son POSIX básicas y cuáles son las extensiones bash, quédese con un bash shebang (es decir, opción # 2), o pruebe el script a fondo con un shell muy básico (como el tablero en sus servidores Ubuntu LTS). El wiki de Ubuntu tiene una buena lista de bashisms a tener en cuenta .

Hay una muy buena información sobre el historial y las diferencias entre los shells en la pregunta de Unix y Linux "¿Qué significa ser compatible con sh?" y la pregunta de Stackoverflow "Diferencia entre sh y bash" .

Además, tenga en cuenta que el shell no es lo único que difiere entre los diferentes sistemas; si está acostumbrado a Linux, está acostumbrado a los comandos GNU, que tienen muchas extensiones no estándar que puede no encontrar en otros sistemas Unix (por ejemplo, bsd, macOS). Desafortunadamente, no hay una regla simple aquí, solo tiene que conocer el rango de variación para los comandos que está utilizando.

Uno de los comandos más desagradables en términos de portabilidad es uno de los más básicos: echo. Cada vez que lo use con alguna opción (por ejemplo, echo -no echo -e), o con cualquier escape (barra invertida) en la cadena para imprimir, las diferentes versiones harán diferentes cosas. Cada vez que desee imprimir una cadena sin un salto de línea después de ella, o con escapes en la cadena, use printfen su lugar (y aprenda cómo funciona, es más complicado de lo que echoes). El pscomando también es un desastre .

Otra cosa que hay que observar en general son las extensiones recientes / GNUish para la sintaxis de la opción de comando: el formato de comando antiguo (estándar) es que el comando va seguido de opciones (con un solo guión, y cada opción es una sola letra), seguido de argumentos de comando. Las variantes recientes (y a menudo no portátiles) incluyen opciones largas (generalmente introducidas con --), que permiten que las opciones aparezcan después de los argumentos y que se usan --para separar las opciones de los argumentos.

Gordon Davisson
fuente
12
La cuarta opción es simplemente una idea equivocada. Por favor no lo use.
pabouk
3
@pabouk Estoy completamente de acuerdo, así que edité mi respuesta para aclarar esto.
Gordon Davisson
1
Su primera declaración es un poco engañosa. El estándar POSIX no especifica nada sobre el shebang fuera de decir que usarlo conduce a un comportamiento no especificado. Además, POSIX no especifica dónde debe ubicarse el shell posix, solo su nombre ( sh), por /bin/shlo que no se garantiza que sea la ruta correcta. Lo más portátil es no especificar ningún shebang o adaptar el shebang al sistema operativo utilizado.
jlliagre
2
Caí en el # 4 con un guión mío muy recientemente, y simplemente no podía entender por qué no estaba funcionando; después de todo, los mismos comandos funcionaron sólidamente e hicieron exactamente lo que quería que hicieran, cuando los probé directamente en el shell. Tan pronto como he cambiado #!/bin/sha #!/bin/bashpesar de que, el guión funcionó a la perfección. (Para mi defensa, que el guión había evolucionado con el tiempo a partir de uno que realmente sólo necesita sh-ismos, a uno que se basó en el comportamiento fiesta similar.)
un CVn
2
@ MichaelKjörling El (verdadero) shell Bourne casi nunca está incluido en las distribuciones de Linux y, de todos modos, no es compatible con POSIX. El shell estándar POSIX se creó a partir del kshcomportamiento, no de Bourne. Lo que hacen la mayoría de las distribuciones de Linux después del FHS es tener /bin/shun enlace simbólico con el shell que seleccionan para proporcionar compatibilidad POSIX, generalmente basho dash.
jlliagre
5

En el ./configurescript que prepara el lenguaje TXR para construir, escribí el siguiente prólogo para una mejor portabilidad. El script se iniciará incluso si se #!/bin/shtrata de un Bourne Shell antiguo no compatible con POSIX. (Construyo cada versión en una VM Solaris 10).

#!/bin/sh

# use your own variable name instead of txr_shell;
# adjust to taste: search for your favorite shells

if test x$txr_shell = x ; then
  for shell in /bin/bash /usr/bin/bash /usr/xpg4/bin/sh ; do
    if test -x $shell ; then
       txr_shell=$shell
       break
    fi
  done
  if test x$txr_shell = x ; then
    echo "No known POSIX shell found: falling back on /bin/sh, which may not work"
    txr_shell=/bin/sh
  fi
  export txr_shell
  exec $txr_shell $0 ${@+"$@"}
fi

# rest of the script here, executing in upgraded shell

La idea aquí es que encontremos un shell mejor que el que estamos ejecutando, y volver a ejecutar el script usando ese shell. Se establece txr_shellla variable de entorno, de modo que el script ejecutado nuevamente sepa que es la instancia recursiva ejecutada nuevamente.

(En mi secuencia de comandos, la txr_shellvariable también se usa posteriormente, para exactamente dos propósitos: en primer lugar, se imprime como parte de un mensaje informativo en la salida de la secuencia de comandos. En segundo lugar, se instala como la SHELLvariable en el Makefile, por lo que makeutilizará esto shell también para ejecutar recetas).

En un sistema donde /bin/shestá el guión, puede ver que la lógica anterior encontrará /bin/bashy volverá a ejecutar el script con eso.

En un cuadro de Solaris 10, se /usr/xpg4/bin/shactivará si no se encuentra Bash.

El prólogo está escrito en un dialecto de shell conservador, utilizando testpara pruebas de existencia de archivos, y el ${@+"$@"}truco para expandir argumentos que atienden a algunos shells viejos rotos (que simplemente sería "$@"si estuviéramos en un shell conforme POSIX).

Kaz
fuente
Uno no necesitaría la xpiratería si se usaran las citas adecuadas, ya que las situaciones que exigían ese modismo rodean las invocaciones de pruebas ahora obsoletas -ao la -ocombinación de múltiples pruebas.
Charles Duffy
@CharlesDuffy Indeed; el test x$whateverque estoy perpetrando allí parece una cebolla en el barniz. Si no podemos confiar en el viejo shell roto para hacer citas, entonces el ${@+"$@"}intento final no tiene sentido.
Kaz
2

Todas las variaciones del lenguaje shell Bourne son objetivamente terribles en comparación con los lenguajes de script modernos como Perl, Python, Ruby, node.js e incluso (posiblemente) Tcl. Si tiene que hacer algo incluso un poco complicado, será más feliz a largo plazo si usa uno de los anteriores en lugar de un script de shell.

La única ventaja que aún tiene el lenguaje shell sobre esos idiomas más nuevos es que se garantiza que algo que se llama a sí mismo /bin/shexiste en todo lo que pretende ser Unix. Sin embargo, ese algo puede no ser compatible con POSIX; muchos de los Unixes propietarios heredados congelaron el lenguaje implementado /bin/shy las utilidades en la RUTA predeterminada antes de los cambios exigidos por Unix95 (sí, Unix95, hace veinte años y contando). Puede haber un conjunto de Unix95, o incluso POSIX.1-2001 si tiene suerte, herramientas en un directorio que no está en la RUTA predeterminada (por ejemplo /usr/xpg4/bin), pero no se garantiza que existan.

Sin embargo, los conceptos básicos de Perl tienen más probabilidades de estar presentes en una instalación de Unix seleccionada arbitrariamente que Bash. (Por "lo básico de Perl" quiero decir que /usr/bin/perlexiste y es una versión, posiblemente bastante antigua, de Perl 5, y si tiene suerte, el conjunto de módulos que se incluyen con esa versión del intérprete también están disponibles).

Por lo tanto:

Si está escribiendo algo que tiene que funcionar en todas partes que pretende ser Unix (como un script de "configuración"), debe usarlo #! /bin/shy no debe usar ninguna extensión. Hoy en día escribiría shell compatible con POSIX.1-2001 en esta circunstancia, pero estaría preparado para parchear POSIXisms si alguien solicitara soporte para hierro oxidado.

Pero si usted está no escribiendo algo que tiene que funcionar en todas partes, entonces el momento en que se ven tentados a utilizar cualquiera del bash en absoluto, usted debe parar y volver a escribir toda la cosa en un mejor lenguaje de script en su lugar. Tu futuro yo te lo agradecerá.

(Entonces, ¿cuándo es apropiado usar extensiones de Bash? Para primer orden: nunca. Para segundo orden: solo para extender el entorno interactivo de Bash, por ejemplo, para proporcionar complementos de pestañas inteligentes y avisos sofisticados).

zwol
fuente
El otro día tuve que escribir un guión que hiciera algo así find ... -print0 |while IFS="" read -rd "" file; do ...; done |tar c --null -T -. No sería posible hacerlo funcionar correctamente sin bashisms ( read -d "") y extensiones GNU ( find -print0, tar --null). Reimplementar esa línea en Perl sería mucho más largo y torpe. Este es el tipo de situación en la que lo correcto es usar bashismos y otras extensiones que no sean POSIX.
michau
@michau Puede que ya no califique como un trazo, dependiendo de lo que ocurra en esas elipses, pero creo que no estás tomando el "reescribir todo en un mejor lenguaje de secuencias de comandos" tan literalmente como lo quise decir. A mi manera se ve algo así. perl -MFile::Find -e 'our @tarcmd = ("tar", "c"); find(sub { return unless ...; ...; push @tarcmd, $File::Find::name }, "."); exec @tarcmdTenga en cuenta que no solo no necesita ningún bashism, no necesita ninguna extensión tar de GNU. (Si la longitud de la línea de comando es preocupante o si desea que el escaneo y el alquitrán se ejecuten en paralelo, entonces lo necesita tar --null -T).
zwol
La cuestión es que finddevolví más de 50,000 nombres de archivo en mi escenario, por lo que es probable que su script alcance el límite de longitud del argumento. Una implementación correcta que no use extensiones que no sean POSIX tendría que construir la salida tar de forma incremental, o tal vez usar Archive :: Tar :: Stream en el código Perl. Por supuesto, se puede hacer, pero en este caso el script Bash es mucho más rápido de escribir y tiene menos repetitivo, y por lo tanto, menos espacio para errores.
michau
Por cierto, que podría utilizar Perl en lugar de Golpe de una manera rápida y concisa: find ... -print0 |perl -0ne '... print ...' |tar c --null -T -. Pero las preguntas son: 1) Estoy usando find -print0y de tar --nulltodos modos, entonces, ¿cuál es el punto de evitar el bashism read -d "", que es exactamente el mismo tipo de cosa (una extensión GNU para manejar el separador nulo que, a diferencia de Perl, algún día puede convertirse en algo POSIX) )? 2) ¿Perl está realmente más extendido que Bash? He estado usando imágenes de Docker recientemente, y muchas de ellas tienen Bash pero no Perl. Es más probable que se ejecuten nuevos scripts allí que en los antiguos Unices.
michau