diff --git a/.gitignore b/.gitignore index a205373..af04dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ *.pyc *.tmp /redo-sh +*.did +.do_built +.do_built.dir diff --git a/minimal/default.zz.do b/minimal/default.zz.do new file mode 100644 index 0000000..397db75 --- /dev/null +++ b/minimal/default.zz.do @@ -0,0 +1 @@ +: diff --git a/minimal/do b/minimal/do index 1794b48..61889d9 100755 --- a/minimal/do +++ b/minimal/do @@ -28,22 +28,24 @@ if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then plain="$(printf '\033[m')" fi +# Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base) _dirsplit() { - base=${1##*/} - dir=${1%$base} + _dirsplit_base=${1##*/} + _dirsplit_dir=${1%$_dirsplit_base} } +# Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics. dirname() ( _dirsplit "$1" - dir=${dir%/} + dir=${_dirsplit_dir%/} echo "${dir:-.}" ) _dirsplit "$0" -export REDO=$(cd "${dir:-.}" && echo "$PWD/$base") -if [ "$base" = "redo-ifchange" ]; then ifchange=1; else ifchange=; fi +export REDO=$(cd "${_dirsplit_dir:-.}" && echo "$PWD/$_dirsplit_base") +_cmd=$_dirsplit_base DO_TOP= if [ -z "$DO_BUILT" ]; then @@ -68,7 +70,7 @@ _debug() { [ -z "$_do_opt_debug" ] || echo "$@" >&2 } -if [ -z "$DO_BUILT" ]; then +if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then DO_TOP=1 [ "$#" -gt 0 ] || set all # only toplevel redo has a default target export DO_BUILT=$PWD/.do_built @@ -84,7 +86,7 @@ if [ -z "$DO_BUILT" ]; then export PATH=$DO_PATH:$PATH rm -rf "$DO_PATH" mkdir "$DO_PATH" - for d in redo redo-ifchange; do + for d in redo redo-ifchange redo-whichdo; do ln -s "$REDO" "$DO_PATH/$d" done [ -e /bin/true ] && TRUE=/bin/true || TRUE=/usr/bin/true @@ -94,35 +96,148 @@ if [ -z "$DO_BUILT" ]; then fi -_find_dofile_pwd() +# Chop the "file" part off a /path/to/file pathname. +# Note that if the filename already ends in a /, we just remove the slash. +_updir() { - dofile=default.$1.do + local v="${1%/*}" + [ "$v" != "$1" ] && echo "$v" + # else "empty" which means we went past the root +} + + +# Returns true if $1 starts with $2. +_startswith() +{ + [ "${1#"$2"}" != "$1" ] +} + + +# Returns true if $1 ends with $2. +_endswith() +{ + [ "${1%"$2"}" != "$1" ] +} + + +# Prints $1 as a path relative to $PWD (not starting with /). +# If it already doesn't start with a /, doesn't change the string. +_relpath() +{ + local here="$(command pwd)" there="$1" out= hadslash= + #echo "RP start '$there' hs='$hadslash'" >&2 + _startswith "$there" "/" || { echo "$there" && return; } + [ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/ + here=${here%/}/ + while [ -n "$here" ]; do + #echo "RP out='$out' here='$here' there='$there'" >&2 + [ "${here%/}" = "${there%/}" ] && there= && break; + [ "${there#$here}" != "$there" ] && break + out=../$out + _dirsplit "${here%/}" + here=$_dirsplit_dir + done + there=${there#$here} + if [ -n "$there" ]; then + echo "$out${there%/}$hadslash" + else + echo "${out%/}$hadslash" + fi +} + + +# Prints a "normalized relative" path, with ".." resolved where possible. +# For example, a/b/../c will be reduced to just a/c. +_normpath() +( + local path="$1" out= isabs= + #echo "NP start '$path'" >&2 + if _startswith "$path" "/"; then + isabs=1 + else + path="${PWD%/}/$path" + fi + set -f + IFS=/ + for d in $path; do + #echo "NP out='$out' d='$d'" >&2 + if [ "$d" = ".." ]; then + out=$(_updir "${out%/}")/ + else + out=$out$d/ + fi + done + #echo "NP out='$out' (done)" >&2 + out=${out%/} + if [ -n "$isabs" ]; then + echo "${out:-/}" + else + _relpath "${out:-/}" + fi +) + + +# List the possible names for default*.do files in dir $1 matching the target +# pattern in $2. We stop searching when we find the first one that exists. +_find_dofiles_pwd() +{ + local dodir="$1" dofile="$2" + _startswith "$dofile" "default." || dofile=${dofile#*.} while :; do dofile=default.${dofile#default.*.} - [ -e "$dofile" -o "$dofile" = default.do ] && break + echo "$dodir$dofile" + [ -e "$dodir$dofile" ] && return 0 + [ "$dofile" = default.do ] && break done - ext=${dofile#default} - ext=${ext%.do} - base=${1%$ext} + return 1 } +# List the possible names for default*.do files in $PWD matching the target +# pattern in $1. We stop searching when we find the first name that works. +# If there are no matches in $PWD, we'll search in .., and so on, to the root. +_find_dofiles() +{ + local target="$1" dodir= dofile= newdir= + _debug "find_dofile: '$PWD' '$target'" + dofile="$target.do" + echo "$dofile" + [ -e "$dofile" ] && return 0 + + # Try default.*.do files, walking up the tree + _dirsplit "$dofile" + dodir=$_dirsplit_dir + dofile=$_dirsplit_base + for i in $(seq 100); do + [ -n "$dodir" ] && dodir=${dodir%/}/ + #echo "_find_dofiles: '$dodir' '$dofile'" >&2 + [ -e "$dodir$dofile" ] && return 0 + _find_dofiles_pwd "$dodir" "$dofile" && return 0 + newdir=$(_normpath "${dodir}..") + [ "$newdir" = "$dodir" ] && break + dodir=$newdir + done + return 1 +} + + +# Print the last .do file returned by _find_dofiles. +# If that file exists, returns 0, else 1. _find_dofile() { - local prefix= - while :; do - _find_dofile_pwd "$1" - [ -e "$dofile" ] && break - [ "$PWD" = "/" ] && break - target=${PWD##*/}/$target - tmp=${PWD##*/}/$tmp - prefix=${PWD##*/}/$prefix - cd .. - done - base=$prefix$base + local files="$(_find_dofiles "$1")" + rv=$? + #echo "files='$files'" >&2 + [ "$rv" -ne 0 ] && return $rv + echo "$files" | { + while read -r linex; do line=$linex; done + printf "%s\n" "$line" + } } +# Actually run the given $dofile with the arguments in $@. +# Note: you should always run this in a subshell. _run_dofile() { export DO_DEPTH="$DO_DEPTH " @@ -133,7 +248,7 @@ _run_dofile() cmd=${line1#"#!/"} if [ "$cmd" != "$line1" ]; then set -$_do_opt_verbose$_do_opt_exec - /$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2" + exec /$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2" else set -$_do_opt_verbose$_do_opt_exec :; . "$PWD/$dofile" >"$tmp.tmp2" @@ -141,23 +256,37 @@ _run_dofile() } +# Find and run the right .do file, starting in dir $1, for target $2, using +# filename $3 as the temporary output file. Renames the temp file to $2 when +# done. _do() { - local dir="$1" target="$2" tmp="$3" - if [ -z "$ifchange" ] || + local dir="$1" target="$2" tmp="$3" dopath= dodir= dofile= ext= + if [ "$_cmd" = "redo" ] || ( [ ! -e "$target" -o -d "$target" ] && [ ! -e "$target.did" ] ); then printf '%sdo %s%s%s%s\n' \ "$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2 - echo "$PWD/$target" >>"$DO_BUILT" - dofile=$target.do - base=$target - ext= - [ -e "$target.do" ] || _find_dofile "$target" - if [ ! -e "$dofile" ]; then - echo "do: $target: no .do file" >&2 + dopath=$(_find_dofile "$target") + if [ ! -e "$dopath" ]; then + echo "do: $target: no .do file ($PWD)" >&2 return 1 fi + echo "$PWD/$target" >>"$DO_BUILT" + _dirsplit "$dopath" + dodir=$_dirsplit_dir dofile=$_dirsplit_base + if _startswith "$dofile" "default."; then + ext=${dofile#default} + ext=${ext%.do} + else + ext= + fi + target=$PWD/$target + tmp=$PWD/$tmp + cd "$dodir" || return 99 + target=$(_relpath "$target") || return 98 + tmp=$(_relpath "$tmp") || return 97 + base=${target%$ext} [ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] || : >>"$target.did" ( _run_dofile "$target" "$base" "$tmp.tmp" ) @@ -185,19 +314,20 @@ _dir_shovel() xdir=$1 xbase=$2 xbasetmp=$2 while [ ! -d "$xdir" -a -n "$xdir" ]; do _dirsplit "${xdir%/}" - xbasetmp=${base}__$xbase - xdir=$dir xbase=$base/$xbase + xbasetmp=${_dirsplit_base}__$xbase + xdir=$_dirsplit_dir xbase=$_dirsplit_base/$xbase _debug "xbasetmp='$xbasetmp'" >&2 done } +# Implementation of the "redo" command. _redo() { set +e for i in "$@"; do _dirsplit "$i" - _dir_shovel "$dir" "$base" + _dir_shovel "$_dirsplit_dir" "$_dirsplit_base" dir=$xdir base=$xbase basetmp=$xbasetmp ( cd "$dir" && _do "$dir" "$base" "$basetmp" ) [ "$?" = 0 ] || return 1 @@ -205,7 +335,19 @@ _redo() } -_redo "$@" +# Implementation of the "redo-whichdo" command. +_whichdo() +{ + _find_dofiles "$1" +} + + +case $_cmd in + do|redo|redo-ifchange) _redo "$@" ;; + redo-whichdo) _whichdo "$1" ;; + do.test) ;; + *) printf "$0: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;; +esac [ "$?" = 0 ] || exit 1 if [ -n "$DO_TOP" ]; then diff --git a/minimal/do.test b/minimal/do.test new file mode 100755 index 0000000..2c0c3c7 --- /dev/null +++ b/minimal/do.test @@ -0,0 +1,164 @@ +#!/bin/sh +. ./do + +set -e + +NAME=$(basename "$0") +TESTNUM=0 +SECT=root + + +SECTION() +{ + SECT=$1 + TESTNUM=0 + printf "\nTesting \"$1\" in $0:\n" +} + + +no_nl() +{ + echo "$1" | { + out= + while read -r line; do + out="$out$line " + done + printf "%s" "${out% }" + } +} + + +check_s() +{ + local want="$1" got="$2" cmd="$3" + want_s=$(no_nl "$want") + got_s=$(no_nl "$got") + [ -z "$cmd" ] && cmd=$got + TESTNUM=$((TESTNUM + 1)) + if [ "$want" != "$got" ]; then + printf "\n<<< expected:\n%s\n=== got:\n%s\n>>>\n" "$want" "$got" + printf "! %s:%d '%.40s' = %s FAILED\n" "$SECT" "$TESTNUM" "$want_s" "$cmd" + exit 1 + else + printf "! %s:%d '%.40s' = %s ok\n" "$SECT" "$TESTNUM" "$want_s" "$cmd" + fi +} + + +check() +{ + local want="$1" got= rv= + shift + rv=0 + got=$("$@") || rv=$? + if [ "$rv" -ne 0 ]; then + check_s "" "" + else + check_s "$want" "$got" "$*" + fi +} + + +retval() +{ + set +e + "$@" >&2 + local rv=$? + set -e + echo "$rv" + return 0 +} + + +SECTION _starts_ends_with +check "0" retval true +check "1" retval false +check "0" retval _startswith "x.test" "x." +check "1" retval _startswith "x.test" "y." +check "1" retval _startswith "" " " +check "1" retval _startswith "x.test" "x*" +check "1" retval _startswith "do.test" "do*" + +check "0" retval _endswith "x.test" ".test" +check "1" retval _endswith "x.test" ".best" +check "1" retval _endswith "" " " +check "1" retval _endswith "x.test" "*.test" +check "1" retval _endswith "do.test" "*.test" + + + +SECTION _dirsplit +_dirsplit "/a/b/c" +check_s "/a/b/" "$_dirsplit_dir" +check_s "c" "$_dirsplit_base" + +_dirsplit "/a/b/c/" +check_s "/a/b/c/" "$_dirsplit_dir" +check_s "" "$_dirsplit_base" + +_dirsplit "/" +check_s "/" "$_dirsplit_dir" +check_s "" "$_dirsplit_base" + + +SECTION _relpath +check "a/b/c" _relpath $PWD/a/b/c +check "../a/b/c" _relpath $PWD/../a/b/c +check "" _relpath "$PWD" +(cd / && check "a/b/c" _relpath a/b/c) +(cd / && check "a/b/c" _relpath /a/b/c) +(cd / && check "" _relpath /) +(cd /usr/bin && check "../lib" _relpath /usr/lib) +(cd /usr/bin && check "../lib/" _relpath /usr/lib/) +(cd /usr/bin && check "../.." _relpath /) +(cd fakedir && check ".." _relpath ..) +(cd fakedir && check "../" _relpath ../) +(cd fakedir && check "../fakedir" _relpath ../fakedir) + + +SECTION _normpath +check "/usr/lib" _normpath /usr/../usr/bin/../lib +check "/" _normpath /a/b/c/../../.. +check "/" _normpath / +check "../a" _normpath ../a +(cd fakedir && check "../a/b" _normpath ../a/b) +(cd fakedir && check ".." _normpath ..) +(cd / && check "tuv" _normpath a/b/../../tuv) +(cd / && check "" _normpath a/b/../..) +check ".." _normpath ../ +check ".." _normpath .. + + +SECTION _find_dofile +check "test.do" _find_dofiles test +check "test.do" _find_dofile test + +want="x.y.zz.do +default.y.zz.do +default.zz.do" +check "$want" _find_dofiles x.y.zz + +want="x.y.zz.do +default.y.zz.do +default.zz.do +default.do +../default.y.zz.do +../default.zz.do" +(cd fakedir && check "$want" _find_dofiles x.y.zz) +(cd fakedir && check "../default.zz.do" _find_dofile x.y.zz) + +set +e +got=$(_find_dofiles x.y.z) +rv=$? +set -e +check_s "1" "$rv" +top_want="x.y.z.do +default.y.z.do +default.z.do +default.do +../default.y.z.do +../default.z.do +../default.do" +check_s "$top_want" "$(echo "$got" | head -n 7)" "_find_dofiles x.y.z" + +exit 0 diff --git a/minimal/fakedir/.empty b/minimal/fakedir/.empty new file mode 100644 index 0000000..e69de29 diff --git a/minimal/test.do b/minimal/test.do new file mode 100644 index 0000000..9239fdc --- /dev/null +++ b/minimal/test.do @@ -0,0 +1,2 @@ +exec >&2 +./do.test diff --git a/test.do b/test.do index a092156..9ea2c31 100644 --- a/test.do +++ b/test.do @@ -1,3 +1,3 @@ redo-ifchange _all -redo t/all +redo minimal/test t/all [ -n "$DO_BUILT" ] || echo "Don't forget to test 'minimal/do -c test'" >&2