Bash está recargando (inyectando) actualizaciones automáticamente en un script en ejecución al guardarlo: ¿Por qué? ¿Algún uso práctico?

10

Estaba escribiendo un script bash, y se me ocurrió actualizar el código (guardé el archivo del script en el disco) mientras el script esperaba alguna entrada en un whilebucle. Después de regresar al terminal y continuar con la invocación previa del script, bash dio un error sobre la sintaxis del archivo:

/home/aularon/bin/script: line 58: unexpected EOF while looking for matching `"'
/home/aularon/bin/script: line 67: syntax error: unexpected end of file

Entonces intenté hacer lo siguiente:

Primero: crea un script, self-update.shllamémoslo:

#!/bin/bash
fname=$(mktemp)
cat $0 | sed 's/BEFORE\./AFTER!./' > $fname
cp $fname $0
rm -f $fname
echo 'String: BEFORE.';

Lo que hace el script es leer su código, cambiar la palabra 'ANTES' a 'DESPUÉS', luego reescribirse con el nuevo código.

Ejecutarlo:

chmod +x self-update.sh
./self-update.sh

Tercera maravilla ...

aularon@aularon-laptop:~$ ./self-update.sh 
String: AFTER!.

¡Ahora, no habría adivinado que en la misma invocación saldría DESPUÉS! , en la segunda carrera seguro, pero no en la primera.

Entonces mi pregunta es: ¿es intencional (por diseño)? o es por la forma en que bash ejecuta el script? Línea por línea o comando por comando. ¿Hay algún buen uso de tal comportamiento? ¿Algún ejemplo de ello?


Editar: intenté formatear el archivo para poner todos los comandos en una línea, no funciona ahora:

#!/bin/bash
fname=$(mktemp);cat $0 | sed 's/BEFORE\./AFTER!./' > $fname;cp $fname $0;rm -f $fname;echo 'String: BEFORE.';

Salida:

aularon@aularon-laptop:~$ ./self-update.sh #First invocation
String: BEFORE.
aularon@aularon-laptop:~$ ./self-update.sh #Second invocation
String: AFTER!.

Mientras mueve la echocadena a la siguiente línea, separándola de la cpllamada rewriting ( ):

#!/bin/bash
fname=$(mktemp);cat $0 | sed 's/BEFORE\./AFTER!./' > $fname;cp $fname $0;rm -f $fname;
echo 'String: BEFORE.';

Y ahora funciona de nuevo:

aularon@aularon-laptop:~$ ./self-update.sh 
String: AFTER!.
aularon
fuente
1
Relacionado: stackoverflow.com/questions/4754592/…
Martin von Wittich
1
He revisado la fuente, pero realmente no puedo encontrar ninguna explicación para esto. Sin embargo, estoy bastante seguro de que he visto algunos instaladores autoextraíbles que se implementaron como scripts de shell hace unos años. Estas secuencias de comandos contienen algunas líneas de comandos de shell ejecutables, y luego un gran bloque de datos que es leído por estos comandos. Así que supondré que esto está diseñado de esta manera para que bash no tenga que leer todo el script en la memoria, lo que no funcionaría con enormes scripts autoextraíbles.
Martin von Wittich el
Aquí hay un ejemplo de un script SFX: ftp.games.skynet.be/pub/wolfenstein/…
Martin von Wittich
¡Agradable! Recuerdo haber visto estos scripts de instalación automática, el controlador AMD Catalyst (propietario) todavía se envía de esta manera, se autoextrae y luego se instala. Estos dependen de tal comportamiento de leer los archivos en fragmentos. Gracias por el ejemplo!
aularon

Respuestas:

12

Esto es por diseño. Bash lee guiones en trozos. Entonces leerá una parte del script, ejecutará las líneas que pueda y luego leerá el siguiente fragmento.

Entonces te encuentras con algo como esto:

  • Bash lee los primeros 256 bytes (bytes 0-255) del script.
  • Dentro de esos primeros 256 bytes hay un comando que tarda un tiempo en ejecutarse, y bash inicia ese comando, esperando que salga.
  • Mientras se ejecuta el comando, la secuencia de comandos se actualiza y la porción cambia después de los 256 bytes que ya leyó.
  • Cuando finaliza el comando bash, continúa leyendo el archivo, reanudando desde donde estaba, obteniendo los bytes 256-511.
  • Esa parte del guión cambió, pero bash no lo sabe.

Donde esto se vuelve aún más problemático es que si editas algo antes del byte 256. Digamos que eliminas un par de líneas. Entonces, los datos en el script que estaban en el byte 256, ahora están en otro lugar, digamos en el byte 156 (100 bytes antes). Debido a esto, cuando bash continúe leyendo, obtendrá lo que originalmente era 356.

Esto es solo un ejemplo. Bash no necesariamente lee 256 bytes a la vez. No sé exactamente cuánto lee a la vez, pero no importa, el comportamiento sigue siendo el mismo.

Patricio
fuente
No, se lee por partes, pero se rebobina hasta donde estaba para asegurarse de leer el siguiente comando tal como está después de que el comando anterior haya regresado.
Stéphane Chazelas
@StephaneChazelas ¿De dónde sacas eso? Acabo de hacer una secuencia y ni siquiera statel archivo para ver si ha cambiado. No hay lseekllamadas
Patrick
Ver pastie.org/8662761 para las partes relevantes de la salida de strace. Vea cómo echo foocambió a a echo bardurante el sleep. Se ha comportado así desde las versiones 2, así que no creo que sea un problema de versión.
Stéphane Chazelas
Aparentemente lee el archivo línea por línea, probé con el archivo y eso fue lo que descubrí. Editaré mi pregunta para resaltar ese comportamiento.
aularon
@Patrick si puedes actualizar tu respuesta para reflejar que está leyendo línea por línea, entonces puedo aceptar tu respuesta. (Verifique mi edición de la pregunta al respecto).
aularon