Как выполнить команду при каждом изменении файла?
Мне нужен быстрый и простой способ выполнения команды всякий раз, когда файл изменяется. Я хочу что-то очень простое, что-то я оставлю на терминале и закрываю его, когда я закончу работу с этим файлом.
В настоящее время я использую это:
while read; do ./myfile.py ; done
И тогда мне нужно зайти на этот терминал и нажать Enter , всякий раз, когда я сохраняю этот файл в своем редакторе. Я хочу что-то вроде этого:
while sleep_until_file_has_changed myfile.py ; do ./myfile.py ; done
Или любое другое решение, столь же простое.
BTW: Я использую Vim, и я знаю, что могу добавить автокоманду для запуска чего-то на BufWrite, но это не то решение, которое я хочу сейчас.
Обновление: Я хочу что-то простое, отбрасывание, если это возможно. Более того, я хочу, чтобы что-то запускалось в терминале, потому что я хочу видеть выход программы (я хочу видеть сообщения об ошибках).
Об ответах: Спасибо за все ваши ответы! Все они очень хороши, и каждый из них принимает совсем другой подход от других. Поскольку мне нужно принять только один, я принимаю тот, который я использовал (это было просто, быстро и легко запомнить), хотя я знаю, что это не самый элегантный.
30 ответов
Простой, используя 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, используя аналог каждого варианта Linux inotify).
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
Решение с использованием Vim:
:au BufWritePost myfile.py :silent !./myfile.py
Но я не хочу этого решения, потому что это неприятно печатать, немного сложно запомнить, что вводить, точно, и немного сложно отменить его эффекты (нужно запустить :au! BufWritePost myfile.py
). Кроме того, это решение блокирует Vim, пока команда не завершит выполнение.
Я добавил это решение здесь только для полноты, так как это могло бы помочь другим людям.
Чтобы отобразить вывод программы (и полностью нарушить поток редактирования, так как вывод будет записываться через ваш редактор в течение нескольких секунд, пока вы не нажмете Enter), удалите команду :silent
.
Если у вас установлен npm
, nodemon
, вероятно, самый простой способ начать работу, особенно на OS X, который, по-видимому, не имеет инструментов inotify. Он поддерживает запуск команды при изменении папки.
Вот простой сценарий оболочки Bourne оболочки, который:
- Принимает два аргумента: файл для мониторинга и команду (с аргументами, если необходимо)
- Копирует файл, который вы контролируете, в каталог /tmp
- Проверяет каждые две секунды, чтобы увидеть, является ли файл, который вы контролируете, новее, чем копия
- Если он новее, он перезаписывает копию с новым оригиналом и выполняет команду
-
Очищает после себя, когда вы нажимаете 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), но в этом случае вы можете просто записать код временного файла.
rerun2
( на github ) - это 10-строчный скрипт Bash вида:
#!/usr/bin/env bash
function execute() {
clear
echo "[email protected]"
eval "[email protected]"
}
execute "[email protected]"
inotifywait --quiet --recursive --monitor --event modify --format "%w%f" . \
| while read change; do
execute "[email protected]"
done
Сохраните версию github как «повтор» на вашей PATH и вызовите ее, используя:
rerun COMMAND
Он запускает COMMAND каждый раз, когда в вашем текущем каталоге происходит изменение файла с файловой системой (рекурсивный.)
Вещи, которые могут понравиться:
- Он использует inotify, поэтому он более отзывчив, чем опрос. Потрясающе для запуска субмиллисекундных модульных тестов или рендеринга графических точечных файлов каждый раз, когда вы нажимаете «save».
- Поскольку это так быстро, вам не нужно беспокоиться о том, чтобы он игнорировал большие поддиры (например, 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 -предел-достигла
- Он не может выполнять команды, содержащие псевдонимы Bash. Я мог бы поклясться, что это сработало. В принципе, поскольку это Bash, не выполняющий COMMAND в подоболочке, я бы ожидал, что это сработает. Я бы хотел услышать, если кто-нибудь знает, почему это не так. Многие другие решения на этой странице также не могут выполнять такие команды.
- Лично мне жаль, что я не смог нажать ключ в терминале, в котором он работает, чтобы вручную вызвать дополнительное выполнение COMMAND. Могу я добавить это как-то просто? Одновременно выполняется цикл while while -n1, который также вызывает выполнение?
- Сейчас я закодировал его, чтобы очистить терминал и распечатать выполненную команду на каждой итерации. Некоторым людям может понравиться добавлять флаги командной строки, чтобы отключить такие вещи и т. Д. Но это увеличит размер и сложность многократно.
Это уточнение анкеты @ cychoi.
Посмотрите incron . Он похож на cron, но использует inotify события вместо времени.
Для тех, кто не может установить inotify-tools
, как я, это должно быть полезно:
watch -d -t -g ls -lR
Эта команда выйдет, когда выход изменится, ls -lR
перечислит каждый файл и каталог с его размерами и датами, поэтому, если файл будет изменен, он должен выйти из команды, поскольку человек говорит:
-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}
Другое решение с NodeJs, fsmonitor :
-
Установить
sudo npm install -g fsmonitor
-
Из командной строки (например, журналы мониторинга и «розничная торговля» при изменении одного файла журнала)
fsmonitor -s -p '+*.log' sh -c "clear; tail -q *.log"
В Linux:
man watch
watch -n 2 your_command_to_run
Запустит команду каждые 2 секунды.
Если ваша команда занимает более 2 секунд для запуска, часы будут ждать, пока она не будет выполнена, прежде чем делать это снова.
Посмотрите на Guard, в частности с этим плагином:
https://github.com/hawx/guard-shell
Вы можете настроить его для просмотра любого количества шаблонов в каталоге вашего проекта и выполнения команд при внесении изменений. Хороший шанс, даже если есть плагин для этого, что вы пытаетесь сделать в первую очередь.
, если у вас установлен nodemon , вы можете сделать это:
nodemon -w <watch directory> -x "<shell command>" -e ".html"
В моем случае я редактирую html локально и отправляю его на мой удаленный сервер, когда файл изменяется.
nodemon -w <watch directory> -x "scp filename [email protected]:/var/www" -e ".html"
Если ваша программа генерирует какой-то журнал /вывод, вы можете создать Makefile с правилом для этого журнала /вывода, который зависит от вашего скрипта и сделать что-то вроде
while true; do make -s my_target; sleep 1; done
В качестве альтернативы вы можете создать фальшивую цель и иметь правило, чтобы он вызывал ваш скрипт и касался фальшивой цели (хотя все еще зависит от вашего скрипта).
swarminglogic написал скрипт под названием watchfile.sh , также доступен как GitHub Gist .
Улучшено после ответа Гилла .
Эта версия запускает inotifywait
один раз и отслеживает события (.e.g .: 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
Немного больше на стороне программирования, но вы хотите что-то вроде inotify . Существуют версии на многих языках, такие как jnotify и pyinotify .
Эта библиотека позволяет отслеживать отдельные файлы или целые каталоги и возвращает события при обнаружении действия. Возвращенная информация включает в себя имя файла, действие (создание, изменение, переименование, удаление) и путь к файлу, среди прочих полезных сведений.
Для тех из вас, кто ищет решение FreeBSD, здесь находится порт:
/usr/ports/sysutils/wait_on
Мне нравится простота while inotifywait ...; do ...; done
, однако у него есть две проблемы:
- Изменения файла, происходящие во время
do ...;
, будут пропущены - Slow при использовании в рекурсивном режиме
Для этого я создал вспомогательный скрипт, который использует inotifywait без этих ограничений: inotifyexec
Я предлагаю вам поместить этот скрипт в свой путь, например, в ~/bin/
. Использование описано только при запуске команды.
Пример: inotifyexec "echo test" -r .
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
EDIT: только что найденный сторожевой таймер имеет официальный CLI под названием watchmedo
, поэтому также проверьте это.
Улучшено решение Себастьяна с помощью команды 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
.
Ответ 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
Я использую этот скрипт для этого. Я использую 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 при каждой записи.
Вы можете попробовать reflex .
Reflex - небольшой инструмент для просмотра каталога и повторной команды при изменении определенных файлов. Это отлично подходит для автоматического запуска задач компиляции /lint /test и для перезагрузки приложения при изменении кода.
# Rerun make whenever a .c file changes
reflex -r '\.c$' make
Для людей, которые находят это в Googling для изменений в конкретном файле, ответ намного проще (вдохновлен
Для тех, кто использует OS X, вы можете использовать LaunchAgent для просмотра пути /файла для изменений и делать что-то, когда это произойдет. FYI - 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>
Инструмент «fido» может быть еще одним вариантом для этой потребности. См. https://www.joedog.org/fido-home/
У меня есть GIST для этого, и использование довольно просто
watchfiles <cmd> <paths...>
https://gist.github.com/thiagoh/5d8f53bfb64985b94e5bc8b3844dba55
Как и некоторые другие, я также написал легкий инструмент командной строки для этого. Он полностью документирован, протестирован и модулен.
Watch-Do
Установка
Вы можете установить его (если у вас есть Python3 и pip), используя:
pip3 install git+https://github.com/vimist/watch-do
Использование
Используйте его сразу, выполнив:
watch-do -w my_file -d 'echo %f changed'
Обзор функций
- Поддерживает файловое globbing (используйте
-w '*.py'
или-w '**/*.py'
) - Запустите несколько команд при изменении файла (просто укажите флаг
-d
) - Динамически поддерживает список файлов для просмотра, если используется глобулизация (
-r
, чтобы включить это) - Несколько способов «смотреть» файл:
- Время модификации (по умолчанию)
- Хэш файла
- Тривиально реализовать свой собственный (это МодификацияTime )
- Модульная конструкция. Если вы хотите, чтобы команды выполнялись, когда к файлу обращается доступ, тривиально писать собственный наблюдатель (механизм, определяющий, должны ли выполняться исполнители).