Как выполнить команду при изменении файла?

Я хочу быстрый и простой способ выполнить команду при изменении файла. Я хочу что-то очень простое, что-то, что я оставлю работающим на терминале и закрою это всякий раз, когда я закончу работать с этим файлом.

В настоящее время я использую это:

while read; do ./myfile.py ; done

И затем мне нужно перейти к этому терминалу и нажимать Enter, всякий раз, когда я сохраняю этот файл в моем редакторе. Я хочу что-то вроде этого:

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

Или любое другое решение, столь же простое.

Кстати: я использую Vim, и я знаю, что могу добавить автокоманду для запуска чего-либо на BufWrite, но сейчас это не то решение, которое мне нужно.

Обновление: я хочу что-то простое, если возможно, отказаться. Более того, я хочу, чтобы что-то запускалось в терминале, потому что я хочу видеть вывод программы (я хочу видеть сообщения об ошибках).

Об ответах: Спасибо за все ваши ответы! Все они очень хороши, и каждый из них отличается от других. Поскольку мне нужно принять только один, я принимаю тот, который я фактически использовал (это было просто, быстро и легко запомнить), хотя я знаю, что это не самый элегантный.

41 ответ

Решение

Просто, с помощью inotifywait (установите ваш дистрибутив inotify-tools упаковка):

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

или же

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

Первый фрагмент проще, но у него есть существенный недостаток: он пропустит изменения, выполненные в то время как inotifywait не работает (в частности, пока myfile бежит). Второй фрагмент не имеет этого дефекта. Однако следует помнить, что предполагается, что имя файла не содержит пробелов. Если это проблема, используйте --format возможность изменить вывод, чтобы не включать имя файла:

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

В любом случае, есть ограничение: если какая-то программа заменяет myfile.py с другим файлом, вместо записи в существующий myfile, inotifywait умрет. Многие редакторы работают таким образом.

Чтобы преодолеть это ограничение, используйте inotifywait в каталоге:

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

В качестве альтернативы используйте другой инструмент, который использует те же базовые функции, например, incron (позволяет регистрировать события при изменении файла) или fswatch (инструмент, который также работает во многих других вариантах Unix с использованием аналога каждого варианта inotify Linux).

entr ( http://entrproject.org/) предоставляет более дружественный интерфейс для inotify (а также поддерживает *BSD и Mac OS X).

Это позволяет очень легко указать несколько файлов для просмотра (ограничено только ulimit -n), избавляет от необходимости иметь дело с заменяемыми файлами и требует меньше синтаксиса bash:

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

Я использовал его во всем дереве исходного кода проекта для запуска модульных тестов кода, который я сейчас изменяю, и это уже значительно улучшило мой рабочий процесс.

Флаги как -c (очистить экран между прогонами) и -d (выйти, когда новый файл добавлен в отслеживаемый каталог), добавить еще большую гибкость, например, вы можете сделать:

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

По состоянию на начало 2018 года он все еще находится в активной разработке, и его можно найти в Debian & Ubuntu (apt install entr); Строительство из репо автора было в любом случае безболезненным.

Я написал программу на Python, чтобы сделать именно это, когда-изменился.

Использование простое:

when-changed FILE COMMAND...

Или посмотреть несколько файлов:

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

FILE может быть каталогом. Смотреть рекурсивно с -r, использование %f передать имя файла команде.

Как насчет этого сценария? Он использует stat команда для получения времени доступа к файлу и запускает команду при каждом изменении времени доступа (при каждом обращении к файлу).

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

Для тех, кто не может установить inotify-tools как и я, это должно быть полезно:

watch -d -t -g ls -lR

Эта команда выйдет при изменении выхода, ls -lR перечислит каждый файл и каталог с указанием его размера и даты, поэтому, если файл будет изменен, он должен выйти из команды, как говорит man:

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

Я знаю, что этот ответ не может быть прочитан никем, но я надеюсь, что кто-то достигнет его.

Пример командной строки:

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

Откройте другой терминал:

~ $ echo "testing" > /tmp/test

Теперь первый терминал будет выводить 1,2,3

Простой пример скрипта:

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

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

Решение с использованием Vim:

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

Но я не хочу, чтобы это решение, потому что это немного раздражает, печатать, немного сложно вспомнить, что именно печатать, и немного сложно отменить его эффекты (нужно запустить :au! BufWritePost myfile.py). Кроме того, это решение блокирует Vim до завершения выполнения команды.

Я добавил это решение здесь только для полноты, так как оно может помочь другим людям.

Чтобы отобразить вывод программы (и полностью прервать процесс редактирования, поскольку вывод будет записываться в ваш редактор в течение нескольких секунд, пока вы не нажмете Enter), удалите :silent команда.

Если вам случится npm установлены, nodemon Вероятно, это самый простой способ начать работу, особенно в OS X, которая, по-видимому, не имеет инструментов inotify. Он поддерживает запуск команды при изменении папки.

rerun2 ( на github) представляет собой 10-строчный Bash-скрипт вида:

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

Сохраните версию github как 'rerun' в вашей переменной PATH и вызовите ее, используя:

rerun COMMAND

Он запускает COMMAND каждый раз, когда в вашем текущем каталоге происходит событие изменения файловой системы (рекурсивно.)

Вещи, которые могут понравиться об этом:

  • Он использует inotify, поэтому более отзывчив, чем опрос. Потрясающе для запуска юнит-тестов с точностью до миллисекунды или рендеринга файлов графических точек каждый раз, когда вы нажимаете "сохранить".
  • Поскольку это так быстро, вам не нужно беспокоиться о том, чтобы игнорировать большие подкаталоги (например, node_modules) только из соображений производительности.
  • Это очень супер-отзывчивый, потому что он вызывает inotifywait только один раз, при запуске, вместо того, чтобы запускать его, и на каждой итерации возникает дорогостоящий удар по созданию часов.
  • Это всего лишь 12 строк Bash
  • Поскольку это Bash, он интерпретирует команды, которые вы передаете, точно так же, как если бы вы вводили их в приглашении Bash. (Предположительно, это менее круто, если вы используете другую оболочку.)
  • Он не теряет события, которые происходят во время выполнения команды COMMAND, в отличие от большинства других решений inotify на этой странице.
  • При первом событии он вводит "мертвый период" на 0,15 секунды, в течение которого другие события игнорируются, прежде чем COMMAND будет запущен ровно один раз. Это происходит из-за того, что поток событий, вызванных танцем create-write-move, который Vi или Emacs делает при сохранении буфера, не вызывает много трудоемких выполнений, возможно, медленного набора тестов. Любые события, которые затем происходят во время выполнения COMMAND, не игнорируются - они вызовут второй мертвый период и последующее выполнение.

Вещи, которые могут не понравиться об этом:

  • Он использует inotify, поэтому не будет работать за пределами Linuxland.
  • Поскольку он использует inotify, он будет пытаться просмотреть каталоги, содержащие больше файлов, чем максимальное количество пользовательских наблюдений inotify. По умолчанию на разных компьютерах, которые я использую, установлено, что оно составляет от 5000 до 8000, но его легко увеличить. См. https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached
  • Не удается выполнить команды, содержащие псевдонимы Bash. Я могу поклясться, что это работало. В принципе, поскольку это Bash, который не выполняет COMMAND в подоболочке, я ожидал, что это сработает. Я хотел бы услышать, если кто-нибудь знает, почему это не так. Многие другие решения на этой странице также не могут выполнять такие команды.
  • Лично я хотел бы нажать клавишу в терминале, в котором он работает, чтобы вручную вызвать дополнительное выполнение команды. Могу ли я добавить это как-нибудь, просто? Параллельно работающий цикл while read -n1, который также вызывает execute?
  • Прямо сейчас я кодировал его, чтобы очистить терминал и печатать выполненную КОМАНДУ на каждой итерации. Некоторые люди могут захотеть добавить флаги командной строки, чтобы отключить подобные вещи и т. Д. Но это увеличит размер и сложность во много раз.

Это уточнение ответа @cychoi.

Если у вас установлен nodemon, то вы можете сделать это:

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

В моем случае я редактирую html локально и отправляю его на мой удаленный сервер при изменении файла.

nodemon -w <watch directory> -x "scp filename jaym@jay-remote.com:/var/www" -e ".html"

Вот простой сценарий оболочки Bourne, который:

  1. Принимает два аргумента: файл для мониторинга и команду (с аргументами, если необходимо)
  2. Копирует файл, который вы отслеживаете, в каталог /tmp
  3. Проверяет каждые две секунды, чтобы увидеть, является ли файл, который вы отслеживаете, более новым, чем копия
  4. Если он новее, он перезаписывает копию новым оригиналом и выполняет команду
  5. Убирает за собой при нажатии 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  
    

Это работает на FreeBSD. Единственная проблема с переносимостью, о которой я могу подумать, это то, что в некоторых других Unix нет команды mktemp(1), но в этом случае вы можете просто жестко закодировать имя временного файла.

Посмотрите на Incron. Это похоже на cron, но использует события inotify вместо времени.

Под Linux:

man watch

watch -n 2 your_command_to_run

Будет запускать команду каждые 2 секунды.

Если ваша команда выполняется более 2 секунд, часы будут ждать, пока она не будет выполнена, прежде чем делать это снова.

Другое решение с NodeJs, fsmonitor:

  1. устанавливать

    sudo npm install -g fsmonitor
    
  2. Из командной строки (например, монитор журналов и "розничная торговля", если один файл журнала изменяется)

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

Улучшено после ответа Жиля .

Эта версия работает inotifywait один раз и следит за событиями (.eg: modify ) после этого Такой, что inotifywait не требует повторного выполнения при каждом обнаруженном событии.

Это быстро и быстро! (даже при рекурсивном мониторинге большого каталога)

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

Watchdog - это проект Python, и он может быть именно тем, что вы ищете:

Поддерживаемые платформы

  • Linux 2.6 (inotify)
  • Mac OS X (FSEvents, kqueue)
  • FreeBSD / BSD (kqueue)
  • Windows (ReadDirectoryChangesW с портами завершения ввода / вывода; рабочие потоки ReadDirectoryChangesW)
  • Независимо от ОС (опрос диска на наличие снимков каталогов и их периодическое сравнение; медленный и не рекомендуется)

Просто написал оболочку командной строки для него watchdog_exec:

Пример работает

На событии fs, включающем файлы и папки в текущем каталоге, запустите echo $src $dst команда, если это событие fs не изменено, затем запустите python $src команда.

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

Использование коротких аргументов и ограничение на выполнение только тогда, когда в событиях используется " main.py":

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

РЕДАКТИРОВАТЬ: Только что обнаружен Watchdog имеет официальный CLI под названием watchmedo так что проверьте это тоже.

Посмотрите на Guard, в частности, с этим плагином:

https://github.com/hawx/guard-shell

Вы можете настроить его на просмотр любого количества шаблонов в каталоге вашего проекта и выполнение команд при возникновении изменений. Хороший шанс, даже если есть плагин для того, что вы пытаетесь сделать в первую очередь.

Если ваша программа генерирует какой-либо журнал / вывод, вы можете создать Makefile с правилом для этого журнала / вывода, который зависит от вашего сценария и сделать что-то вроде

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

Кроме того, вы можете создать фальшивую цель и назначить ей правило, и вызвать ваш скрипт, и коснуться фальшивой цели (хотя все еще зависит от вашего скрипта).

Мне нравится простота while inotifywait ...; do ...; done однако у него есть две проблемы:

  • Изменения файлов, происходящие во время do ...; будет пропущено
  • Медленно при использовании в рекурсивном режиме

Для этого я создал вспомогательный скрипт, который использует inotifywait без этих ограничений: inotifyexec

Я предлагаю вам поставить этот скрипт на вашем пути, как в ~/bin/, Использование описывается просто запуском команды.

Пример: inotifyexec "echo test" -r .

Улучшенное решение Себастьяна с 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

Пример звонка:

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

Это работает, но будьте осторожны: watch команда имеет известные ошибки (см. man): она реагирует на изменения только в VISIBLE в терминальных частях -g CMD выход.

Вы можете попробовать рефлекс.

Reflex - небольшой инструмент для просмотра каталога и повторного запуска команды при изменении определенных файлов. Он отлично подходит для автоматического запуска задач компиляции /lint/test и для перезагрузки приложения при изменении кода.

# Rerun make whenever a .c file changes
reflex -r '\.c$' make

Еще немного о программировании, но вы хотите что-то вроде inotify. Существуют реализации на многих языках, таких как jnotify и pyinotify.

Эта библиотека позволяет отслеживать отдельные файлы или целые каталоги и возвращает события при обнаружении действия. Возвращаемая информация включает в себя имя файла, действие (создание, изменение, переименование, удаление) и путь к файлу, а также другую полезную информацию.

Для тех из вас, кто ищет решение FreeBSD, вот порт:

/usr/ports/sysutils/wait_on

Я использую этот скрипт, чтобы сделать это. Я использую inotify в режиме монитора

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

Сохраните это как runatwrite.sh

Usage: runatwrite.sh myfile.sh

он будет запускать myfile.sh при каждой записи.

Проверьте https://github.com/watchexec/watchexec.

watchexec - это простой, автономный инструмент, который отслеживает путь и запускает команду всякий раз, когда обнаруживает изменения.

пример

Просматривайте все файлы JavaScript, CSS и HTML в текущем каталоге и все подкаталоги на наличие изменений, запуская make при обнаружении изменения:

$ watchexec --exts js,css,html make

Ответ oneliner, который я использую, чтобы отслеживать изменения файла:

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

Вам не нужно инициализировать BF, если вы знаете, что первая дата является временем начала.

Это просто и портативно. Здесь есть другой ответ, основанный на той же стратегии с использованием сценария. Посмотрите также.


Использование: я использую это, чтобы отладить и следить за ~/.kde/share/config/plasma-desktop-appletsrc; что по неизвестной причине продолжает терять SwitchTabsOnHover=false

Я написал программу на Python, чтобы сделать именно это, называется rerun,

ОБНОВЛЕНИЕ: Этот ответ представляет собой скрипт Python, который запрашивает изменения, что полезно в некоторых случаях. Для сценария Bash только для Linux, который использует inotify, см. Мой другой ответ, поищите на этой странице "rerun2".

Установите для Python2 или Python3 с:

pip install --user rerun

и использование очень просто:

rerun "COMMAND"

Команда ожидается как один аргумент, а не как последовательность аргументов, разделенных пробелом. Следовательно, процитируйте это, как показано, что уменьшает любые дополнительные побеги, которые вам придется добавить. Просто введите команду так, как вы бы ввели ее в командной строке, но в кавычках.

По умолчанию он просматривает все файлы в текущем каталоге или под ним, пропуская такие вещи, как известные каталоги управления исходным кодом,.git,.svn и т. Д.

Необязательные флаги включают '-i NAME', который игнорирует изменения именованных файлов или каталогов. Это может быть дано несколько раз.

Поскольку это скрипт Python, он должен запускать команду как подпроцесс, и мы используем новый экземпляр текущей оболочки пользователя для интерпретации "КОМАНДЫ" и решения, какой процесс на самом деле запустить. Однако, если ваша команда содержит псевдонимы оболочки и т.п., которые определены в.bashrc, они не будут загружены подоболочкой. Чтобы это исправить, вы можете повторно установить флаг '-I', чтобы использовать интерактивные (иначе называемые 'логин') субоболочки. Это медленнее и более подвержено ошибкам, чем запуск обычной оболочки, потому что она должна быть источником вашего.bashrc.

Я использую его с Python 3, но последний раз, когда я проверял, повторный запуск все еще работал с Python 2.

Обоюдоострым мечом является то, что он использует опрос вместо inotify. С другой стороны, это означает, что он работает на любой ОС. Кроме того, это лучше, чем некоторые другие решения, показанные здесь, с точки зрения запуска данной команды только один раз для нескольких изменений файловой системы, а не один раз для каждого измененного файла, и в то же время она запускает команду второй раз, если какие-либо файлы снова изменяются пока команда работает.

С другой стороны, опрос означает, что задержка составляет от 0,0 до 1,0 секунды, и, конечно, медленнее отслеживать очень большие каталоги. Сказав это, я никогда не сталкивался с проектом, достаточно большим, чтобы это было даже заметно, если вы используете '-i', чтобы игнорировать такие важные вещи, как ваши virtualenv и node_modules.

Хммм. rerun был незаменим для меня в течение многих лет - я в основном использую его по восемь часов каждый день для запуска тестов, восстановления точечных файлов при их редактировании и т. д. Но теперь я пришел, чтобы напечатать это здесь, ясно, что мне нужно перейти к решению который использует inotify (я больше не использую Windows или OSX.) и написан на Bash (поэтому он работает с псевдонимами без каких-либо дополнительных действий).

Как и некоторые другие, я также написал для этого облегченный инструмент командной строки. Он полностью документирован, протестирован и модульный.

Часы-Do

Монтаж

Вы можете установить его (если у вас есть Python3 и pip), используя:

pip3 install git+https://github.com/vimist/watch-do

использование

Используйте это сразу, запустив:

watch-do -w my_file -d 'echo %f changed'

Обзор возможностей

  • Поддерживает глобализацию файлов (используйте -w '*.py' или же -w '**/*.py')
  • Запустите несколько команд для изменения файла (просто укажите -d флаг снова)
  • Динамически поддерживает список файлов для просмотра, если используется глобализация (-r включить это)
  • Несколько способов "посмотреть" файл:
    • Время модификации (по умолчанию)
    • Файловый хеш
    • Тривиально реализовать свой собственный (это наблюдатель ModificationTime)
  • Модульная конструкция. Если вы хотите, чтобы команды выполнялись, когда к файлу обращаются, написать свой собственный наблюдатель (механизм, который определяет, должны ли исполнители выполняться), тривиально.

Для тех, кто использует OS X, вы можете использовать LaunchAgent, чтобы отслеживать путь / файл на предмет изменений и что-то делать, когда это происходит. К вашему сведению - LaunchControl - хорошее приложение для простого создания / изменения / удаления демонов / агентов.

( пример взят здесь)

<?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>

У меня есть GIST для этого, и использование довольно просто

watchfiles <cmd> <paths...>

https://gist.github.com/thiagoh/5d8f53bfb64985b94e5bc8b3844dba55

Другие вопросы по тегам