A better way to trigger a build on file change in Linux

Posted in category shell on 2017-05-22
Tags: shell, bash, linux

Earlier I went through a solution to this problem based on inotifywait utility which is fairly common approach (“Triggering a build on file change in Linux”).

Though after using this solution for some time I came to conclusion that there is an issue that is very difficult to overcome - terminal colors from an invoked script are lost and after the first build you see monochrome picture. I ended up thinking this is due to inotifywait or piping in bash.

So, here is another solution based on find, date, stat, sort and head utilities that come pre-installed on literally most if not all linux distributions since stone age.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env bash

command -v date >/dev/null 2>&1 || \
  { echo >&2 "[date] is required, but not installed.  Aborting."; exit 1; }
command -v find >/dev/null 2>&1 || \
  { echo >&2 "[find] is required, but not installed.  Aborting."; exit 1; }
command -v stat >/dev/null 2>&1 || \
  { echo >&2 "[stat] is required, but not installed.  Aborting."; exit 1; }
command -v sort >/dev/null 2>&1 || \
  { echo >&2 "[sort] is required, but not installed.  Aborting."; exit 1; }
command -v head >/dev/null 2>&1 || \
  { echo >&2 "[head] is required, but not installed.  Aborting."; exit 1; }

if [[ "$#" -ne 4 ]]; then
  echo "Expected arguments are missing: EXTENSIONS IGNORE_FOLDERS DELAY COMMAND"
  exit 1
fi

IFS=',' read -r -a exts <<< "$1"
IFS=',' read -r -a igns <<< "$2"
delay=$3
cmd=$4

YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

function run () {
  clear
  x=$(date -ud "$(date -u)" +'%s')
  echo -e "${YELLOW}>> Triggered on -> $(date) <<${NC}"
  echo ""
  bash -c "eval $cmd"
  echo ""
  echo -e "${YELLOW}>> Completed on -> $(date) <<${NC}"
  y=$(date -ud "$(date -u)" +'%s')
  s=$(($y-$x))
  m=$(($s/60))
  h=$(($s/60/60))
  printf "${CYAN}>>     Duration -> %02d:%02d:%02d     <<${NC}\n" $h $m $s
}

changed="find ./ -type f \( "
for ext in "${exts[@]}"; do
  changed="$changed -iname \"*.$ext\" -o"
done
changed=${changed::-2}
changed="$changed \)"
for ign in "${igns[@]}"; do
  changed="$changed -not -path \"./$ign/*\""
done
changed="$changed -exec stat -c \"%Y\" {} ';' | sort -r | head -n 1"

t=0
while true; do
  x=$(eval $changed)
  if [ -z "$x" ]; then x=0; fi
  if [[ $x > $t ]]; then
    t=$x
    run
  fi
  sleep $delay
done

Notice that extensions and folders to ignore should be passed to the script as comma-separated line, e.g. fs,cs,sh or output,test.

The gist here is to construct a command to find the very latest timestamp for any files that satisfy filter (extensions - ignored folders).

And now here is how you can call it for your project:

$ ./monitor.sh fs,cs output,test 3 ./build.sh

This command will trigger ./build.sh on any updates to files with fs and cs extensions that are not located inside output or test subfolders.

And this time around terminal colors from ./build.sh are fully preserved on all invokation of ./build.sh.

Happy hacking!