Questão Como executar um comando sempre que um arquivo é alterado?


Eu quero uma maneira rápida e simples de executar um comando sempre que um arquivo é 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 esse terminal e pressionar Entrar, 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.

Atualizar: Eu quero algo simples, descartável, se possível. Além do mais, eu quero que algo seja executado em um terminal, porque eu quero ver a saída do programa (eu 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, eu estou aceitando o que eu realmente usei (foi simples, rápido e fácil de lembrar), mesmo sabendo que não é o mais elegante.


357


origem


Possível duplicado entre sites de: stackoverflow.com/questions/2972765/… (embora aqui esteja no tópico =)) - Ciro Santilli 新疆改造中心 六四事件 法轮功
Eu fiz referência antes de um cross site duplicado e foi negado: S;) - Francisco Tapia
A solução de Jonathan Hartley baseia-se em outras soluções aqui e corrige grandes problemas que as respostas mais votadas têm: falta algumas modificações e ser ineficiente. Por favor mude a resposta aceita para a dele, que também está sendo mantida no github em github.com/tartley/rerun2 (ou para alguma outra solução sem essas falhas) - nealmcb


Respostas:


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

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 trecho é mais simples, mas tem uma desvantagem significativa: perderá as alterações realizadas enquanto inotifywait não está em execução (em particular enquanto myfile está correndo). 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ços em branco. Se isso é um problema, use o --format opção 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 maneira, há uma limitação: se algum programa substituir myfile.py com um arquivo diferente, em vez de escrever no arquivo existente myfile, inotifywait irá 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).


341



Eu encapsulei tudo isso (com alguns truques bash) em um simples de usar sleep_until_modified.sh roteiro, disponível em: bitbucket.org/denilsonsa/small_scripts/src - Denilson Sá Maia
while sleep_until_modified.sh derivation.tex ; do latexmk -pdf derivation.tex ; done é fantástico. Obrigado. - Rhys Ulerich
inotifywait -e delete_self parece funcionar bem para mim. - Kos
É simples, mas tem dois problemas importantes: Eventos podem ser perdidos (todos os eventos no loop) e a inicialização do inotifywait é feita a cada vez, o que torna essa solução mais lenta para grandes pastas recursivas. - Wernight
Por algum motivo while inotifywait -e close_write myfile.py; do ./myfile.py; done sempre sai sem executar o comando (bash e zsh). Para isso funcionar eu precisava adicionar || true, por exemplo: while inotifywait -e close_write myfile.py || true; do ./myfile.py; done - ideasman42


entr (http://entrproject.org/) 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 projeto para executar os testes de unidade para o código que estou modificando no momento, e isso já foi um grande impulso para o meu fluxo de trabalho.

Bandeiras como -c (limpe a tela entre as execuções) e -d (saia quando um novo arquivo é adicionado a um diretório monitorado) adicione 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); a construção do repositório do autor era indolor em qualquer caso.


120



Não manipula novos arquivos e suas modificações. - Wernight
@Wernight - a partir de 7 de maio de 2014 entr tem o novo -d bandeira; é um pouco mais longo, mas você pode fazer while sleep 1 ; do find . -name '*.py' | entr -d ./myfile.py ; done para lidar com novos arquivos. - Paul Fenney
disponível em aur aur.archlinux.org/packages/entr - Victor Häggqvist
melhor que eu encontrei no OS X com certeza. fswatch pega muitos eventos funky e eu não quero gastar o tempo para descobrir o porquê - dtc
Vale a pena notar que entr está disponível no Homebrew, então brew install entr funcionará como esperado - jmarceli


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

O uso é simples:

when-changed FILE COMMAND...

Ou para assistir vários arquivos:

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

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


100



@ Sim, sim, na versão mais recente do código :) - joh
Agora disponível em "pip install when-changed". Ainda funciona bem. Obrigado. - A. L. Flanagan
Para limpar a tela primeiro você pode usar when-changed FILE 'clear; COMMAND'. - Dave James Miller
Esta resposta é muito melhor porque também posso fazê-lo no Windows. E esse cara realmente escreveu um programa para obter a resposta. - Wolfpack'08
Boas notícias, pessoal! when-changed agora é multi-plataforma! Confira as últimas Liberação 0.3.0 :) - joh


Como sobre esse script? Ele usa o stat comando 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

45



Não seria stat-o tempo modificado é melhor "sempre que um arquivo muda" responder? - Xen2050
A execução da estatística várias vezes por segundo causa muitas leituras no disco? ou a chamada do sistema fstat faria automaticamente cache dessas respostas de alguma forma? Estou tentando escrever uma espécie de "grunhido" para compilar meu código c sempre que fizer alterações - Oskenso Kashi
Isso é bom se você souber o nome do arquivo a ser observado com antecedência. Melhor seria passar o nome do arquivo para o script. Melhor ainda seria se você pudesse passar muitos nomes de arquivos (por exemplo, "mywatch * .py"). Melhor ainda seria se pudesse operar recursivamente em arquivos em subdiretórios também, o que algumas das outras soluções fazem. - Jonathan Hartley
Apenas no caso de alguém estar se perguntando sobre leituras pesadas, eu testei esse script no Ubuntu 17.04 com um sono de 0.05s e vmstat -d para assistir ao acesso ao disco. Parece que o Linux faz um trabalho fantástico no cache desse tipo de coisa: D - Oskenso Kashi
Há erro de digitação no "comando", eu estava tentando consertar, mas S.O. diz "Editar não deve ter menos de 6 caracteres" - user337085


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 rodar :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 seu fluxo de edição, como a saída irá escrever sobre o seu editor por alguns segundos, até que você pressione Enter), remova o :silent comando.


28



Isso pode ser muito bom quando combinado com entr (veja abaixo) - apenas faça o vim tocar um arquivo fictício que o entr está assistindo, e deixe o entr fazer o resto em segundo plano ... ou tmux send-keys se acontecer de você estar em tal ambiente :) - Paul Fenney
bom! você pode fazer uma macro para o seu .vimrc Arquivo - ErichBSchulz


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.


24



No entanto, ele só assiste arquivos .js e .coffee. - zelk
A versão atual parece suportar qualquer comando, por exemplo: nodemon -x "bundle exec rspec" spec/models/model_spec.rb -w app/models -w spec/models - kek
Eu gostaria de ter mais informações, mas o osx tem um método para rastrear mudanças, fsevents - ConstantineK
No OS X, você também pode usar Lançamento Daemons com um WatchPaths chave como mostrado no meu link. - Adam Johns


Aqui está um shell shell Bourne simples que:

  1. Leva 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 novo 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 mesmo 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.


12



Polling é a única forma portável, mas a maioria dos sistemas tem um mecanismo de notificação de mudança de arquivo (inotify no Linux, kqueue no FreeBSD, ...). Você tem um problema de citação grave quando você faz $cmd, mas felizmente isso é facilmente corrigível: cmd variável e executar "$@". Seu script não é adequado para monitorar um arquivo grande, mas isso pode ser corrigido ao substituir cp de touch -r (você só precisa da data, não do conteúdo). Portabilidade-wise, o -nt teste requer bash, ksh ou zsh. - Gilles


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 invoque-a usando:

rerun COMMAND

Ele executa o COMMAND toda vez que há um evento modify do sistema de arquivos dentro do diretório atual (recursivo).

Coisas que alguém pode gostar:

  • Ele usa inotify, então é mais responsivo do que polling. Fabuloso para executar testes unitários de sub-milissegundos ou renderizar arquivos de pontos graphviz, sempre que você clicar em "salvar".
  • Porque é tão rápido, você não precisa se preocupar em dizer para ignorar subdirs grandes (como node_modules) apenas por razões 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
  • Como é 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, ao contrário da maioria das outras soluções de inotificação nesta página.
  • No primeiro evento, ele entra em um "período morto" 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.
  • Porque ele usa inotify, ele irá tentar assistir a diretórios contendo mais arquivos do que o número máximo de usuários inotify relógios. 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. Vejo https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached
  • Falha ao executar comandos contendo aliases de Bash. Eu poderia jurar que isso costumava funcionar. Em princípio, como esse é o 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, eu 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?
  • Agora eu o codifiquei para limpar o terminal e imprimir o comando executado em cada iteração. Algumas pessoas gostariam de adicionar sinalizadores de linha de comando para ativar coisas como essas, etc. Mas isso aumentaria o tamanho e a complexidade em muitas vezes.

Este é um refinamento do anwer do @ cychoi.


12



Eu acredito que você deveria usar "$@" ao invés de $@, a fim de trabalhar corretamente com argumentos contendo espaços. Mas ao mesmo tempo você usa eval, o que força o usuário de reexecução a ser mais cuidadoso ao citar. - Denilson Sá Maia
Obrigado Denilson. Você poderia dar um exemplo de onde a citação precisa ser feita com cuidado? Eu tenho usado nas últimas 24 horas e não vi nenhum problema com espaços até agora, nem cuidadosamente citado qualquer coisa - apenas invocado como rerun 'command'. Você está apenas dizendo que se eu usasse "$ @", o usuário poderia invocar como rerun command (sem aspas?) Isso não parece tão útil para mim: eu geralmente não quero que o Bash faça qualquer processamento do comando antes de passá-lo para executar novamente. por exemplo. Se o comando contiver "echo $ myvar", então eu quero ver os novos valores de myvar em cada iteração. - Jonathan Hartley
Algo como rerun foo "Some File" pode quebrar. Mas desde que você está usando eval, pode ser reescrito como rerun 'foo "Some File". Observe que, às vezes, a expansão do caminho pode introduzir espaços: rerun touch *.foo provavelmente vai quebrar, e usando rerun 'touch *.foo' tem uma semântica ligeiramente diferente (a expansão do caminho ocorre apenas uma vez ou várias vezes). - Denilson Sá Maia
Obrigado pela ajuda. Sim: rerun ls "some file" quebra por causa dos espaços. rerun touch *.foo* funciona normalmente, mas falha se os nomes de arquivos que correspondem a * .foo contiverem espaços. Obrigado por me ajudar a ver como rerun 'touch *.foo' tem semânticas diferentes, mas eu suspeito que a versão com aspas simples é a semântica que eu quero: eu quero que cada iteração da reprise funcione como se eu tivesse digitado o comando novamente - daí eu quer  *.foo para ser expandido em cada iteração. Vou tentar suas sugestões para examinar seus efeitos ... - Jonathan Hartley
Mais discussão sobre este PR (github.com/tartley/rerun2/pull/1) e outros. - Jonathan Hartley


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


8



Isso pode ser feito para funcionar, mas a criação de uma entrada de incron é um processo muito trabalhoso em comparação com outras soluções nesta página. - Jonathan Hartley


Para quem não consegue 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.

Eu sei que esta resposta pode não ser lida por ninguém, mas espero que alguém chegue a ela.

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 produzirá 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}

8



Nice hack. Eu testei e parece ter um problema quando a listagem é longa e o arquivo alterado fica fora da tela. Uma pequena modificação poderia ser algo assim: watch -d -t -g "ls -lR tmp | sha1sum" - Atle
se você observar sua solução a cada segundo, ela funcionará para sempre e executará MY_COMMAND somente se algum arquivo for alterado: watch -n1 "watch -d -t -g ls -lR && MY_COMMAND" - mnesarco
Minha versão do relógio (no Linux, watch from procps-ng 3.3.10) aceita segundos de flutuação para seu intervalo, watch -n0.2 ... vai pesquisar a cada quinto de segundo. Bom para aqueles testes unitários sub-milissegundos saudáveis. - Jonathan Hartley


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"
    

7



Nota lateral: o exemplo poderia ser resolvido por tail -F -q *.log, Eu acho que. - Volker Siegel