Como executar um comando sempre que um arquivo é alterado?

408

Eu quero uma maneira rápida e simples de executar um comando sempre que um arquivo for alterado. Eu quero algo muito simples, algo que deixarei em execução em um terminal e fechá-lo sempre que terminar de trabalhar com esse arquivo.

Atualmente, estou usando isso:

while read; do ./myfile.py ; done

E então eu preciso ir para aquele terminal e pressionar Enter , sempre que eu salvar esse arquivo no meu editor. O que eu quero é algo assim:

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

Ou qualquer outra solução tão fácil quanto isso.

BTW: Estou usando o Vim, e sei que posso adicionar um autocommand para executar algo no BufWrite, mas esse não é o tipo de solução que quero agora.

Atualização: quero algo simples, descartável, se possível. Além disso, quero que algo seja executado em um terminal porque quero ver a saída do programa (quero ver mensagens de erro).

Sobre as respostas: Obrigado por todas as suas respostas! Todos eles são muito bons, e cada um tem uma abordagem muito diferente dos outros. Desde que eu preciso aceitar apenas um, estou aceitando o que eu realmente usei (foi simples, rápido e fácil de lembrar), mesmo sabendo que não é o mais elegante.

    
por Denilson Sá Maia 27.08.2010 / 22:02

35 respostas

382

Simples, usando inotifywait (instale o pacote inotify-tools da sua distribuição):

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

ou

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

O primeiro snippet é mais simples, mas tem uma desvantagem significativa: ele perderá as alterações realizadas enquanto inotifywait não estiver em execução (em especial, enquanto myfile estiver em execução). O segundo trecho não tem esse defeito. No entanto, tenha em atenção que pressupõe que o nome do ficheiro não contém espaço em branco. Se isso for um problema, use a opção --format para alterar a saída para não incluir o nome do arquivo:

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

De qualquer forma, há uma limitação: se algum programa substituir myfile.py por um arquivo diferente, em vez de gravar no myfile existente, inotifywait morrerá. Muitos editores trabalham dessa maneira.

Para superar essa limitação, use inotifywait no diretório:

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

Como alternativa, use outra ferramenta que use a mesma funcionalidade subjacente, como incron (permite registrar eventos quando um arquivo é modificado) ou fswatch (uma ferramenta que também funciona em muitas outras variantes do Unix, usando o análogo de cada variante do inotify do Linux).

    
por 27.08.2010 / 22:54
148

entr ( link ) fornece uma interface mais amigável para inotify (e também suporta * BSD e Mac OS X).

Isso torna muito fácil especificar vários arquivos para assistir (limitado apenas por ulimit -n ), elimina o trabalho de lidar com arquivos sendo substituídos e requer menos sintaxe de bash:

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

Eu tenho usado isso em toda a árvore de código-fonte do meu projeto para executar os testes de unidade para o código que estou modificando no momento, e isso já contribuiu muito para o meu fluxo de trabalho.

Sinalizadores como -c (limpar a tela entre execuções) e -d (sair quando um novo arquivo é adicionado a um diretório monitorado) adicionam ainda mais flexibilidade, por exemplo, você pode fazer:

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

No início de 2018 ele ainda está em desenvolvimento ativo e pode ser encontrado no Debian & Ubuntu ( apt install entr ); construção do repositório do autor estava livre de dor em qualquer caso.

    
por 25.10.2013 / 11:41
105

Eu escrevi um programa em Python para fazer exatamente isso chamado quando mudou .

O uso é simples:

when-changed FILE COMMAND...

Ou para assistir a vários arquivos:

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

FILE pode ser um diretório. Assista recursivamente com -r . Use %f para passar o nome do arquivo para o comando.

    
por 30.06.2011 / 15:34
48

Que tal este script? Ele usa o comando stat para obter o tempo de acesso de um arquivo e executa um comando sempre que houver uma alteração no tempo de acesso (sempre que o arquivo for acessado).

#!/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
    
por 20.08.2013 / 19:12
30

Solução usando o Vim:

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

Mas eu não quero essa solução porque é meio irritante digitar, é um pouco difícil lembrar o que digitar, exatamente, e é um pouco difícil desfazer seus efeitos (precisa executar :au! BufWritePost myfile.py ). Além disso, esta solução bloqueia o Vim até que o comando tenha terminado a execução.

Eu adicionei esta solução aqui apenas por completo, pois pode ajudar outras pessoas.

Para exibir a saída do programa (e interromper completamente o fluxo de edição, como a saída irá escrever sobre o editor por alguns segundos, até pressionar Enter), remova o comando :silent .

    
por 27.08.2010 / 22:12
23

Se você tiver npm instalado, nodemon é provavelmente a maneira mais fácil de começar, especialmente no OS X, que aparentemente não possui ferramentas de inotificação. Ele suporta a execução de um comando quando uma pasta é alterada.

    
por 10.06.2012 / 01:51
15

rerun2 ( no github ) é um script Bash de 10 linhas do formulário:

#!/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

Salve a versão do github como 're-executar' no seu PATH, e chame-a usando:

rerun COMMAND

Ele executa o comando toda vez que há um evento de modificação do sistema de arquivos dentro do seu diretório atual (recursivo).

Coisas que alguém pode gostar:

  • Ele usa inotify, portanto, é mais responsivo do que o polling. Fabuloso para executar testes unitários de sub-milissegundos ou renderizar arquivos de pontos graphviz, sempre que você clicar em "salvar".
  • Por ser muito rápido, você não precisa se preocupar em dizer que ignora grandes subdiretórios (como node_modules) apenas por motivos de desempenho.
  • É extremamente super responsivo, porque só chama inotifywait uma vez, na inicialização, em vez de executá-lo, e incorrer no caro golpe de estabelecer relógios, em cada iteração.
  • São apenas 12 linhas de Bash
  • Por ser o Bash, ele interpreta comandos que você passa exatamente como se tivesse digitado em um prompt do Bash. (Presumivelmente, isso é menos legal se você usar outro shell).
  • Ele não perde eventos que ocorrem enquanto o COMMAND está sendo executado, diferentemente da maioria das outras soluções de inotificação nesta página.
  • No primeiro evento, ele insere um 'período inativo' por 0,15 segundo, durante o qual outros eventos são ignorados, antes que o COMMAND seja executado exatamente uma vez. Isto é assim que a enxurrada de eventos causados pela dança create-write-move que o Vi ou o Emacs faz ao salvar um buffer não causa múltiplas execuções laboriosas de um conjunto de testes possivelmente lento. Quaisquer eventos que ocorrerem enquanto o COMMAND está sendo executado não serão ignorados - eles causarão um segundo período morto e a execução subseqüente.

Coisas que alguém pode não gostar:

  • Ele usa inotify, então não funcionará fora do Linuxland.
  • Como ele usa o inotify, ele irá tentar assistir a diretórios contendo mais arquivos do que o número máximo de usuários inotify watches. Por padrão, isso parece ser definido em torno de 5.000 a 8.000 em diferentes máquinas que uso, mas é fácil aumentar. Consulte link
  • Falha ao executar comandos contendo aliases de Bash. Eu poderia jurar que isso costumava funcionar. Em princípio, como esse é Bash, não está executando o COMMAND em uma subshell, espero que isso funcione. Eu adoraria ouvir Se alguém souber por que isso não acontece. Muitas das outras soluções nesta página também não podem executar tais comandos.
  • Pessoalmente, gostaria de poder acertar uma chave no terminal em que ela está sendo executada manualmente para causar uma execução extra do COMMAND. Eu poderia adicionar isso de alguma forma, simplesmente? Um loop concorrente 'while read -n1' que também chama execute?
  • No momento, eu o codifiquei para limpar o terminal e imprimir o COMMAND executado em cada iteração. Algumas pessoas gostariam de adicionar sinalizadores de linha de comando para ativar coisas como isso, etc. Mas isso aumentaria o tamanho e a complexidade em muitos aspectos.

Este é um refinamento do anwer da @ cychoi.

    
por 09.09.2015 / 23:49
13

Para quem não pode instalar inotify-tools como eu, isso deve ser útil:

watch -d -t -g ls -lR

Este comando sairá quando a saída for alterada, ls -lR listará todos os arquivos e diretórios com seu tamanho e datas, portanto, se um arquivo for alterado, ele deve sair do comando, como diz o homem:

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

Sei que esta resposta pode não ser lida por ninguém, mas espero que alguém a alcance.

Exemplo de linha de comando:

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

Abra outro terminal:

~ $ echo "testing" > /tmp/test

Agora, o primeiro terminal gerará 1,2,3

Exemplo de script simples:

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

watch -d -t -g ls -lR ${DIR_TO_WATCH} && ${COMMAND}
    
por 22.03.2016 / 15:57
12

Aqui está um shell shell Bourne simples que:

  1. Aceita dois argumentos: o arquivo a ser monitorado e um comando (com argumentos, se necessário)
  2. Copia o arquivo que você está monitorando para o diretório / tmp
  3. Verifica a cada dois segundos para ver se o arquivo que você está monitorando é mais recente que a cópia
  4. Se for mais recente, substitui a cópia pelo original mais recente e executa o comando
  5. Limpa depois de si quando você pressiona 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  
    

Isso funciona no FreeBSD. O único problema de portabilidade em que posso pensar é se algum outro Unix não tiver o comando mktemp (1), mas nesse caso você pode codificar apenas o nome do arquivo temporário.

    
por 27.08.2010 / 23:23
8

Dê uma olhada em incron . É semelhante ao cron, mas usa eventos inotify em vez de tempo.

    
por 27.08.2010 / 22:12
6

Outra solução com o NodeJs, fsmonitor :

  1. Instalar

    sudo npm install -g fsmonitor
    
  2. Da linha de comando (exemplo, monitore logs e "varejo" se um arquivo de log for alterado)

    fsmonitor -s -p '+*.log' sh -c "clear; tail -q *.log"
    
por 22.01.2014 / 15:55
6

No Linux:

man watch

watch -n 2 your_command_to_run

Será executado o comando a cada 2 segundos.

Se o seu comando demorar mais de 2 segundos para ser executado, o relógio esperará até que seja feito antes de executá-lo novamente.

    
por 02.07.2012 / 18:36
6

Olhe para o Guard, em particular com este plug-in:

link

Você pode configurá-lo para assistir a qualquer número de padrões no diretório do seu projeto e executar comandos quando ocorrem alterações. Boa chance até mesmo que haja um plugin disponível para o que você está tentando fazer em primeiro lugar.

    
por 09.07.2014 / 15:16
5

se você tiver o nodemon instalado, você poderá fazer isso:

nodemon -w <watch directory> -x "<shell command>" -e ".html"

No meu caso eu edito o html localmente e envio para o meu servidor remoto quando um arquivo é alterado.

nodemon -w <watch directory> -x "scp filename [email protected]:/var/www" -e ".html"
    
por 09.08.2014 / 00:20
5

Watchdog é um projeto em Python e pode ser exatamente o que você está procurando:

Supported platforms

  • Linux 2.6 (inotify)
  • Mac OS X (FSEvents, kqueue)
  • FreeBSD/BSD (kqueue)
  • Windows (ReadDirectoryChangesW with I/O completion ports; ReadDirectoryChangesW worker threads)
  • OS-independent (polling the disk for directory snapshots and comparing them periodically; slow and not recommended)

Acabei de escrever um wrapper de linha de comando para watchdog_exec :

Exemplo é executado

No evento fs envolvendo arquivos e pastas no diretório atual, execute o comando echo $src $dst , a menos que o evento fs seja modificado e, em seguida, execute o comando python $src .

python -m watchdog_exec . --execute echo --modified python

Usando argumentos curtos e restringindo-se a somente executar quando os eventos envolvem " main .py":

python -m watchdog_exec . -e echo -a echo -s __main__.py

EDIT: Acabou de encontrar o Watchdog tem um CLI oficial chamado watchmedo , então verifique isso também.

    
por 28.12.2015 / 11:44
4

Se o seu programa gerar algum tipo de log / output, você pode criar um Makefile com uma regra para esse log / output que depende do seu script e fazer algo como

while true; do make -s my_target; sleep 1; done

Como alternativa, você pode criar um destino falso e fazer com que a regra chame seu script e toque no destino falso (embora ainda dependa do seu script).

    
por 27.08.2010 / 22:37
4

Melhorado com a resposta de Gilles .

Esta versão executa inotifywait uma vez e monitora eventos (por exemplo, modify ) a partir de então. Tal que inotifywait não precise ser re-executado em todos os eventos encontrados.

É rápido e rápido! (mesmo ao monitorar o grande diretório de forma recursiva)

inotifywait --quiet --monitor --event modify FILE | while read; do
    # trim the trailing space from inotifywait output
    REPLY=${REPLY% }
    filename=${REPLY%% *}
    # do whatever you want with the $filename
done
    
por 17.06.2015 / 20:03
3

Um pouco mais sobre o lado da programação, mas você quer algo como inotify . Existem implementações em muitos idiomas, como jnotify e pyinotify .

Esta biblioteca permite monitorar arquivos únicos ou diretórios inteiros e retorna eventos quando uma ação é descoberta. As informações retornadas incluem o nome do arquivo, a ação (criar, modificar, renomear, excluir) e o caminho do arquivo, entre outras informações úteis.

    
por 27.08.2010 / 22:05
3

Para aqueles que estão procurando por uma solução FreeBSD, aqui está a porta:

/usr/ports/sysutils/wait_on
    
por 11.11.2012 / 22:48
3

Eu gosto da simplicidade de while inotifywait ...; do ...; done , mas tem dois problemas:

  • Alterações no arquivo que acontecem durante o do ...; serão perdidas
  • Lento ao usar no modo recursivo

Por isso, criei um script auxiliar que usa o inotifywait sem essas limitações: inotifyexec

Sugiro que você coloque este script em seu caminho, como em ~/bin/ . O uso é descrito apenas executando o comando.

Exemplo: inotifyexec "echo test" -r .

    
por 29.04.2014 / 15:56
3

Melhorou a solução de Sebastian com o comando watch :

watch_cmd.sh :

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

while true; do
  watch -d -g "${WATCH_COMMAND}"
  ${COMMAND}
  sleep 1     # to allow break script by Ctrl+c
done

Exemplo de chamada:

watch_cmd.sh "ls -lR /etc/nginx | grep .conf$" "sudo service nginx reload"

Funciona, mas tenha cuidado: o comando watch tem erros conhecidos (veja man): ele reage apenas às alterações no VISIBLE nas partes do terminal de -g CMD output.

    
por 12.04.2016 / 16:53
2

Você pode tentar reflexionar .

Reflex é uma pequena ferramenta para assistir a um diretório e reexecutar um comando quando certos arquivos são alterados. É ótimo para executar automaticamente tarefas de compilação / lint / teste e para recarregar seu aplicativo quando o código é alterado.

# Rerun make whenever a .c file changes
reflex -r '\.c$' make
    
por 01.03.2017 / 21:14
1

Uma resposta on-line que estou usando para acompanhar uma alteração no arquivo:

$ while true ; do NX='stat -c %Z file' ; [[ $BF != $NX ]] && date >> ~/tmp/fchg && BF=$NX || sleep 2 ; done

Você não precisa inicializar o BF se souber que a primeira data é a hora de início.

Isto é simples e portátil. Há outra resposta baseada na mesma estratégia usando um script aqui. Dê uma olhada também.

Uso: estou usando isso para depurar e ficar de olho em ~/.kde/share/config/plasma-desktop-appletsrc ; que por algum motivo desconhecido continua perdendo meu SwitchTabsOnHover=false

    
por 01.09.2015 / 06:53
1

Eu uso esse script para fazer isso. Estou usando o inotify no modo monitor

#!/bin/bash
MONDIR=$(dirname $1)
ARQ=$(basename $1)

inotifywait -mr -e close_write $MONDIR | while read base event file 
do
  if (echo $file |grep -i "$ARQ") ; then
    $1
  fi
done

Salve isso como runatwrite.sh

Usage: runatwrite.sh myfile.sh

ele irá rodar myfile.sh em cada gravação.

    
por 26.02.2016 / 20:10
1

Eu tenho um GIST para isso e o uso é bastante simples

watchfiles <cmd> <paths...>

link

    
por 05.04.2017 / 09:38
1

Para aqueles que usam o OS X, você pode usar um LaunchAgent para assistir a um caminho / arquivo para alterações e fazer algo quando isso acontecer. FYI - LaunchControl é um bom aplicativo para fazer / modificar / remover facilmente daemons / agentes.

( exemplo tirado daqui )

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN
http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>test</string>
    <key>ProgramArguments</key>
    <array>
        <string>say</string>
        <string>yy</string>
    </array>
    <key>WatchPaths</key>
    <array>
        <string>~/Desktop/</string>
    </array>
</dict>
</plist>
    
por 22.03.2016 / 16:33
0

find pode fazer o truque.

while true; do
    find /path/to/watched/file -ctime 1s | xargs do-what-you-will
done

find -ctime 1s imprime o nome do arquivo se foi alterado no último 1s .

    
por 28.01.2019 / 17:51
0

Eu tive uma situação um pouco diferente. Mas sinto que isso pode ser útil para alguém que esteja lendo essa pergunta.

Eu precisava ser notificado quando um arquivo de log mudava de tamanho, mas não era necessário imediatamente. E poderia levar dias ou semanas no futuro, então eu não poderia usar o inotify (que não estava instalado / ativado naquele servidor) na linha de comando (eu não queria usar nohup ou similar). Então, decidi executar um script bash em cron para verificar

O script grava o tamanho do arquivo assistido em um arquivo de texto e em cada cron run e verifica se esse valor foi alterado e envia a última linha para mim se for alterado

#!/bin/bash
FILE_TO_WATCH="/path/to/log_file.log"
FILESIZE_FILE="/path_to/record.txt"
SUBJECT="Log file 'log_file.log' has changed"
MAILTO="[email protected]"
BODY="Last line of log file:\n"
LAST_LINES=1

# get old recorded file size from file
OLD_FILESIZE=$(cat "${FILESIZE_FILE}")
# write current file size into file
stat --printf="%s" "${FILE_TO_WATCH}" > "${FILESIZE_FILE}"
# get new recorded file size from file
NEW_FILESIZE=$(cat "${FILESIZE_FILE}")


if [ "${OLD_FILESIZE}" != "${NEW_FILESIZE}" ]; then
    echo -e "${BODY}"$(tail -${LAST_LINES} ${FILE_TO_WATCH}) | mail -s "${SUBJECT}" "${MAILTO}"
fi
    
por 21.03.2019 / 12:33
0

Para as pessoas que acham isso pesquisando as alterações em um arquivo específico , a resposta é muito mais simples (inspirada em Resposta de Gilles ).

Se você quiser fazer algo após um arquivo em particular foi gravado, veja como:

while true; do
  inotifywait -e modify /path/to/file
  # Do something *after* a write occurs, e.g. copy the file
  /bin/cp /path/to/file /new/path
done

Salve isso como, por exemplo, copy_myfile.sh e coloque o arquivo .sh na pasta /etc/init.d/ para que ele seja executado na inicialização.

    
por 13.05.2015 / 17:21