Search code examples
bashshellfindwatch

Multiple 'find' for watchers at once - only executes first one


I am trying to run a script which will watch different folder and files and if they change, execute a command. One of the folder paths accepts a dynamic variable which will change based on what is entered when running the script in the CLI, and the other one won't change.

Depending on the file path however, a different action needs to be taken.

#!/usr/bin/env bash

THEME=$1

while true;
do
  echo "inside find"
    find code/themes/$THEME/src -name '*.html' -o -name '*.ts' -o -name '*.scss' | entr -d rsync -avh code/themes/$THEME/*  ./
    find code/base/src -name '*.html' -o -name '*.ts' -o -name '*.scss' | entr -d rsync -avh code/base/*  ./
done

What is happening when I run the script is that it only executes and places the watch on the first find (in the code pasted above's example, the dynamic path only).

Currently my problem is:

  • Only the FIRST find gets called and only a watch placed on the code/themes/$THEME/src paths
  • The second find never gets executed

How do I get both to run at once? Or, how can I write it all in one line?

Also, I have never written scripts or used bash before, so if you have any advice on refactoring or are able to help with my problem, please be gentle with technical explanations.

My main goal is:

  1. I want to set up a watch for any changes in path code/themes/$THEME/src
  2. i want to set up a watch for any changes in path code/base/src
  3. I want both watchers to be running at the same time

Solution

  • If your task is simply to make sure the downstream copies of the two (or 1 + number of themes) directories are always up to date, a cron job which syncs them every n minutes might be the simplest and most robust solution. cron has a per-minute granularity, though something like every five minutes may well be sufficient for your use case. rsync by design does not copy files which are already up to date, so it should be reasonably quick to execute in the no-op case.

    * * * * * bin/rsyncthemes
    

    (This is once per minute; on Linux you can say */5 in the first field to run every 5 minutes, but this is not portable to all cron dialects.) ... where rsyncthemes contains something like

    #!/bin/sh
    # cron jobs always start in your home directory,
    # but you want to put an absolute path here
    # to run this in the directory where you want rsync to copy these files
    cd somewhere
    
    for dir in code/themes/one \
        code/themes/other \
        code/base
    do
        rsync -avh "$dir"/*  ./
    done
    

    where obviously this needs to be marked executable (chmod a+x rsyncthemes) and saved in the directory we specify in the cron job (viz. bin in your home directory).

    If you are hellbent on using entr to avoid running rsync needlessly, though, I'm guessing you want something like

    for dir in code/themes/one \
        code/themes/other \
        code/base
    do
        while true; do
            find "$dir"/src -name '*.html' -o -name '*.ts' -o -name '*.scss' |
            entr -d rsync -avh "$dir"/*  ./
        done & # <-- run each loop as a background job
    done
    

    In some more detail, we are running three while true loops as background jobs, one for each of the directories enumerated in the for loop. They are running in parallel with your interactive shell, and will stop running when you log out (unless you separately set them up to stay running with something like nohup). The & operator is what runs something in the background, and putting it after the while loop's done statement makes it apply to the entire loop.

    I'm guessing you may need to use entr -n in order for the jobs to run in the background.

    The wildcard in the rsync command gets expanded by the shell, so you might need to refactor this to not use a wildcard, or to evaluate the wildcard only after find finishes, if you expect it to be possible for new files to appear in these directories occasionally. (Quoting the entire rsync command and passing it with entr -s should trivially accomplish this, if I'm reading the entr man page correctly.)