¿Qué sucede si editas un script durante la ejecución?


31

Tengo una pregunta general, que podría ser el resultado de una mala comprensión de cómo se manejan los procesos en Linux.

Para mis propósitos, voy a definir un 'script' como un fragmento de código bash guardado en un archivo de texto con permisos de ejecución habilitados para el usuario actual.

Tengo una serie de guiones que se llaman juntos. Por simplicidad, los llamaré guiones A, B y C. El guión A lleva a cabo una serie de declaraciones y luego hace una pausa, luego ejecuta el guión B, luego hace una pausa, luego ejecuta el guión C. En otras palabras, la serie de pasos es algo como esto:

Ejecute el script A:

  1. Serie de declaraciones
  2. Pausa
  3. Ejecutar script B
  4. Pausa
  5. Ejecutar script C

Sé por experiencia que si ejecuto el script A hasta la primera pausa, luego realizo ediciones en el script B, esas ediciones se reflejan en la ejecución del código cuando dejo que se reanude. Del mismo modo, si realizo modificaciones en el script C mientras el script A todavía está en pausa, luego permito que continúe después de guardar los cambios, esos cambios se reflejan en la ejecución del código.

Aquí está la verdadera pregunta, ¿hay alguna forma de editar el Script A mientras aún se está ejecutando? ¿O es imposible la edición una vez que comienza su ejecución?


2
Creo que depende de la cáscara. aunque declares que estás usando bash. Parece que dependería de la forma en que el shell carga los scripts internamente.
strugee

el comportamiento también puede cambiar si obtiene el archivo en lugar de ejecutarlo.
strugee

1
Creo que bash lee un script completo en la memoria antes de ejecutarlo.
w4etwetewtwet

2
@handuel, no, no lo hace. Al igual que no espera hasta que escriba "salir" en el indicador para comenzar a interpretar los comandos que ingresó.
Stéphane Chazelas

1
@StephaneChazelas Sí, leer desde el terminal no lo hace, sin embargo, eso es diferente a ejecutar un script.
w4etwetewtwet

Respuestas:


21

En Unix, la mayoría de los editores trabajan creando un nuevo archivo temporal que contiene los contenidos editados. Cuando se guarda el archivo editado, el archivo original se elimina y el archivo temporal se renombra al nombre original. (Por supuesto, existen varias salvaguardas para evitar daños). Este es, por ejemplo, el estilo utilizado por sedo perlcuando se invoca con la -ibandera ("en el lugar"), que no está realmente "en el lugar". Debería haberse llamado "lugar nuevo con nombre antiguo".

Esto funciona bien porque Unix asegura (al menos para los sistemas de archivos locales) que un archivo abierto continúa existiendo hasta que se cierra, incluso si se "elimina" y se crea un nuevo archivo con el mismo nombre. (No es casualidad que la llamada al sistema Unix para "eliminar" un archivo se llame realmente "desvincular"). Entonces, en términos generales, si un intérprete de shell tiene algún archivo fuente abierto y usted "edita" el archivo de la manera descrita anteriormente , el shell ni siquiera verá los cambios ya que todavía tiene abierto el archivo original.

[Nota: como con todos los comentarios basados ​​en estándares, lo anterior está sujeto a múltiples interpretaciones y hay varios casos de esquina, como NFS. Los pendientes pueden llenar los comentarios con excepciones.]

Por supuesto, es posible modificar archivos directamente; simplemente no es muy conveniente para fines de edición, porque si bien puede sobrescribir datos en un archivo, no puede eliminarlos ni insertarlos sin cambiar todos los datos siguientes, lo que implicaría una gran cantidad de reescritura. Además, mientras realizaba ese cambio, el contenido del archivo sería impredecible y los procesos que tenían el archivo abierto sufrirían. Para salirse con la suya (como con los sistemas de bases de datos, por ejemplo), necesita un conjunto sofisticado de protocolos de modificación y bloqueos distribuidos; cosas que están más allá del alcance de una utilidad de edición de archivos típica

Entonces, si desea editar un archivo mientras lo procesa un shell, tiene dos opciones:

  1. Puedes adjuntarlo al archivo. Esto siempre debería funcionar.

  2. Puede sobrescribir el archivo con nuevos contenidos de exactamente la misma longitud . Esto puede o no funcionar, dependiendo de si el shell ya ha leído esa parte del archivo o no. Dado que la mayoría de las E / S de archivos implican memorias intermedias de lectura, y dado que todos los shells que conozco leen un comando compuesto completo antes de ejecutarlo, es bastante poco probable que pueda salirse con la suya. Ciertamente no sería confiable.

No conozco ninguna redacción en el estándar Posix que realmente requiera la posibilidad de agregar un archivo de script mientras se ejecuta el archivo, por lo que podría no funcionar con cada shell compatible con Posix, mucho menos con la oferta actual de casi- y, a veces, conchas compatibles con posix. Entonces YMMV. Pero hasta donde yo sé, funciona de manera confiable con bash.

Como evidencia, aquí hay una implementación "sin bucles" del infame programa de 99 botellas de cerveza en bash, que se utiliza ddpara sobrescribir y agregar (la sobrescritura es presumiblemente segura porque sustituye a la línea que se está ejecutando actualmente, que siempre es la última línea del archivo, con un comentario de exactamente la misma longitud; lo hice para que el resultado final pueda ejecutarse sin el comportamiento de modificación automática).

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #

Cuando ejecuto esto, comienza con "No más", luego continúa a -1 y en los números negativos indefinidamente.
Daniel Hershcovich

Si lo hago export beer=100antes de ejecutar el script, funciona como se esperaba.
Daniel Hershcovich

@DanielHershcovich: bastante cierto; pruebas descuidadas de mi parte. Creo que lo arreglé; ahora toma un parámetro de conteo opcional. Una solución mejor y más interesante sería restablecer automáticamente si el parámetro no se corresponde con la copia en caché.
rici

18

bash hace un largo camino para asegurarse de que lee los comandos justo antes de ejecutarlos.

Por ejemplo en:

cmd1
cmd2

El shell leerá el script por bloques, por lo que es probable que lea ambos comandos, interprete el primero y luego busque el final del cmd1script y lea el script nuevamente para leerlo cmd2y ejecutarlo.

Puedes verificarlo fácilmente:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(aunque mirando el straceresultado en eso, parece que hace algunas cosas más elegantes (como leer los datos varias veces, buscar de nuevo ...) que cuando intenté lo mismo hace unos años, por lo que mi declaración anterior sobre buscar de nuevo puede ya no se aplica en versiones más recientes).

Sin embargo, si escribe su script como:

{
  cmd1
  cmd2
  exit
}

El shell tendrá que leer hasta el cierre }, almacenarlo en la memoria y ejecutarlo. Debido a esto exit, el shell no volverá a leer el script para que pueda editarlo de manera segura mientras el shell lo está interpretando.

Alternativamente, cuando edite el guión, asegúrese de escribir una nueva copia del guión. El shell seguirá leyendo el original (incluso si se elimina o cambia de nombre).

Para hacer eso, cambiar el nombre the-scriptde the-script.oldy copiar the-script.olda the-scripty editarlo.


4

Realmente no hay una forma segura de modificar el script mientras se está ejecutando porque el shell puede usar el almacenamiento en búfer para leer el archivo. Además, si el script se modifica al reemplazarlo con un nuevo archivo, los shells normalmente solo leerán el nuevo archivo después de realizar ciertas operaciones.

A menudo, cuando se cambia un script mientras se ejecuta, el shell termina informando errores de sintaxis. Esto se debe al hecho de que, cuando el shell cierra y vuelve a abrir el archivo de secuencia de comandos, utiliza el desplazamiento de bytes en el archivo para reposicionarse a su regreso.


4

Puede evitar esto estableciendo una trampa en su secuencia de comandos y luego utilizando execpara recoger el nuevo contenido de la secuencia de comandos. Sin embargo, exectenga en cuenta que la llamada inicia el script desde cero y no desde donde llegó en el proceso de ejecución, por lo que se llamará al script B (en adelante).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Esto continuará mostrando la fecha en la pantalla. Luego podría editar mi script y cambiar datea echo "Date: $(date)". Al escribir eso, el script en ejecución solo muestra la fecha. Sin embargo, si envío la señal que configuré trappara capturar, el script exec(reemplaza el proceso actual en ejecución con el comando especificado), que es el comando $CMDy los argumentos $@. Puede hacerlo emitiendo kill -1 PID, donde PID es el PID del script en ejecución, y la salida cambia para mostrarse Date:antes de la datesalida del comando.

Puede almacenar el "estado" de su secuencia de comandos en un archivo externo (en say / tmp) y leer el contenido para saber dónde "reanudar" cuando el programa se vuelve a ejecutar. A continuación, puede agregar una terminación de trampas adicional (SIGINT / SIGQUIT / SIGKILL / SIGTERM) para borrar ese archivo tmp para que cuando reinicie después de interrumpir el "Script A", comience desde el principio. Una versión con estado sería algo como:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup

He solucionado ese problema capturando $0y $@al comienzo del script y usando esas variables en su execlugar.
Drav Sloan
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.