¿Cómo leer todo el script de shell antes de ejecutarlo?

35

Por lo general, si edita un scrpit, todos los usos en ejecución del script son propensos a errores.

Por lo que yo entiendo, bash (¿otros shells también?) Lee el script de forma incremental, por lo que si modifica el archivo de script externamente, comienza a leer las cosas incorrectas. Hay alguna manera de prevenirlo?

Ejemplo:

sleep 20

echo test

Si ejecuta este script, bash leerá la primera línea (digamos 10 bytes) y se irá a dormir. Cuando se reanuda, puede haber diferentes contenidos en la secuencia de comandos a partir del décimo byte. Puedo estar en el medio de una línea en el nuevo guión. Por lo tanto, el script en ejecución se romperá.

VasyaNovikov
fuente
¿Qué quiere decir con "modificar el script externamente"?
maulinglawns
1
Tal vez hay una manera de envolver todo el contenido en una función o algo así, ¿entonces el shell leerá todo el script primero? Pero, ¿qué pasa con la última línea donde invocas la función, se leerá hasta EOF? ¿Quizás omitir el último \nharía el truco? Tal vez una subshell ()servirá? No tengo mucha experiencia, ¡por favor ayuda!
VasyaNovikov
@maulinglawns si el script tiene contenido como sleep 20 ;\n echo test ;\n sleep 20y empiezo a editarlo, puede comportarse mal. Por ejemplo, bash podría leer los primeros 10 bytes del script, comprender el sleepcomando y dormir. Después de que se reanude, habrá diferentes contenidos en el archivo a partir de 10 bytes.
VasyaNovikov
1
Entonces, ¿lo que estás diciendo es que estás editando un script que se está ejecutando? Primero detenga la secuencia de comandos, realice sus ediciones y luego vuelva a iniciarla.
maulinglawns
@maulinglawns sí, eso es básicamente todo. El problema es que no es conveniente para mí detener los scripts, y es difícil recordar siempre hacerlo. Tal vez hay una manera de obligar a bash a leer todo el guión primero.
VasyaNovikov

Respuestas:

43

Sí, los shells, y bashen particular, tienen cuidado de leer el archivo de una línea a la vez, por lo que funciona igual que cuando lo usa de forma interactiva.

Notará que cuando el archivo no se puede buscar (como una tubería), bashincluso lee un byte a la vez para asegurarse de no leer más allá del \ncarácter. Cuando se puede buscar el archivo, se optimiza leyendo bloques completos a la vez, pero se vuelve a buscar después de \n.

Eso significa que puedes hacer cosas como:

bash << \EOF
read var
var's content
echo "$var"
EOF

O escriba guiones que se actualicen a sí mismos. Lo que no podría hacer si no le brindara esa garantía.

Ahora, es raro que desee hacer cosas así y, como descubrió, esa característica tiende a interferir con más frecuencia de lo que es útil.

Para evitarlo, puede intentar asegurarse de no modificar el archivo en el lugar (por ejemplo, modificar una copia y mover la copia en su lugar (como sed -io perl -piy algunos editores lo hacen, por ejemplo)).

O podrías escribir tu guión como:

{
  sleep 20
  echo test
}; exit

(tenga en cuenta que es importante que exitesté en la misma línea que }; aunque también podría ponerlo dentro de los corchetes justo antes del cierre).

o:

main() {
  sleep 20
  echo test
}
main "$@"; exit

El shell necesitará leer el script hasta exitantes de comenzar a hacer algo. Eso asegura que el shell no volverá a leer el script.

Eso significa que todo el script se almacenará en la memoria.

Eso también puede afectar el análisis del script.

Por ejemplo, en bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Daría salida a ese U + 00E9 codificado en UTF-8. Sin embargo, si lo cambia a:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

El \ue9se expandirá en el juego de caracteres que estaba vigente en el momento en que se analizó el comando, que en este caso es antes de queexport se ejecute el comando.

También tenga en cuenta que si se usa el comando sourceaka ., con algunos shells, tendrá el mismo tipo de problema para los archivos de origen.

Sin bashembargo, ese no es el caso de cuyo sourcecomando lee el archivo completamente antes de interpretarlo. Si escribe bashespecíficamente, podría hacer uso de eso, agregando al comienzo del script:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Sin embargo, no me basaría en eso, ya que podría imaginar que las versiones futuras de bashpodrían cambiar ese comportamiento que actualmente puede verse como una limitación (bash y AT&T ksh son los únicos shells similares a POSIX que se comportan así hasta donde se sabe) y el already_sourcedtruco es un poco frágil, ya que supone que la variable no está en el entorno, sin mencionar que afecta el contenido de la variable BASH_SOURCE)

Stéphane Chazelas
fuente
@VasyaNovikov, parece haber algo mal con SE en este momento (o al menos para mí). Hubo solo un par de respuestas cuando agregué las mías, y su comentario parece haber aparecido ahora a pesar de que dice que fue publicado hace 16 minutos (o tal vez solo estoy perdiendo mis canicas). De todos modos, tenga en cuenta la "salida" adicional que se necesita aquí para evitar problemas cuando aumenta el tamaño del archivo (como se señala en el comentario que he agregado a su respuesta).
Stéphane Chazelas
Stéphane, creo que he encontrado otra solución. Es para usar }; exec true. De esta manera, no hay requisitos para las nuevas líneas al final del archivo, lo que es amigable para algunos editores (como emacs). Todas las pruebas en las que podría pensar funcionan correctamente}; exec true
VasyaNovikov
@VasyaNovikov, no estoy seguro de lo que quieres decir. ¿Cómo es mejor que }; exit? También estás perdiendo el estado de salida.
Stéphane Chazelas
Como se mencionó en una pregunta diferente: es común analizar primero todo el archivo y luego ejecutar la declaración compuesta en caso de que . scriptse use el comando dot ( ).
schily
@schily, sí, menciono eso en esta respuesta como una limitación de AT&T ksh y bash. Otros shells tipo POSIX no tienen esa limitación.
Stéphane Chazelas
12

Simplemente necesita eliminar el archivo (es decir, copiarlo, eliminarlo, cambiar el nombre de la copia al nombre original). De hecho, muchos editores pueden configurarse para hacer esto por usted. Cuando edite un archivo y guarde un búfer modificado, en lugar de sobrescribir el archivo, cambiará el nombre del archivo antiguo, creará uno nuevo y colocará los nuevos contenidos en el nuevo archivo. Por lo tanto, cualquier script en ejecución debe continuar sin problemas.

Al usar un sistema de control de versiones simple como RCS que está disponible para vim y emacs, obtienes la doble ventaja de tener un historial de tus cambios, y el sistema de pago debería eliminar por defecto el archivo actual y recrearlo con los modos correctos. (Tenga cuidado con la vinculación de dichos archivos, por supuesto).

meuh
fuente
"eliminar" en realidad no es parte del proceso. Si desea hacerlo correctamente atómico, cambie el nombre del archivo de destino; si tiene un paso de eliminación, existe el riesgo de que su proceso falle después de la eliminación pero antes del cambio de nombre, sin dejar ningún archivo en su lugar ( o un lector intenta acceder al archivo en esa ventana y no encuentra ni versiones antiguas ni nuevas disponibles).
Charles Duffy
11

La solución más simple:

{
  ... your code ...

  exit
}

De esta manera, bash leerá todo el {}bloque antes de ejecutarlo, y elexit directiva se asegurará de que no se lea nada fuera del bloque de código.

Si no desea "ejecutar" el script, sino más bien "fuente", necesita una solución diferente. Esto debería funcionar entonces:

{
  ... your code ...

  return 2>/dev/null || exit
}

O si desea control directo sobre el código de salida:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voilà! Este script es seguro para editar, obtener y ejecutar. Aún debe asegurarse de no modificarlo en esos milisegundos cuando se está leyendo inicialmente.

VasyaNovikov
fuente
1
Lo que encontré es que no ve EOF y deja de leer el archivo, pero se enreda en su procesamiento de "flujo de búfer" y termina buscando más allá del final del archivo, por lo que se ve bien si el tamaño de el archivo aumenta no mucho, pero se ve mal cuando hace que el archivo sea más del doble de grande que antes. Informaré un error a los mantenedores de bash en breve.
Stéphane Chazelas
1
error reportado ahora , ver también parche .
Stéphane Chazelas
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
terdon
5

Prueba de concepto. Aquí hay un script que se modifica a sí mismo:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

vemos la versión modificada imprimir

Esto se debe a que bash cargas mantiene un identificador de archivo para abrir en el script, por lo que los cambios en el archivo se verán de inmediato.

Si no desea actualizar la copia en memoria, desvincula el archivo original y reemplácelo.

Una forma de hacerlo es usando sed -i.

sed -i '' filename

prueba de concepto

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Si está utilizando un editor para cambiar la secuencia de comandos, habilitar la función "mantener una copia de seguridad" puede ser todo lo que se necesita para que el editor escriba la versión modificada en un nuevo archivo en lugar de sobrescribir la existente.

Jasen
fuente
2
No, bashno abre el archivo con mmap(). Es cuidadoso leer una línea a la vez según sea necesario, como cuando recibe los comandos de un dispositivo terminal cuando es interactivo.
Stéphane Chazelas
2

Envolver su script en un bloque {}es probablemente la mejor opción, pero requiere cambiar sus scripts.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

sería la segunda mejor opción (suponiendo tmpfs ) la desventaja es que rompe $ 0 si sus scripts usan eso.

usar algo así F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashes menos ideal porque tiene que mantener todo el archivo en la memoria y rompe $ 0.

se debe evitar tocar el archivo original para que la última hora de modificación, los bloqueos de lectura y los enlaces duros no se vean alterados. de esa manera puede dejar un editor abierto mientras ejecuta el archivo y rsync no hará innecesariamente la suma de comprobación del archivo para que las copias de seguridad y los enlaces duros funcionen como se esperaba.

reemplazar el archivo en la edición funcionaría, pero es menos robusto porque no es exigible a otros scripts / usuarios / o uno podría olvidarlo. Y nuevamente rompería los enlaces duros.

usuario1133275
fuente
cualquier cosa que haga una copia funcionaría. tac test.sh | tac | bash
Jasen