modsync (2958B)
1 #!/bin/sh 2 # Copyright 2022 Jacob R. Edwards 3 # Configuration module syncing script 4 # 5 # This script should conform to POSIX, because why not: 6 POSIXLY_CORRECT= 7 8 error() { 9 echo "$*" 1>&2 10 exit 1 11 } 12 13 usererror() { 14 error "usage: $name [push|pull|diff|stop] [-cdnop] [-s subs] dest module ..." 15 test "${1:-}" && 16 error "$@" 17 } 18 19 ask() { 20 test -t 0 -a -t 2 && 21 echo -n "$1? [$2] " 1>&2 22 awk -vd="$2" '{ a=$0; exit } END{ if(!a) a=d; exit a !~ "^[Yy]" }' 23 } 24 25 # Could be dynamically generated for efficiency, install(1) is also 26 # an option. 27 update() { 28 cp -p "$1" "$2" 29 test "$owner" && 30 chown "$owner" "$2" 31 test "$perms" && 32 chmod "$perms" "$2" 33 } 34 35 install() { 36 mkdir -p "$(dirname "$2")" 37 # If there is an error, it's likely that we're on a different 38 # filesystem. If not, cp(1) will likely have the same one. 39 ! $fcopy && 40 ln "$1" "$2" 2>/dev/null && return 41 update "$1" "$2" 42 } 43 44 # TODO: Differing source mode, owner, etc. should be updated in dest 45 filesync() { 46 # Fix diff(1) output, I cannot get it to produce it for the 47 # file I want when pulling. 48 49 if test "$1" -ef "$2" 50 then 51 return 52 elif ! test -f "$2" 53 then 54 echo "$2" 1>&2 55 install "$1" "$2" 56 elif $fdiff || test "$1" -nt "$2" -o "$1" -ot "$2" 57 then 58 if ! diff -u "$2" "$1" 1>&2 59 then 60 $patch && ask "patch $2" y && 61 update "$1" "$2" 62 else 63 touch -r "$1" "$2" 64 fi 65 fi 66 return 0 67 } 68 69 addsub() { 70 awk -vOFS="$IFS2" -F/ -vsubs="$1" 'BEGIN { 71 len = split(subs, a, ","); 72 for (i = 1; i <= len; ++i) { 73 if (!(n = index(a[i], "="))) { 74 print "Invalid substitution" > "/dev/stderr"; 75 exit 1; 76 } 77 t[substr(a[i], 1, n - 1)] = substr(a[i], n + 1); 78 } 79 } 80 81 { 82 new = $0 83 if ($1 in t) 84 new = t[$1] substr(new, length($1) + 1); 85 print $0, new 86 }' 87 } 88 89 modsync() { 90 cmd="$1" 91 mod="$2" 92 dir="$3" 93 94 case "$cmd" in 95 (push) sync() filesync "$1" "$2" ;; 96 (pull) sync() filesync "$2" "$1" ;; 97 (stop) sync() rm -v "$2" ;; 98 (*) usererror "$name: '$1': Invalid command" ;; 99 esac 100 101 for files in $(cd "$mod" && find . -type f | cut -c 3- | addsub "$subs") 102 do ( 103 IFS="$IFS2" 104 set -- $files 105 test $# -ne 2 && { 106 echo 'error: fs (034) character in path' 1>&2 107 exit 1 108 } 109 sync "$mod/$1" "$dir/$2" 110 ) done 111 } 112 113 # Unset variables are unchecked on purpose, nounset (-u) handles it 114 set -eu 115 116 IFS=' 117 ' 118 # \034 (fs) 119 IFS2='' 120 name="$(basename "$0")" 121 122 # Assume exit is due to user error, namely an unset variable 123 trap usererror 0 124 125 if test "$1" = 'diff' 126 then 127 shift 128 set -- push -n "$@" 129 fi 130 131 cmd="$1" 132 shift 133 134 patch=true 135 owner= 136 perms= 137 fdiff=false 138 fcopy=false 139 subs= 140 while expr x"$1" : x- > /dev/null 141 do 142 case "$1" in 143 (-n) patch=false ;; 144 (-o) owner="$2"; shift ;; 145 (-p) perms="$2"; shift ;; 146 (-d) fdiff=true ;; 147 (-c) fcopy=true ;; 148 (-s) subs="$2"; shift ;; 149 (*) usererror "$name: '$1': Invalid option" ;; 150 esac 151 shift 152 done 153 154 dir="${1:%/}" 155 shift 156 157 trap "" 0 158 159 if test "$cmd" = stop && $patch 160 then 161 "$0" pull "$dir" "$@" 162 fi 163 164 for mod in "$@" 165 do 166 modsync "$cmd" "$mod" "$dir" 167 done 168 exit 0