do (11096B)
1 #!/usr/bin/env sh 2 # 3 # A minimal alternative to djb redo that doesn't support incremental builds. 4 # For the full version, visit http://github.com/apenwarr/redo 5 # 6 # The author disclaims copyright to this source file and hereby places it in 7 # the public domain. (2010 12 14; updated 2019 02 24) 8 # 9 USAGE=" 10 usage: do [-d] [-x] [-v] [-c] <targets...> 11 -d print extra debug messages (mostly about dependency checks) 12 -v run .do files with 'set -v' 13 -x run .do files with 'set -x' 14 -c clean up all old targets before starting 15 16 Note: do is an implementation of redo that does *not* check dependencies. 17 It will never rebuild a target it has already built, unless you use -c. 18 " 19 20 # CDPATH apparently causes unexpected 'cd' output on some platforms. 21 unset CDPATH 22 23 # By default, no output coloring. 24 green="" 25 bold="" 26 plain="" 27 28 if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then 29 green="$(printf '\033[32m')" 30 bold="$(printf '\033[1m')" 31 plain="$(printf '\033[m')" 32 fi 33 34 # The 'seq' command is not available on all platforms. 35 _seq() { 36 local x=0 max="$1" 37 while [ "$x" -lt "$max" ]; do 38 x=$((x + 1)) 39 echo "$x" 40 done 41 } 42 43 # Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base) 44 _dirsplit() { 45 _dirsplit_base=${1##*/} 46 _dirsplit_dir=${1%$_dirsplit_base} 47 } 48 49 # Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics. 50 qdirname() ( 51 _dirsplit "$1" 52 dir=${_dirsplit_dir%/} 53 echo "${dir:-.}" 54 ) 55 56 _dirsplit "$0" 57 REDO=$(cd "$(pwd -P)" && 58 cd "${_dirsplit_dir:-.}" && 59 echo "$PWD/$_dirsplit_base") 60 export REDO 61 _cmd=$_dirsplit_base 62 63 DO_TOP= 64 if [ -z "$DO_BUILT" ]; then 65 export _do_opt_debug= 66 export _do_opt_exec= 67 export _do_opt_verbose= 68 export _do_opt_clean= 69 fi 70 while getopts 'dxvcj:h?' _opt; do 71 case $_opt in 72 d) _do_opt_debug=1 ;; 73 x) _do_opt_exec=x ;; 74 v) _do_opt_verbose=v ;; 75 c) _do_opt_clean=1 ;; 76 j) ;; # silently ignore, for compat with real redo 77 \?|h|*) printf "%s" "$USAGE" >&2 78 exit 99 79 ;; 80 esac 81 done 82 shift "$((OPTIND - 1))" 83 _debug() { 84 [ -z "$_do_opt_debug" ] || echo "$@" >&2 85 } 86 87 if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then 88 DO_TOP=1 89 if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then 90 set all # only toplevel redo has a default target 91 fi 92 export DO_STARTDIR="$(pwd -P)" 93 # If starting /bin/pwd != $PWD, this will fix it. 94 # That can happen when $PWD contains symlinks that the shell is 95 # trying helpfully (but unsuccessfully) to hide from the user. 96 cd "$DO_STARTDIR" || exit 99 97 export DO_BUILT="$PWD/.do_built" 98 if [ -z "$_do_opt_clean" -a -e "$DO_BUILT" ]; then 99 echo "do: Incremental mode. Use -c for clean rebuild." >&2 100 fi 101 : >>"$DO_BUILT" 102 sort -u "$DO_BUILT" >"$DO_BUILT.new" 103 while read f; do 104 [ -n "$_do_opt_clean" ] && printf "%s\0%s.did\0" "$f" "$f" 105 printf "%s.did.tmp\0" "$f" 106 done <"$DO_BUILT.new" | 107 xargs -0 rm -f 2>/dev/null 108 mv "$DO_BUILT.new" "$DO_BUILT" 109 export DO_PATH="$DO_BUILT.dir" 110 export PATH="$DO_PATH:$PATH" 111 rm -rf "$DO_PATH" 112 mkdir "$DO_PATH" 113 for d in redo redo-ifchange redo-whichdo; do 114 ln -s "$REDO" "$DO_PATH/$d" 115 done 116 for d in redo-ifcreate redo-stamp redo-always redo-ood \ 117 redo-targets redo-sources; do 118 echo "#!/bin/sh" >"$DO_PATH/$d" 119 chmod a+rx "$DO_PATH/$d" 120 done 121 fi 122 123 124 # Chop the "file" part off a /path/to/file pathname. 125 # Note that if the filename already ends in a /, we just remove the slash. 126 _updir() 127 { 128 local v="${1%/*}" 129 [ "$v" != "$1" ] && echo "$v" 130 # else "empty" which means we went past the root 131 } 132 133 134 # Returns true if $1 starts with $2. 135 _startswith() 136 { 137 [ "${1#"$2"}" != "$1" ] 138 } 139 140 141 # Returns true if $1 ends with $2. 142 _endswith() 143 { 144 [ "${1%"$2"}" != "$1" ] 145 } 146 147 148 # Prints $1 if it's absolute, or $2/$1 if $1 is not absolute. 149 _abspath() 150 { 151 local here="$2" there="$1" 152 if _startswith "$1" "/"; then 153 echo "$1" 154 else 155 echo "$2/$1" 156 fi 157 } 158 159 160 # Prints $1 as a path relative to $PWD (not starting with /). 161 # If it already doesn't start with a /, doesn't change the string. 162 _relpath() 163 { 164 local here="$2" there="$1" out= hadslash= 165 #echo "RP start '$there' hs='$hadslash'" >&2 166 _startswith "$there" "/" || { echo "$there" && return; } 167 [ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/ 168 here=${here%/}/ 169 while [ -n "$here" ]; do 170 #echo "RP out='$out' here='$here' there='$there'" >&2 171 [ "${here%/}" = "${there%/}" ] && there= && break; 172 [ "${there#$here}" != "$there" ] && break 173 out=../$out 174 _dirsplit "${here%/}" 175 here=$_dirsplit_dir 176 done 177 there=${there#$here} 178 if [ -n "$there" ]; then 179 echo "$out${there%/}$hadslash" 180 else 181 echo "${out%/}$hadslash" 182 fi 183 } 184 185 186 # Prints a "normalized relative" path, with ".." resolved where possible. 187 # For example, a/b/../c will be reduced to just a/c. 188 _normpath() 189 ( 190 local path="$1" relto="$2" out= isabs= 191 #echo "NP start '$path'" >&2 192 if _startswith "$path" "/"; then 193 isabs=1 194 else 195 path="${relto%/}/$path" 196 fi 197 set -f 198 IFS=/ 199 for d in ${path%/}; do 200 #echo "NP out='$out' d='$d'" >&2 201 if [ "$d" = ".." ]; then 202 out=$(_updir "${out%/}")/ 203 else 204 out=$out$d/ 205 fi 206 done 207 #echo "NP out='$out' (done)" >&2 208 out=${out%/} 209 if [ -n "$isabs" ]; then 210 echo "${out:-/}" 211 else 212 _relpath "${out:-/}" "$relto" 213 fi 214 ) 215 216 217 # Prints a "real" path, with all symlinks resolved where possible. 218 _realpath() 219 { 220 local path="$1" relto="$2" isabs= rest= 221 if _startswith "$path" "/"; then 222 isabs=1 223 else 224 path="${relto%/}/$path" 225 fi 226 ( 227 for d in $(_seq 100); do 228 #echo "Trying: $PWD--$path" >&2 229 if cd -P "$path" 2>/dev/null; then 230 # success 231 pwd=$(pwd -P) 232 #echo " chdir ok: $pwd--$rest" >&2 233 np=$(_normpath "${pwd%/}/$rest" "$relto") 234 if [ -n "$isabs" ]; then 235 echo "$np" 236 else 237 _relpath "$np" "$relto" 238 fi 239 break 240 fi 241 _dirsplit "${path%/}" 242 path=$_dirsplit_dir 243 rest="$_dirsplit_base/$rest" 244 done 245 ) 246 } 247 248 249 # List the possible names for default*.do files in dir $1 matching the target 250 # pattern in $2. We stop searching when we find the first one that exists. 251 _find_dofiles_pwd() 252 { 253 local dodir="$1" dofile="$2" 254 _startswith "$dofile" "default." || dofile=${dofile#*.} 255 while :; do 256 dofile=default.${dofile#default.*.} 257 echo "$dodir$dofile" 258 [ -e "$dodir$dofile" ] && return 0 259 [ "$dofile" = default.do ] && break 260 done 261 return 1 262 } 263 264 265 # List the possible names for default*.do files in $PWD matching the target 266 # pattern in $1. We stop searching when we find the first name that works. 267 # If there are no matches in $PWD, we'll search in .., and so on, to the root. 268 _find_dofiles() 269 { 270 local target="$1" dodir= dofile= newdir= 271 _debug "find_dofile: '$PWD' '$target'" 272 dofile="$target.do" 273 echo "$dofile" 274 [ -e "$dofile" ] && return 0 275 276 # Try default.*.do files, walking up the tree 277 _dirsplit "$dofile" 278 dodir=$_dirsplit_dir 279 dofile=$_dirsplit_base 280 [ -n "$dodir" ] && dodir=${dodir%/}/ 281 [ -e "$dodir$dofile" ] && return 0 282 for i in $(_seq 100); do 283 [ -n "$dodir" ] && dodir=${dodir%/}/ 284 #echo "_find_dofiles: '$dodir' '$dofile'" >&2 285 _find_dofiles_pwd "$dodir" "$dofile" && return 0 286 newdir=$(_realpath "${dodir}.." "$PWD") 287 [ "$newdir" = "$dodir" ] && break 288 dodir=$newdir 289 done 290 return 1 291 } 292 293 294 # Print the last .do file returned by _find_dofiles. 295 # If that file exists, returns 0, else 1. 296 _find_dofile() 297 { 298 local files="$(_find_dofiles "$1")" 299 rv=$? 300 #echo "files='$files'" >&2 301 [ "$rv" -ne 0 ] && return $rv 302 echo "$files" | { 303 while read -r linex; do line=$linex; done 304 printf "%s\n" "$line" 305 } 306 } 307 308 309 # Actually run the given $dofile with the arguments in $@. 310 # Note: you should always run this in a subshell. 311 _run_dofile() 312 { 313 export DO_DEPTH="$DO_DEPTH " 314 export REDO_TARGET="$PWD/$target" 315 local line1 316 set -e 317 read line1 <"$PWD/$dofile" || true 318 cmd=${line1#"#!/"} 319 if [ "$cmd" != "$line1" ]; then 320 set -$_do_opt_verbose$_do_opt_exec 321 exec /$cmd "$PWD/$dofile" "$@" 322 else 323 set -$_do_opt_verbose$_do_opt_exec 324 # If $dofile is empty, "." might not change $? at 325 # all, so we clear it first with ":". 326 :; . "$PWD/$dofile" 327 fi 328 } 329 330 331 # Find and run the right .do file, starting in dir $1, for target $2, 332 # providing a temporary output file as $3. Renames the temp file to $2 when 333 # done. 334 _do() 335 { 336 local dir="$1" target="$1$2" tmp="$1$2.redo.tmp" tdir= 337 local dopath= dodir= dofile= ext= 338 if [ "$_cmd" = "redo" ] || 339 ( [ ! -e "$target" -o -d "$target" ] && 340 [ ! -e "$target.did" ] ); then 341 printf '%sdo %s%s%s%s\n' \ 342 "$green" "$DO_DEPTH" "$bold" "$target" "$plain" >&2 343 dopath=$(_find_dofile "$target") 344 if [ ! -e "$dopath" ]; then 345 echo "do: $target: no .do file ($PWD)" >&2 346 return 1 347 fi 348 _dirsplit "$dopath" 349 dodir=$_dirsplit_dir dofile=$_dirsplit_base 350 if _startswith "$dofile" "default."; then 351 ext=${dofile#default} 352 ext=${ext%.do} 353 else 354 ext= 355 fi 356 target=$PWD/$target 357 tmp=$PWD/$tmp 358 cd "$dodir" || return 99 359 target=$(_relpath "$target" "$PWD") || return 98 360 tmp=$(_relpath "$tmp" "$PWD") || return 97 361 base=${target%$ext} 362 tdir=$(qdirname "$target") 363 [ ! -e "$DO_BUILT" ] || [ ! -w "$tdir/." ] || 364 : >>"$target.did.tmp" 365 # $qtmp is a temporary file used to capture stdout. 366 # Since it might be accidentally deleted as a .do file 367 # does its work, we create it, then open two fds to it, 368 # then immediately delete the name. We use one fd to 369 # redirect to stdout, and the other to read from after, 370 # because there's no way to fseek(fd, 0) in sh. 371 qtmp=$DO_PATH/do.$$.tmp 372 ( 373 rm -f "$qtmp" 374 ( _run_dofile "$target" "$base" "$tmp" >&3 3>&- 4<&- ) 375 rv=$? 376 if [ $rv != 0 ]; then 377 printf "do: %s%s\n" "$DO_DEPTH" \ 378 "$target: got exit code $rv" >&2 379 rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did" 380 return $rv 381 fi 382 echo "$PWD/$target" >>"$DO_BUILT" 383 if [ ! -e "$tmp" ]; then 384 # if $3 wasn't created, copy from stdout file 385 cat <&4 >$tmp 386 # if that's zero length too, forget it 387 [ -s "$tmp" ] || rm -f "$tmp" 388 fi 389 ) 3>$qtmp 4<$qtmp # can't use "|| return" here... 390 # ...because "|| return" would mess up "set -e" inside the () 391 # on some shells. Running commands in "||" context, even 392 # deep inside, will stop "set -e" from functioning. 393 rv=$? 394 [ "$rv" = 0 ] || return "$rv" 395 mv "$tmp" "$target" 2>/dev/null 396 [ -e "$target.did.tmp" ] && 397 mv "$target.did.tmp" "$target.did" || 398 : >>"$target.did" 399 else 400 _debug "do $DO_DEPTH$target exists." >&2 401 fi 402 } 403 404 405 # Implementation of the "redo" command. 406 _redo() 407 { 408 local i startdir="$PWD" dir base 409 set +e 410 for i in "$@"; do 411 i=$(_abspath "$i" "$startdir") 412 ( 413 cd "$DO_STARTDIR" || return 99 414 i=$(_realpath "$(_relpath "$i" "$PWD")" "$PWD") 415 _dirsplit "$i" 416 dir=$_dirsplit_dir base=$_dirsplit_base 417 _do "$dir" "$base" 418 ) 419 [ "$?" = 0 ] || return 1 420 done 421 } 422 423 424 # Implementation of the "redo-whichdo" command. 425 _whichdo() 426 { 427 _find_dofiles "$1" 428 } 429 430 431 case $_cmd in 432 do|redo|redo-ifchange) _redo "$@" ;; 433 redo-whichdo) _whichdo "$1" ;; 434 do.test) ;; 435 *) printf "do: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;; 436 esac 437 [ "$?" = 0 ] || exit 1 438 439 if [ -n "$DO_TOP" ]; then 440 if [ -n "$_do_opt_clean" ]; then 441 echo "do: Removing stamp files..." >&2 442 [ ! -e "$DO_BUILT" ] || 443 while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" | 444 xargs -0 rm -f 2>/dev/null 445 fi 446 fi