題 如何在文件發生變化時執行命令?


我希望在文件發生變化時快速簡單地執行命令。我想要一些非常簡單的東西,我將在終端上運行並在我完成該文件時關閉它。

目前,我正在使用這個:

while read; do ./myfile.py ; done

然後我需要去那個終端然後按 輸入每當我在編輯器上保存該文件時。我想要的是這樣的:

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

或者任何其他解決方案都很簡單。

BTW:我正在使用Vim,我知道我可以添加一個自動命令來在BufWrite上運行一些東西,但這不是我現在想要的那種解決方案。

更新: 如果可能的話,我想要一些簡單,可丟棄的東西。更重要的是,我希望在終端中運行某些東西,因為我想看到程序輸出(我希望看到錯誤消息)。

關於答案: 謝謝你的所有答案!所有這些都非常好,每個人採取與其他人截然不同的方法。因為我只需要接受一個,所以我接受了我實際使用的那個(它簡單,快速且容易記住),儘管我知道它不是最優雅的。


375
2017-08-27 20:02


起源


可能的跨站點副本: stackoverflow.com/questions/2972765/... (雖然這裡是主題=)) - Ciro Santilli 新疆改造中心 六四事件 法轮功
我在跨站點重複之前引用它並被拒絕:S;) - Francisco Tapia
Jonathan Hartley的解決方案建立在此處的其他解決方案上,並解決了最受歡迎的答案所帶來的重大問題:缺少一些修改並且效率低下。請將接受的答案更改為他的,也將在github上維護 github.com/tartley/rerun2 (或沒有這些缺陷的其他解決方案) - nealmcb


答案:


簡單,使用 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 使用不同的文件,而不是寫入現有文件 myfileinotifywait 會死。許多編輯都是這樣工作的。

要克服此限制,請使用 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模擬)。


357
2017-08-27 20:54



我用一個簡單易用的方法封裝了所有這些(有很多bash技巧) sleep_until_modified.sh 腳本,可在: bitbucket.org/denilsonsa/small_scripts/src - Denilson Sá Maia
while sleep_until_modified.sh derivation.tex ; do latexmk -pdf derivation.tex ; done 太棒了謝謝。 - Rhys Ulerich
inotifywait -e delete_self 似乎對我有用。 - Kos
它很簡單,但有兩個重要問題:事件可能會丟失(循環中的所有事件),每次都會完成inotifywait的初始化,這使得大型遞歸文件夾的解決方案變得更慢。 - Wernight
由於某些原因 while inotifywait -e close_write myfile.py; do ./myfile.py; done 總是退出而不運行命令(bash和zsh)。為此,我需要添加 || true,例如: while inotifywait -e close_write myfile.py || true; do ./myfile.py; done - ideasman42


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);無論如何,從作者的回購中建立起來都是無痛苦的。


127
2017-10-25 09:41



不處理新文件及其修改。 - Wernight
@Wernight - 截至2014年5月7日,entr有新的 -d 旗;它稍微囉嗦,但你可以做到 while sleep 1 ; do find . -name '*.py' | entr -d ./myfile.py ; done 處理新文件。 - Paul Fenney
entr也可以在debian repos中獲得至少來自debian jessie / 8.2 on ... - Peter V. Mørch
我在OS X上找到的最好的一個肯定。 fswatch抓住了太多時髦的事件,我不想花時間弄清楚原因 - dtc
值得一提的是 ENTR 可以在Homebrew上找到,所以 brew install entr 將按預期工作 - jmarceli


我編寫了一個Python程序來完成這個調用 當改變的

用法很簡單:

when-changed FILE COMMAND...

或者觀看多個文件:

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

FILE 可以是一個目錄。用遞歸方式觀看 -r。使用 %f 將文件名傳遞給命令。


102
2018-06-30 13:34



@ysangkok是的,在最新版本的代碼:) - joh
現在可以從“pip install when-changed”獲得。仍然很好。謝謝。 - A. L. Flanagan
要先清除屏幕,您可以使用 when-changed FILE 'clear; COMMAND'。 - Dave James Miller
這個答案要好得多,因為我也可以在Windows上做到這一點。而這個人實際上寫了一個程序來得到答案。 - Wolfpack'08
大家好消息! when-changed 現在是跨平台的!看看最新的 0.3.0發布 :) - joh


這個劇本怎麼樣?它使用了 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

46
2017-08-20 17:12



豈不 stat - 每當文件發生變化時,修改時間會更好嗎?答案? - Xen2050
每秒運行多次運行會導致多次讀取磁盤?或者fstat系統調用會以某種方式自動緩存這些響應嗎?每當我進行更改時,我都會嘗試編寫一種“grunt watch”來編譯我的c代碼 - Oskenso Kashi
如果你知道要提前觀看的文件名,這很好。更好的方法是將文件名傳遞給腳本。更好的是,如果你可以傳遞許多文件名(例如“mywatch * .py”)。更好的是,如果它可以遞歸地對子目錄中的文件進行操作,這是其他一些解決方案所做的。 - Jonathan Hartley
萬一有人想知道重讀,我在Ubuntu 17.04中測試了這個腳本,睡眠時間為0.05s, vmstat -d 監視磁盤訪問。似乎linux在緩存這類事情方面做得非常出色:D - Oskenso Kashi
“COMMAND”中有拼寫錯誤,我試圖解決,但是S.O.說“編輯不應少於6個字符” - user337085


使用Vim的解決方案:

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

但是我不想要這個解決方案,因為打字有點煩人,要記住要輸入的內容有點難以準確,而且撤消其效果有點困難(需要運行) :au! BufWritePost myfile.py)。此外,此解決方案會阻止Vim,直到命令執行完畢。

我在這裡添加此解決方案只是為了完整性,因為它可能會幫助其他人。

要顯示程序輸出(並完全中斷編輯流程,因為輸出將在編輯器上寫入幾秒鐘,直到您按Enter鍵),刪除 :silent 命令。


28
2017-08-27 20:12



結合使用時,這可能非常好 entr (見下文) - 只需讓vim觸摸一個正在觀看的虛擬文件,然後讓其他人在後台進行操作......或者 tmux send-keys 如果你碰巧在這樣的環境中:) - Paul Fenney
太好了!你可以為你做一個宏 .vimrc 文件 - ErichBSchulz


如果你碰巧有 npm 安裝, nodemon 可能是最簡單的入門方式,特別是在OS X上,顯然沒有inotify工具。它支持在文件夾更改時運行命令。


24
2018-06-09 23:51



但是,它只能監視.js和.coffee文件。 - zelk
當前版本似乎支持任何命令,例如: nodemon -x "bundle exec rspec" spec/models/model_spec.rb -w app/models -w spec/models - kek
我希望我有更多的信息,但osx確實有一種方法來跟踪變化,fsevents - ConstantineK
在OS X上,您也可以使用 啟動守護進程 用一個 WatchPaths 密鑰如我的鏈接所示。 - Adam Johns


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版本保存為PATH上的“重新運行”,並使用以下命令調用它:

rerun COMMAND

每次在當前目錄中有文件系統修改事件時,它都會運行COMMAND(遞歸)。

人們可能喜歡的事情:

  • 它使用inotify,因此比輪詢更敏感。每次點擊“保存”時,非常適合運行亞毫秒單位測試或渲染graphviz點文件。
  • 因為它如此之快,所以出於性能原因,您不必費心地告訴它忽略大型子目錄(如node_modules)。
  • 它具有額外的超級響應能力,因為它只會在啟動時調用inotifywait一次,而不是運行它,並且在每次迭代時都會產生昂貴的建立手錶。
  • 它只有12行Bash
  • 因為它是Bash,它會解釋您傳遞的命令,就像您在Bash提示符下鍵入它們一樣。 (如果你使用另一個外殼,大概這不太酷。)
  • 它不會丟失COMMAND執行時發生的事件,這與此頁面上的大多數其他inotify解決方案不同。
  • 在第一個事件中,它進入“死區”0.15秒,在此期間忽略其他事件,在COMMAND運行一次之前。這是因為Vi或Emacs在保存緩衝區時所做的創建 - 寫 - 移動舞蹈引起的一系列事件不會導致可能運行緩慢的測試套件的多次繁重執行。執行COMMAND時發生的任何事件都不會被忽略 - 它們將導致第二個死區並隨後執行。

人們可能不喜歡的事情:

  • 它使用inotify,因此不會在Linuxland之外工作。
  • 因為它使用inotify,它會嘗試查看包含比用戶inotify監視的最大數量更多的文件的目錄。默認情況下,這似乎在我使用的不同機器上設置為大約5,000到8,000,但很容易增加。看到 https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached
  • 它無法執行包含Bash別名的命令。我可以發誓這曾經工作過。原則上,因為這是Bash,而不是在子shell中執行COMMAND,我希望這可以工作。我很想听聽如果有人知道為什麼不這樣做。此頁面上的許多其他解決方案也無法執行此類命令。
  • 就個人而言,我希望我能夠在其運行的終端中點擊一個鍵,以手動導致額外執行COMMAND。我可以用某種方式添加它嗎?同時運行'while read -n1'循環,它也調用execute?
  • 現在我已將其編碼為清除終端並在每次迭代時打印執行的COMMAND。有些人可能想添加命令行標誌來關閉這樣的東西,等等。但是這會增加​​大小和復雜性。

這是@ cychoi的anwer的改進。


13
2017-09-09 21:49



我相信你應該用 "$@" 代替 $@,以便正確使用包含空格的參數。但與此同時你使用 eval,這會強制重新運行的用戶在引用時要格外小心。 - Denilson Sá Maia
謝謝德尼爾森。您能舉一個例子,說明需要仔細進行報價嗎?我在過去24小時內一直在使用它,到目前為止還沒有看到任何空間問題 小心 引用任何東西 - 只是調用為 rerun 'command'。你只是說,如果我使用“$ @”,那麼用戶可以調用as rerun command (沒有引號?)這對我來說似乎沒有用處:我一般不希望Bash這樣做 任何 在將命令傳遞給重新運行之前處理命令。例如如果命令包含“echo $ myvar”,那麼我想在每次迭代中看到myvar的新值。 - Jonathan Hartley
就像是 rerun foo "Some File" 可能會破裂。但是因為你正在使用 eval,它可以改寫為 rerun 'foo "Some File"。請注意,有時路徑擴展可能會引入空格: rerun touch *.foo 可能會破裂,並使用 rerun 'touch *.foo' 語義略有不同(路徑擴展只發生一次或多次)。 - Denilson Sá Maia
謝謝您的幫助。是的: rerun ls "some file" 由於空間而中斷。 rerun touch *.foo* 通常工作正常,但如果匹配* .foo的文件名包含空格則會失敗。謝謝你幫我看看如何 rerun 'touch *.foo' 有不同的語義,但我懷疑單引號的版本是我想要的語義:我希望每次重新運行的迭代都像我再次輸入命令一樣 - 因此我 想  *.foo 在每次迭代時進行擴展。我會嘗試你的建議來檢查它們的影響...... - Jonathan Hartley
關於這個PR的更多討論(github.com/tartley/rerun2/pull/1) 和別的。 - Jonathan Hartley


這是一個簡單的shell Bourne shell腳本:

  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)命令,但在這種情況下,您只需硬編碼臨時文件名即可。


12
2017-08-27 21:23



輪詢是唯一可移植的方式,但大多數係統都有文件更改通知機制(Linux上的inotify,FreeBSD上的kqueue,...)。當你這樣做時,你會遇到嚴重的引用問題 $cmd,但幸運的是,這很容易解決:放棄了 cmd 變量和執行 "$@"。您的腳本不適合監視大文件,但可以通過替換來修復 cp 通過 touch -r (你只需要日期,而不是內容)。便攜性方面, -nt 測試需要bash,ksh或zsh。 - Gilles