Pregunta ¿Cómo ejecutar un comando cada vez que un archivo cambia?


Quiero una forma rápida y simple de ejecutar un comando cada vez que un archivo cambia. Quiero algo muy simple, algo que dejaré funcionando en una terminal y lo cerraré cuando termine de trabajar con ese archivo.

Actualmente, estoy usando esto:

while read; do ./myfile.py ; done

Y luego tengo que ir a esa terminal y presionar Entrar, cada vez que guardo ese archivo en mi editor. Lo que quiero es algo como esto:

while sleep_until_file_has_changed myfile.py ; do ./myfile.py ; done

O cualquier otra solución tan fácil como eso.

Por cierto: estoy usando Vim, y sé que puedo agregar un autocomando para ejecutar algo en BufWrite, pero este no es el tipo de solución que quiero ahora.

Actualizar: Quiero algo simple, descartable si es posible. Lo que es más, quiero que algo se ejecute en un terminal porque quiero ver el resultado del programa (quiero ver los mensajes de error).

Sobre las respuestas: ¡Gracias por todas sus respuestas! Todos ellos son muy buenos, y cada uno toma un enfoque muy diferente de los demás. Como necesito aceptar solo uno, estoy aceptando el que realmente he usado (era simple, rápido y fácil de recordar), aunque sé que no es el más elegante.


357


origen


Posible duplicado de sitios cruzados de: stackoverflow.com/questions/2972765/... (Aunque aquí está en el tema =)) - Ciro Santilli 新疆改造中心 六四事件 法轮功
He hecho referencia antes de un duplicado entre sitios y fue denegado: S;) - Francisco Tapia
La solución de Jonathan Hartley se basa en otras soluciones aquí y soluciona los grandes problemas que tienen las respuestas mejor votadas: faltan algunas modificaciones y son ineficientes. Por favor, cambie la respuesta aceptada a la suya, que también se mantiene en github en github.com/tartley/rerun2 (o para alguna otra solución sin esos defectos) - nealmcb


Respuestas:


Simple, usando inotifywait (instale su distribución inotify-tools paquete):

while inotifywait -e close_write myfile.py; do ./myfile.py; done

o

inotifywait -q -m -e close_write myfile.py |
while read -r filename event; do
  ./myfile.py         # or "./$filename"
done

El primer fragmento es más simple, pero tiene un inconveniente significativo: se perderán los cambios realizados mientras inotifywait no se está ejecutando (en particular, mientras myfile Esta corriendo). El segundo fragmento no tiene este defecto. Sin embargo, tenga en cuenta que asume que el nombre del archivo no contiene espacios en blanco. Si eso es un problema, usa el --format opción para cambiar el resultado para que no incluya el nombre del archivo:

inotifywait -q -m -e close_write --format %e myfile.py |
while read events; do
  ./myfile.py
done

De cualquier manera, hay una limitación: si algún programa reemplaza myfile.py con un archivo diferente, en lugar de escribir en el archivo existente myfile, inotifywait morirá. Muchos editores trabajan de esa manera.

Para superar esta limitación, use inotifywait en el directorio:

inotifywait -e close_write,moved_to,create -m . |
while read -r directory events filename; do
  if [ "$filename" = "myfile.py" ]; then
    ./myfile.py
  fi
done

Alternativamente, use otra herramienta que use la misma funcionalidad subyacente, como incron (le permite registrar eventos cuando se modifica un archivo) o fswatch (una herramienta que también funciona en muchas otras variantes de Unix, utilizando el análogo de cada variante de inotify de Linux).


341



He encapsulado todo esto (con bastantes trucos de bash) en un simple de usar sleep_until_modified.sh script, disponible en: bitbucket.org/denilsonsa/small_scripts/src - Denilson Sá Maia
while sleep_until_modified.sh derivation.tex ; do latexmk -pdf derivation.tex ; done es fantastico. Gracias. - Rhys Ulerich
inotifywait -e delete_self Parece que funciona bien para mí. - Kos
Es simple pero tiene dos problemas importantes: se pueden perder eventos (todos los eventos en el ciclo) y la inicialización de inotifywait se realiza cada vez, lo que hace que esta solución sea más lenta para grandes carpetas recursivas. - Wernight
Por alguna razón while inotifywait -e close_write myfile.py; do ./myfile.py; done siempre sale sin ejecutar el comando (bash y zsh). Para que esto funcione, necesitaba agregar || true, p.ej: while inotifywait -e close_write myfile.py || true; do ./myfile.py; done - ideasman42


entr (http://entrproject.org/) proporciona una interfaz más amigable para inotify (y también es compatible con * BSD y Mac OS X).

Hace que sea muy fácil especificar múltiples archivos para mirar (limitado solo por ulimit -n), elimina la molestia de lidiar con archivos que se reemplazan y requiere menos sintaxis bash:

$ find . -name '*.py' | entr ./myfile.py

Lo he estado usando en todo el árbol de fuentes de mi proyecto para ejecutar las pruebas unitarias del código que estoy modificando actualmente, y ya ha sido un gran impulso para mi flujo de trabajo.

Banderas como -c (borre la pantalla entre ejecuciones) y -d (salir cuando se agrega un archivo nuevo a un directorio supervisado) agregue aún más flexibilidad, por ejemplo, puede hacer:

$ while sleep 1 ; do find . -name '*.py' | entr -d ./myfile.py ; done

A principios de 2018 todavía está en desarrollo activo y se puede encontrar en Debian y Ubuntu (apt install entr); la construcción del repositorio del autor no tenía ningún dolor en ningún caso.


120



No maneja los archivos nuevos y sus modificaciones. - Wernight
@Wernight - a partir del 7 de mayo de 2014 entr tiene el nuevo -d bandera; es un poco más largo, pero puedes hacer while sleep 1 ; do find . -name '*.py' | entr -d ./myfile.py ; done para tratar con nuevos archivos. - Paul Fenney
disponible en aur aur.archlinux.org/packages/entr - Victor Häggqvist
mejor que encontré en el OS X seguro. fswatch toma demasiados eventos funky y no quiero pasar el tiempo para descubrir por qué - dtc
Cabe resaltar que entr está disponible en Homebrew, entonces brew install entr funcionará como se esperaba - jmarceli


Escribí un programa de Python para hacer exactamente esto llamado cuando-cambiado.

El uso es simple:

when-changed FILE COMMAND...

O para ver archivos múltiples:

when-changed FILE [FILE ...] -c COMMAND

FILE puede ser un directorio Mire recursivamente con -r. Utilizar %f pasar el nombre de archivo al comando.


100



@ysangkok sí, en la última versión del código :) - joh
Ahora disponible desde "pip install when-changed". Aún funciona bien. Gracias. - A. L. Flanagan
Para borrar la pantalla primero, puede usar when-changed FILE 'clear; COMMAND'. - Dave James Miller
Esta respuesta es mucho mejor porque también puedo hacerlo en Windows. Y este tipo realmente escribió un programa para obtener la respuesta. - Wolfpack'08
¡Buenas noticias para todos! when-changed ahora es multiplataforma! Mira la última Lanzamiento 0.3.0 :) - joh


¿Qué tal este guion? Utiliza el stat comando para obtener el tiempo de acceso de un archivo y ejecuta un comando cada vez que hay un cambio en el tiempo de acceso (siempre que se acceda al archivo).

#!/bin/bash

### Set initial time of file
LTIME=`stat -c %Z /path/to/the/file.txt`

while true    
do
   ATIME=`stat -c %Z /path/to/the/file.txt`

   if [[ "$ATIME" != "$LTIME" ]]
   then    
       echo "RUN COMMAND"
       LTIME=$ATIME
   fi
   sleep 5
done

45



No lo haría stat-ing la hora modificada será una mejor respuesta "cada vez que un archivo cambia"? - Xen2050
¿Ejecutar estadísticas muchas veces por segundo causa muchas lecturas en el disco? ¿O la llamada al sistema fstat automáticamente haría caché de estas respuestas de alguna manera? Intento escribir una especie de 'reloj ronco' para compilar mi código c cada vez que hago cambios - Oskenso Kashi
Esto es bueno si conoce el nombre de archivo que se va a ver por adelantado. Mejor sería pasar el nombre de archivo a la secuencia de comandos. Mejor aún sería si pudieras pasar muchos nombres de archivos (por ejemplo, "mywatch * .py"). Mejor aún sería si pudiera operar recursivamente en archivos en subdirectorios también, lo que algunas de las otras soluciones hacen. - Jonathan Hartley
En caso de que alguien se esté preguntando acerca de las lecturas pesadas, probé esta secuencia de comandos en Ubuntu 17.04 con un sueño de 0.05s y vmstat -d para ver el acceso al disco Parece que Linux hace un trabajo fantástico almacenando en caché este tipo de cosas: D - Oskenso Kashi
Hay un error tipográfico en "COMANDO", estaba tratando de corregir, pero S.O. dice "Editar no debe tener menos de 6 caracteres" - user337085


Solución usando Vim:

:au BufWritePost myfile.py :silent !./myfile.py

Pero no quiero esta solución porque es un poco molesto escribir, es un poco difícil recordar qué escribir exactamente, y es un poco difícil deshacer sus efectos (necesita ejecutar :au! BufWritePost myfile.py) Además, esta solución bloquea a Vim hasta que el comando ha terminado de ejecutarse.

He agregado esta solución aquí solo para completarla, ya que podría ayudar a otras personas.

Para mostrar la salida del programa (e interrumpir por completo el flujo de edición, ya que la salida se escribirá sobre su editor durante unos segundos, hasta que presione Entrar), elimine el :silent mando.


28



Esto puede ser bastante bueno cuando se combina con entr (ver a continuación): simplemente haz que vim toque un archivo ficticio que entr esté viendo, y deja que entr haga el resto en el fondo ... o tmux send-keys si te encuentras en un ambiente así :) - Paul Fenney
¡bonito! puedes hacer una macro para tu .vimrc archivo - ErichBSchulz


Si tienes npm instalado, nodemon es probablemente la forma más fácil de comenzar, especialmente en OS X, que aparentemente no tiene herramientas de inotify. Es compatible con ejecutar un comando cuando una carpeta cambia.


24



Sin embargo, solo mira los archivos .js y .coffee. - zelk
La versión actual parece ser compatible con cualquier comando, por ejemplo: nodemon -x "bundle exec rspec" spec/models/model_spec.rb -w app/models -w spec/models - kek
Desearía tener más información, pero osx tiene un método para rastrear cambios, fsevents - ConstantineK
En OS X también puedes usar Lanzar Daemons con un WatchPaths clave como se muestra en mi enlace. - Adam Johns


Aquí hay un script de shell Bourne shell simple que:

  1. Toma dos argumentos: el archivo a ser monitoreado y un comando (con argumentos, si es necesario)
  2. Copia el archivo que está monitoreando en el directorio / tmp
  3. Comprueba cada dos segundos para ver si el archivo que está monitoreando es más nuevo que la copia
  4. Si es más reciente, sobrescribe la copia con el original más nuevo y ejecuta el comando
  5. Se limpia después de sí mismo cuando presiona Ctr-C

    #!/bin/sh  
    f=$1  
    shift  
    cmd=$*  
    tmpf="`mktemp /tmp/onchange.XXXXX`"  
    cp "$f" "$tmpf"  
    trap "rm $tmpf; exit 1" 2  
    while : ; do  
        if [ "$f" -nt "$tmpf" ]; then  
            cp "$f" "$tmpf"  
            $cmd  
        fi  
        sleep 2  
    done  
    

Esto funciona en FreeBSD. El único problema de portabilidad que puedo pensar es si algún otro Unix no tiene el comando mktemp (1), pero en ese caso puedes codificar el nombre del archivo temporal.


12



El sondeo es la única forma portátil, pero la mayoría de los sistemas tienen un mecanismo de notificación de cambio de archivo (inotify en Linux, kqueue en FreeBSD, ...). Usted tiene un problema de cotización severo cuando lo hace $cmd, pero, afortunadamente, eso es fácilmente reparable: abandona el cmd variable y ejecutar "$@". Su secuencia de comandos no es adecuada para controlar un archivo grande, pero eso podría solucionarse reemplazando cp por touch -r (solo necesita la fecha, no el contenido). Portabilidad, el -nt la prueba requiere bash, ksh o zsh. - Gilles


rerun2 (en github) es un script Bash de 10 líneas de la forma:

#!/usr/bin/env bash

function execute() {
    clear
    echo "$@"
    eval "$@"
}

execute "$@"

inotifywait --quiet --recursive --monitor --event modify --format "%w%f" . \
| while read change; do
    execute "$@"
done

Guarde la versión de github como 'repetir' en su RUTA, e iníciela usando:

rerun COMMAND

Ejecuta COMMAND cada vez que hay un evento de modificación del sistema de archivos dentro de su directorio actual (recursivo).

Cosas que a uno le pueden agradar:

  • Utiliza inotify, por lo que es más receptivo que el sondeo. Es fabuloso para ejecutar pruebas unitarias de un milisegundo, o para renderizar gráficos de puntos gráficos, cada vez que presionas 'guardar'.
  • Debido a que es tan rápido, no tiene que molestarse en decirle que ignore los subdirectorios grandes (como node_modules) solo por razones de rendimiento.
  • Es súper receptivo, ya que solo llama a inotifywait una vez, en el inicio, en lugar de ejecutarlo, y incurrir en el caro costo de establecer relojes, en cada iteración.
  • Son solo 12 líneas de Bash
  • Como es Bash, interpreta los comandos que pasas exactamente como si los hubieras escrito en un indicador de Bash. (Presumiblemente esto es menos genial si usa otro shell).
  • No pierde eventos que suceden mientras COMMAND se está ejecutando, a diferencia de la mayoría de las soluciones de inotify en esta página.
  • En el primer evento, ingresa en un 'período muerto' durante 0,15 segundos, durante el cual se ignoran otros eventos, antes de que COMMAND se ejecute exactamente una vez. Esto es para que la ráfaga de eventos provocada por la danza de creación-escritura-movimiento que hacen Vi o Emacs al guardar un búfer no provoque múltiples ejecuciones laboriosas de un conjunto de pruebas que posiblemente se ejecute lentamente. No se ignorarán los eventos que ocurran durante la ejecución de COMMAND, ya que causarán un segundo período muerto y la ejecución posterior.

Cosas que a uno podría no gustarle:

  • Utiliza inotify, por lo que no funcionará fuera de Linuxland.
  • Debido a que utiliza inotify, no permitirá ver directorios que contengan más archivos que el número máximo de relojes inotify de usuario. Por defecto, esto parece estar configurado en alrededor de 5,000 a 8,000 en diferentes máquinas que uso, pero es fácil de aumentar. Ver https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached
  • No ejecuta comandos que contienen alias Bash. Juraría que esto solía funcionar. En principio, como esto es Bash, no ejecuta COMMAND en una subshell, espero que esto funcione. Me encantaría escuchar Si alguien sabe por qué no lo hace. Muchas de las otras soluciones en esta página tampoco pueden ejecutar dichos comandos.
  • Personalmente, desearía poder presionar una tecla en el terminal en el que se está ejecutando para provocar manualmente una ejecución adicional de COMMAND. ¿Podría agregar esto de alguna manera, simplemente? ¿Un bucle 'while read -n1' en ejecución simultánea que también ejecuta ejecutar?
  • En este momento lo he codificado para borrar el terminal e imprimir el COMANDO ejecutado en cada iteración. A algunas personas les gustaría agregar indicadores de línea de comandos para desactivar cosas como esta, etc. Pero esto aumentaría el tamaño y la complejidad varias veces.

Este es un refinamiento del anwer de @ cychoi.


12



Creo que deberías usar "$@" en lugar de $@, para trabajar adecuadamente con argumentos que contengan espacios. Pero al mismo tiempo que usas eval, lo que obliga al usuario de la repetición a ser más cuidadoso al citar. - Denilson Sá Maia
Gracias Denilson. ¿Podría dar un ejemplo de dónde se necesita hacer citas cuidadosamente? Lo he estado utilizando las últimas 24 horas y no he visto ningún problema con los espacios hasta el momento, ni cuidadosamente citó algo - solo invocado como rerun 'command'. ¿Estás diciendo que si usé "$ @", el usuario podría invocar como rerun command (sin comillas?) Eso no me parece útil: generalmente no quiero que Bash lo haga alguna procesamiento del comando antes de pasarlo a repetir. p.ej. si el comando contiene "echo $ myvar", entonces querré ver los nuevos valores de myvar en cada iteración. - Jonathan Hartley
Algo como rerun foo "Some File" podría romperse Pero ya que estás usando eval, puede ser reescrito como rerun 'foo "Some File". Tenga en cuenta que a veces la expansión de la ruta puede introducir espacios: rerun touch *.foo probablemente se rompa, y usando rerun 'touch *.foo' tiene una semántica ligeramente diferente (la expansión de ruta ocurre una sola vez o varias veces). - Denilson Sá Maia
Gracias por la ayuda. Sí: rerun ls "some file" se rompe debido a los espacios. rerun touch *.foo* funciona bien por lo general, pero falla si los nombres de archivo que coinciden con * .foo contienen espacios. Gracias por ayudarme a ver cómo rerun 'touch *.foo' tiene una semántica diferente, pero sospecho que la versión con comillas simples es la semántica que quiero: quiero que cada iteración de repetición actúe como si volviera a escribir el comando, por lo tanto, querer  *.foo para ser ampliado en cada iteración. Probaré tus sugerencias para examinar sus efectos ... - Jonathan Hartley
Más discusión sobre este PR (github.com/tartley/rerun2/pull/1) y otros. - Jonathan Hartley


Mira esto incron. Es similar a cron, pero usa eventos inotify en lugar de tiempo.


8



Esto se puede hacer funcionar, pero crear una entrada incron es un proceso bastante laborioso en comparación con otras soluciones en esta página. - Jonathan Hartley


Para aquellos que no pueden instalar inotify-tools como yo, esto debería ser útil:

watch -d -t -g ls -lR

Este comando saldrá cuando cambie la salida, ls -lR listará cada archivo y directorio con su tamaño y fechas, por lo que si se cambia un archivo, debe salir del comando, como dice el hombre:

-g, --chgexit
          Exit when the output of command changes.

Sé que esta respuesta puede no ser leída por nadie, pero espero que alguien la use.

Ejemplo de línea de comando:

~ $ cd /tmp
~ $ watch -d -t -g ls -lR && echo "1,2,3"

Abra otra terminal:

~ $ echo "testing" > /tmp/test

Ahora saldrá el primer terminal 1,2,3

Ejemplo de script simple:

#!/bin/bash
DIR_TO_WATCH=${1}
COMMAND=${2}

watch -d -t -g ls -lR ${DIR_TO_WATCH} && ${COMMAND}

8



Buen hack. Probé y parece tener un problema cuando el listado es largo y el archivo cambiado cae fuera de la pantalla. Una pequeña modificación podría ser algo como esto: watch -d -t -g "ls -lR tmp | sha1sum" - Atle
si miras tu solución cada segundo, funciona para siempre y ejecuta MY_COMMAND solo si cambia algún archivo: mira -n1 "mira -d -t -g ls -lR && MY_COMMAND" - mnesarco
Mi versión de reloj (en Linux, watch from procps-ng 3.3.10) acepta segundos flotantes para su intervalo, por lo tanto watch -n0.2 ... sondeará cada quinto de segundo. Bueno para esas pruebas sanas de unidades de un milisegundo. - Jonathan Hartley


Otra solución con NodeJs, fsmonitor :

  1. Instalar 

    sudo npm install -g fsmonitor
    
  2. Desde línea de comando (ejemplo, registros del monitor y "venta al por menor" si cambia un archivo de registro)

    fsmonitor -s -p '+*.log' sh -c "clear; tail -q *.log"
    

7



Nota al margen: el ejemplo podría ser resuelto por tail -F -q *.log, Creo. - Volker Siegel