🔁 entr

A timesaver: save a file and see the test result, the restarted server or the rebuilt output at once, with every option run for real on a small sample project.

Video: entr (Timesavers video)
Prefer to watch? This short video explains what entr is for, then runs it on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
entr: run a command every time one of your files changes. You give it the file names, it does the watching.
ls src/*.py | entr -c python3 test.py     rerun the test whenever a source file changes
ls src/*.py | entr -r python3 serve.py    restart a program after every change
ls docs/*.md | entr -s 'wc -w docs/*.md'  run a shell line, with globs and pipes
q is not needed: Ctrl-C stops it

entr is a very small program. It reads a list of file names from standard input, waits, and runs the command you give it as soon as one of those files changes. There is nothing to configure and no file to write. Because the list comes from a pipe, anything that prints file names can feed it: ls, find, git ls-files. It is the quickest way to get a "watch mode" for a tool that has none.

This page is the written version of the Timesavers video. Every option below was run for real, in an Ubuntu 24.04 container with entr 5.5, on a small made-up Python project (a calculator with a test, a notes file, a config file and a toy server). In the lab, a second terminal made the changes, as an editor would. The screens shown are the real screens from that run. The section "What was not run" lists what was not tried.

What entr is for

Use it when you change a file and then want something to happen, over and over:

run the tests on every save        ls src/*.py test_calc.py | entr -c python3 test_calc.py
restart a program you are editing  ls src/*.py | entr -r python3 serve.py
rebuild or reformat                ls docs/*.md | entr -s 'make docs'  (the make line was not run)
copy or sync on change             ls app.conf | entr -p echo changed: /_
use it in a script                 entr -n and -z, with no terminal needed

It watches the files you name, nothing else. It does not watch a folder for new files unless you ask (the -d option, below), it has no configuration file, and it is not a build system. It is a small program, and it is in the Ubuntu archive.

Install

entr is in the Ubuntu package archive:

$ sudo apt install entr
$ apt-cache policy entr | head -3
entr:
  Installed: 5.5-1
  Candidate: 5.5-1
$ entr
release: 5.5
usage: entr [-acdnprsz] utility [argument [/_] ...] < filenames

(The package version is 5.5-1, which is the one used in the lab. entr was already installed in the lab, so the apt install line is the one used to put it there.) Run without arguments, entr prints the release and its usage line: entr [-acdnprsz] utility [argument [/_] ...] < filenames. That is the whole interface: some flags, the command (the "utility") and its arguments, and the file names on standard input. The lab container has no manual page installed, so man entr was not read and the descriptions below come from running each option.

Run your tests on every save

The pattern is always the same: print the file names, pipe them to entr, and give it the command. Here the files are the source and the test, and the command runs the test. -c clears the screen before each run, so only the latest result is on screen:

$ ls src/*.py test_calc.py | entr -c python3 test_calc.py
all tests passed

It ran the test once at the start. (Without -p, entr always runs the command once when it starts.) Then, in a second terminal, a change that breaks add (a - b instead of a + b):

$ sed -i 's/a + b/a - b/' src/calc.py
Traceback (most recent call last):
  File "/home/ubuntu/proj/test_calc.py", line 5, in <module>
    assert add(2, 3) == 5, "add(2, 3) should be 5"
           ^^^^^^^^^^^^^^
AssertionError: add(2, 3) should be 5

The moment the file was saved, the watcher cleared the screen and ran the test again. The fix goes the same way:

$ sed -i 's/a - b/a + b/' src/calc.py
all tests passed

Without -c the new result is added under the old ones instead of replacing them, so the screen fills up with history:

$ ls src/*.py test_calc.py | entr python3 test_calc.py
all tests passed
all tests passed

(Two runs are shown: the first at the start and the second after a touch of src/calc.py.) Ctrl-C stops entr. Pressed in the lab, it printed ^C and the shell reported exit status 130, which is the usual status for a program stopped by Ctrl-C:

ubuntu@timesaver:~/proj$ ^C
ubuntu@timesaver:~/proj$ echo "exit status: $?"
exit status: 130
ubuntu@timesaver:~/proj$

Restart a program: -r

A test runs and ends. A server keeps running, so it has to be stopped before it is started again. -r does both: when a file changes entr stops the running program and starts it again. The toy server here only prints its process number and waits:

$ ls src/*.py | entr -r python3 serve.py
ubuntu@timesaver:~/proj$ ls src/*.py | entr -r python3 serve.py
server started, pid 4349
server started, pid 4363
server started, pid 4377

The first line is the start. The two touch commands in the second terminal gave two more, each with a new pid, so each time it was a fresh process. Only the pid lines are from the program; with a real server you would see its own start-up lines in the same way.

Pipes and globs: -s

Without -s, entr does not run the command through a shell, so a glob in it is not expanded. In the lab, wc -w 'docs/*.md' (the glob quoted) said wc: 'docs/*.md': No such file or directory, and the exit status was 1. -s hands the whole text to a shell instead. Here the shell counts the words of every note each time one changes:

$ ls docs/*.md | entr -s 'wc -w docs/*.md'
8 docs/notes.md
bash returned exit code 0
13 docs/notes.md
bash returned exit code 0

The second terminal ran echo 'Check the edge cases too.' >> docs/notes.md, and the count went from 8 to 13. With -s entr also prints a line saying which shell ran and the exit status (bash returned exit code 0). A run started from sh -c printed the same line with sh in it:

$ ls src/*.py | entr -nps 'echo changed; echo hello | wc -c'
changed
6
sh returned exit code 0

The text after -s is one argument, in quotes, as in the examples.

Which file changed: /_ and -p

/_ in the command is replaced by the full path of the first file that changed. -p postpones the first run until a file changes, which is what you want when the command does something and you do not want it to happen just because entr started:

$ ls app.conf src/*.py | entr -p echo changed: /_
changed: /home/ubuntu/proj/app.conf
changed: /home/ubuntu/proj/src/calc.py

Nothing printed when it started. The two changes in the second terminal (an added line in app.conf, then a touch of src/calc.py) each printed the path. The list can come from anything that prints names. With find, one list covered two kinds of files:

$ find . -name '*.py' -o -name '*.md' | entr -np echo changed: /_
changed: /home/ubuntu/proj/docs/notes.md

(Here a touch of docs/notes.md was the change.) The -n in that line is explained in the section on scripts.

New files, replaced files and deleted files

entr watches the files it was given at the start. It does not know about a file that is created later. -d (directory) changes that in a blunt way: entr watches the folders of those files and exits when a new file appears in them, with exit status 2:

$ ls src/*.py | entr -nd echo ran
ran
ran
entr: directory altered
exit status: 2

The first ran is the start. The second is the run for the new file, and then entr printed entr: directory altered and stopped (status 2). Wrap it in a loop and the list is read again each time, so the new file is in it:

$ while sleep 1; do ls src/*.py | entr -dp echo changed; done
changed
changed
entr: directory altered
changed

In the lab: a touch of src/calc.py printed changed; a touch of a new file src/stats.py printed changed and then entr: directory altered, the loop started entr again with the new list, and the next touch of src/stats.py printed changed. Stop a loop like this with two quick Ctrl-C presses, one for entr and one for the loop. In the lab, two presses half a second apart ended it, as shown below. A single press only stopped entr, and the loop started it again a second later, so the prompt did not come back:

^C
ubuntu@timesaver:~/proj$

Editors that save by writing a new file and renaming it over the old one are fine. In the lab a file was replaced with cp and mv, and then touched, and entr ran the command for the start and for each of the two changes:

$ ls src/*.py | entr -n echo ran
ran
ran
ran

A deleted file is different. When src/gone.py, which was on the list, was removed, entr ran the command and then stopped with an error and exit status 1:

$ ls src/*.py | entr -n echo ran
ran
entr: cannot open 'src/gone.py': No such file or directory
exit status: 1

In scripts: -n and -z

entr normally needs a terminal. When there is none, for example in a script, a cron job or a pipe, it says so:

$ ls src/*.py | entr echo hi
entr: unable to get terminal attributes, use '-n' to run non-interactively
exit status: 1

(This was run in a shell with no terminal.) -n is the non-interactive mode, and every other example of this kind in the lab used it. -z runs the command once, when a file changes, and then entr exits, handing back the exit status of the command. That makes entr a way to wait for a change:

$ ls src/*.py | entr -nz echo ran-once
ran-once
exit status: 0
$ ls src/*.py | entr -nz sh -c 'exit 3'
exit status: 3

(In both runs the file was touched from another process. In the first the exit status is the 0 of echo, in the second it is the 3 of the command.) Without -z, entr keeps running. The same lines under timeout showed that entr -n with echo ran at the start and after the touch and kept waiting (exit status 124 is what timeout reports):

$ timeout 4 sh -c 'ls src/*.py | entr -n echo ran'
ran
ran
exit status: 124 (124 = stopped by timeout)

When it will not start

Three mistakes the lab ran into, with the real messages. No command:

$ ls src/*.py | entr
release: 5.5
usage: entr [-acdnprsz] utility [argument [/_] ...] < filenames
exit status: 1

A file name that does not exist:

$ echo nothing.xyz | entr echo hi
entr: unable to stat 'nothing.xyz'
entr: No regular files to watch
exit status: 1

No names at all:

$ entr echo hi < /dev/null
entr: No regular files to watch
exit status: 1

All three end with exit status 1. The missing-file case is the usual one: a typo in a name, or a glob that matched nothing.

The options at a glance

entr [-acdnprsz] utility [argument [/_] ...] < filenames

-c   clear the screen before each run (run)
-r   stop the program and start it again after every change (run)
-s   run the command in the shell, so globs and pipes work (run)
-p   wait for a change before the first run (run)
-d   exit with status 2 when a new file appears in the watched folders (run)
-z   run once after the change, then exit with the command's status (run)
-n   non-interactive: no terminal needed (run)
-a   accepted by this version (run, effect not tried)
/_   in the command, the first file that changed (run)

Options can be combined in one group, as in -nps and -ndp above. The word "run" means the option was run in the lab. The only one whose effect was not looked at is -a.

What was not run in the lab

The manual page was not read, because the container has none; the options were learned by running them. The effect of -a was not tried, and neither were any keys that entr reads from the keyboard. -r was not compared with a run without it for a command that takes a long time, and combining -c with -r was not tried. Files in folders that are deleted, symbolic links, very long lists of files and the limits on how many files entr can watch were not tried. The make line in the first table is an example, and was not run. Only Linux was used: entr also runs on BSD and macOS, with different file watching under the hood. Everything ran in an Ubuntu 24.04 container with entr 5.5, with a second tmux pane standing in for an editor.

Common problems and tips

entr: No regular files to watch. The list was empty, or a name did not exist, or it was a folder. entr watches files, so list files (ls src/*.py, not ls src).

entr: unable to get terminal attributes, use '-n'. entr is running without a terminal. Add -n.

It stopped by itself with "directory altered". You gave -d and a new file appeared. Run it in a loop, as shown above.

It stopped when I deleted a file. A removed file that is on the list ends entr with an error. Run it in a loop, so the list is read again.

My pipe or glob does not work in the command. Use -s, and put the command in quotes.

The command runs once when I start it. That is on purpose. Add -p to wait for the first change.

Tips. Use -c for tests and -r for servers. Put the line in your shell history or a small script, because you will type it every day. When your editor saves in an odd way, try it with echo first: the lab's replace-by-rename test worked. And keep the watched list short: only the files that matter.