home ~ projects ~ socials

Automatically run scripts from inside their directory with watchexec

Run Where I Made Ya

I use a bunch of Python scripts to generate Neopolitan parser tests1. Each script gets its own directory. Jumping around my text editor to change them is easy. Plodding through directories on the command line to run them is a pain.

I cooked up this little script to take care of that with watchexec2:

#!/bin/bash

watchexec \
  --project-origin .\
  -c -r -p -e py\
  --shell=bash -- '\
  FILE_PATH="$WATCHEXEC_COMMON_PATH/$WATCHEXEC_WRITTEN_PATH" \
  && PARENT_DIR="$(dirname "$FILE_PATH")" \
  && cd $PARENT_DIR \
  && python3 "$FILE_PATH"'

The script sits at the top of Neopolitan's source tree. It kicks off a watchexec process that keeps an eye on Python files. When one changes watchexec runs it.

The key feature is that watchexec does a cd into the script's directory before running it. That means I can use relative file paths and they'll Just Work™.

What are all those flags?

You can run watchexec from the command line. I like putting it in its own script. That way, I don't have to remember all these incantations:

  • watchexec

    The command itself.

  • \

    The backslash is used to break the overall watchexec command onto multiple lines. It gets used repeatedly. Things are a lot easier to work with that way than if everything was a single line the length of a novel.

  • --project-origin .

    watchexec tries to set what it thinks is the root of a project. It causes issues when it gets things wrong. Using the --project-origin . sets it to the current directory explicitly.

    I don't have a clear enough mental model of watchexec to know when it's necessary and when it's not. I always throw it in so I don't have to think about it.

  • -c

    Clears the screen before every run of the command.

    A nice quality of life improvement knowing that you can scroll up and you won't end up looking at the output from a previous run.

  • -r

    Restarts the target command if it's still running.

    Neopolitan's scripts complete so fast this doesn't matter here. It's handy for things like Neopoligen3 that have longer build times.

  • -p

    Postpone running the target command until the first change is detected.

    Without this, watchexec fires off the target command as soon as it starts. That's fine/desirable in some cases. For this script, that would mean trying to use environmental variables before they are set.

  • -e py

    Tells watchexec what file extensions to watch for changes. In this case that's py for my Python files.

    You can add multiple extensions by separating them with a comma. For example, this would do .py and .rs files: -e py,rs

  • --shell=bash

    Tells watchexec to to use bash to run the target command.

  • --

    the separator used to package up the command to run as a single argument surrounded by ' characters.

  • '

    The opening single quote. Everything between it and the ending ' constitutes the command bash runs when files change.

  • FILE_PATH="$WATCHEXEC_COMMON_PATH/$WATCHEXEC_WRITTEN_PATH"

    This assemble the full path to the target Python script by combining two environmental variables that watchexec sets:

    $WATCHEXEC_COMMON_PATH

    and

    $WATCHEXEC_WRITTEN_PATH"

    It's worth pointing out that there are other environmental variables for file creation, renaming, etc. It would be a little safer to explicitly check to see if $WATCHEXEC_WRITTEN_PATH" is set. I'll add that if it ever becomes a problem.

  • &&

    The && gets used a few times. It makes sure that each part of the command chain only runs if the part before it succeeded.

  • PARENT_DIR="$(dirname "$FILE_PATH")"

    Grabs the parent directory of the target script.

    The double quotes nested inside another pair of double quotes always looks weird to me. That's just how bash does it. Without it, you run the risk of file paths wish spaces breaking things in unpredictable ways.

  • cd $PARENT_DIR

    Change into the parent directory we just grabbed.

  • python3 "$FILE_PATH"

    Call python3 with the path to our script stored in $FILE_PATH. This is what actually runs the script.

  • '

    Finally, close the single quote string to finish packaging everything up for watchexec.

Off And Running

It took half an hour to come up with this script. That's after hours spent figuring out different way to work with watchexec over the years.

I'm happy to trade that time. It allows me to stay in the flow. Plus, I never have to spend that half hour again. I can just grab the script from here whenever I need it. If you end up using it, it'll be even more worth the time spent.

-a

-- end of line --

Footnotes

A plain-text file format that's like Markdown on steroids

"a simple, standalone tool that watches a path and runs a command whenever it detects modifications."

Way more complicated than it sounds. Awesome when you learn how to turn it on properly.

The site builder I built that works with Neopolitan files. At the time of this writing it's got a bunch of stuff hard coded to my site. I'm working to remove that so other folks can play with it too.