On MacOS (at least 10.11.6), /bin/pwd defaults to using $PWD (ie. pwd -L). On most other OSes it defaults to *not* using $PWD (ie. pwd -P). We need the latter behaviour. It appears that 'pwd -P' has been specified by POSIX for quite a few years now, so let's rely on it. shelltest.od will now also check for it, though if your 'sh' doesn't support this feature, it'll be too late, because shelltest needs minimal/do in order to run.
436 lines
11 KiB
Bash
Executable file
436 lines
11 KiB
Bash
Executable file
#!/usr/bin/env sh
|
|
#
|
|
# A minimal alternative to djb redo that doesn't support incremental builds.
|
|
# For the full version, visit http://github.com/apenwarr/redo
|
|
#
|
|
# The author disclaims copyright to this source file and hereby places it in
|
|
# the public domain. (2010 12 14; updated 2018 12 13)
|
|
#
|
|
USAGE="
|
|
usage: do [-d] [-x] [-v] [-c] <targets...>
|
|
-d print extra debug messages (mostly about dependency checks)
|
|
-v run .do files with 'set -v'
|
|
-x run .do files with 'set -x'
|
|
-c clean up all old targets before starting
|
|
|
|
Note: do is an implementation of redo that does *not* check dependencies.
|
|
It will never rebuild a target it has already built, unless you use -c.
|
|
"
|
|
|
|
# By default, no output coloring.
|
|
green=""
|
|
bold=""
|
|
plain=""
|
|
|
|
if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
|
|
green="$(printf '\033[32m')"
|
|
bold="$(printf '\033[1m')"
|
|
plain="$(printf '\033[m')"
|
|
fi
|
|
|
|
# Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base)
|
|
_dirsplit()
|
|
{
|
|
_dirsplit_base=${1##*/}
|
|
_dirsplit_dir=${1%$_dirsplit_base}
|
|
}
|
|
|
|
# Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics.
|
|
qdirname()
|
|
(
|
|
_dirsplit "$1"
|
|
dir=${_dirsplit_dir%/}
|
|
echo "${dir:-.}"
|
|
)
|
|
|
|
_dirsplit "$0"
|
|
REDO=$(cd "$(pwd -P)" &&
|
|
cd "${_dirsplit_dir:-.}" &&
|
|
echo "$PWD/$_dirsplit_base")
|
|
export REDO
|
|
_cmd=$_dirsplit_base
|
|
|
|
DO_TOP=
|
|
if [ -z "$DO_BUILT" ]; then
|
|
export _do_opt_debug=
|
|
export _do_opt_exec=
|
|
export _do_opt_verbose=
|
|
export _do_opt_clean=
|
|
fi
|
|
while getopts 'dxvcj:h?' _opt; do
|
|
case $_opt in
|
|
d) _do_opt_debug=1 ;;
|
|
x) _do_opt_exec=x ;;
|
|
v) _do_opt_verbose=v ;;
|
|
c) _do_opt_clean=1 ;;
|
|
j) ;; # silently ignore, for compat with real redo
|
|
\?|h|*) printf "%s" "$USAGE" >&2
|
|
exit 99
|
|
;;
|
|
esac
|
|
done
|
|
shift "$((OPTIND - 1))"
|
|
_debug() {
|
|
[ -z "$_do_opt_debug" ] || echo "$@" >&2
|
|
}
|
|
|
|
if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then
|
|
DO_TOP=1
|
|
if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then
|
|
set all # only toplevel redo has a default target
|
|
fi
|
|
export DO_STARTDIR="$(pwd -P)"
|
|
# If starting /bin/pwd != $PWD, this will fix it.
|
|
# That can happen when $PWD contains symlinks that the shell is
|
|
# trying helpfully (but unsuccessfully) to hide from the user.
|
|
cd "$DO_STARTDIR" || exit 99
|
|
export DO_BUILT="$PWD/.do_built"
|
|
if [ -z "$_do_opt_clean" -a -e "$DO_BUILT" ]; then
|
|
echo "do: Incremental mode. Use -c for clean rebuild." >&2
|
|
fi
|
|
: >>"$DO_BUILT"
|
|
sort -u "$DO_BUILT" >"$DO_BUILT.new"
|
|
while read f; do
|
|
[ -n "$_do_opt_clean" ] && printf "%s\0%s.did\0" "$f" "$f"
|
|
printf "%s.did.tmp\0" "$f"
|
|
done <"$DO_BUILT.new" |
|
|
xargs -0 rm -f 2>/dev/null
|
|
mv "$DO_BUILT.new" "$DO_BUILT"
|
|
export DO_PATH="$DO_BUILT.dir"
|
|
export PATH="$DO_PATH:$PATH"
|
|
rm -rf "$DO_PATH"
|
|
mkdir "$DO_PATH"
|
|
for d in redo redo-ifchange redo-whichdo; do
|
|
ln -s "$REDO" "$DO_PATH/$d"
|
|
done
|
|
for d in redo-ifcreate redo-stamp redo-always redo-ood \
|
|
redo-targets redo-sources; do
|
|
echo "#!/bin/sh" >"$DO_PATH/$d"
|
|
chmod a+rx "$DO_PATH/$d"
|
|
done
|
|
fi
|
|
|
|
|
|
# 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()
|
|
{
|
|
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 if it's absolute, or $2/$1 if $1 is not absolute.
|
|
_abspath()
|
|
{
|
|
local here="$2" there="$1"
|
|
if _startswith "$1" "/"; then
|
|
echo "$1"
|
|
else
|
|
echo "$2/$1"
|
|
fi
|
|
}
|
|
|
|
|
|
# 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="$2" 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" relto="$2" out= isabs=
|
|
#echo "NP start '$path'" >&2
|
|
if _startswith "$path" "/"; then
|
|
isabs=1
|
|
else
|
|
path="${relto%/}/$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:-/}" "$relto"
|
|
fi
|
|
)
|
|
|
|
|
|
# Prints a "real" path, with all symlinks resolved where possible.
|
|
_realpath()
|
|
{
|
|
local path="$1" relto="$2" isabs= rest=
|
|
if _startswith "$path" "/"; then
|
|
isabs=1
|
|
else
|
|
path="${relto%/}/$path"
|
|
fi
|
|
(
|
|
for d in $(seq 100); do
|
|
#echo "Trying: $PWD--$path" >&2
|
|
if cd -P "$path" 2>/dev/null; then
|
|
# success
|
|
pwd=$(pwd -P)
|
|
#echo " chdir ok: $pwd--$rest" >&2
|
|
np=$(_normpath "${pwd%/}/$rest" "$relto")
|
|
if [ -n "$isabs" ]; then
|
|
echo "$np"
|
|
else
|
|
_relpath "$np" "$relto"
|
|
fi
|
|
break
|
|
fi
|
|
_dirsplit "${path%/}"
|
|
path=$_dirsplit_dir
|
|
rest="$_dirsplit_base/$rest"
|
|
done
|
|
)
|
|
}
|
|
|
|
|
|
# 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.*.}
|
|
echo "$dodir$dofile"
|
|
[ -e "$dodir$dofile" ] && return 0
|
|
[ "$dofile" = default.do ] && break
|
|
done
|
|
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
|
|
[ -n "$dodir" ] && dodir=${dodir%/}/
|
|
[ -e "$dodir$dofile" ] && return 0
|
|
for i in $(seq 100); do
|
|
[ -n "$dodir" ] && dodir=${dodir%/}/
|
|
#echo "_find_dofiles: '$dodir' '$dofile'" >&2
|
|
_find_dofiles_pwd "$dodir" "$dofile" && return 0
|
|
newdir=$(_realpath "${dodir}.." "$PWD")
|
|
[ "$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 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 "
|
|
export REDO_TARGET="$PWD/$target"
|
|
local line1
|
|
set -e
|
|
read line1 <"$PWD/$dofile" || true
|
|
cmd=${line1#"#!/"}
|
|
if [ "$cmd" != "$line1" ]; then
|
|
set -$_do_opt_verbose$_do_opt_exec
|
|
exec /$cmd "$PWD/$dofile" "$@"
|
|
else
|
|
set -$_do_opt_verbose$_do_opt_exec
|
|
# If $dofile is empty, "." might not change $? at
|
|
# all, so we clear it first with ":".
|
|
:; . "$PWD/$dofile"
|
|
fi
|
|
}
|
|
|
|
|
|
# Find and run the right .do file, starting in dir $1, for target $2,
|
|
# providing a temporary output file as $3. Renames the temp file to $2 when
|
|
# done.
|
|
_do()
|
|
{
|
|
local dir="$1" target="$1$2" tmp="$1$2.redo.tmp" tdir=
|
|
local 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" "$target" "$plain" >&2
|
|
dopath=$(_find_dofile "$target")
|
|
if [ ! -e "$dopath" ]; then
|
|
echo "do: $target: no .do file ($PWD)" >&2
|
|
return 1
|
|
fi
|
|
_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" "$PWD") || return 98
|
|
tmp=$(_relpath "$tmp" "$PWD") || return 97
|
|
base=${target%$ext}
|
|
tdir=$(qdirname "$target")
|
|
[ ! -e "$DO_BUILT" ] || [ ! -w "$tdir/." ] ||
|
|
: >>"$target.did.tmp"
|
|
# $qtmp is a temporary file used to capture stdout.
|
|
# Since it might be accidentally deleted as a .do file
|
|
# does its work, we create it, then open two fds to it,
|
|
# then immediately delete the name. We use one fd to
|
|
# redirect to stdout, and the other to read from after,
|
|
# because there's no way to fseek(fd, 0) in sh.
|
|
qtmp=$DO_PATH/do.$$.tmp
|
|
(
|
|
rm -f "$qtmp"
|
|
( _run_dofile "$target" "$base" "$tmp" >&3 3>&- 4<&- )
|
|
rv=$?
|
|
if [ $rv != 0 ]; then
|
|
printf "do: %s%s\n" "$DO_DEPTH" \
|
|
"$target: got exit code $rv" >&2
|
|
rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did"
|
|
return $rv
|
|
fi
|
|
echo "$PWD/$target" >>"$DO_BUILT"
|
|
if [ ! -e "$tmp" ]; then
|
|
# if $3 wasn't created, copy from stdout file
|
|
cat <&4 >$tmp
|
|
# if that's zero length too, forget it
|
|
[ -s "$tmp" ] || rm -f "$tmp"
|
|
fi
|
|
) 3>$qtmp 4<$qtmp # can't use "|| return" here...
|
|
# ...because "|| return" would mess up "set -e" inside the ()
|
|
# on some shells. Running commands in "||" context, even
|
|
# deep inside, will stop "set -e" from functioning.
|
|
rv=$?
|
|
[ "$rv" = 0 ] || return "$rv"
|
|
mv "$tmp" "$target" 2>/dev/null
|
|
[ -e "$target.did.tmp" ] &&
|
|
mv "$target.did.tmp" "$target.did" ||
|
|
: >>"$target.did"
|
|
else
|
|
_debug "do $DO_DEPTH$target exists." >&2
|
|
fi
|
|
}
|
|
|
|
|
|
# Implementation of the "redo" command.
|
|
_redo()
|
|
{
|
|
local i startdir="$PWD" dir base
|
|
set +e
|
|
for i in "$@"; do
|
|
i=$(_abspath "$i" "$startdir")
|
|
(
|
|
cd "$DO_STARTDIR" || return 99
|
|
i=$(_realpath "$(_relpath "$i" "$PWD")" "$PWD")
|
|
_dirsplit "$i"
|
|
dir=$_dirsplit_dir base=$_dirsplit_base
|
|
_do "$dir" "$base"
|
|
)
|
|
[ "$?" = 0 ] || return 1
|
|
done
|
|
}
|
|
|
|
|
|
# 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 "do: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;;
|
|
esac
|
|
[ "$?" = 0 ] || exit 1
|
|
|
|
if [ -n "$DO_TOP" ]; then
|
|
if [ -n "$_do_opt_clean" ]; then
|
|
echo "do: Removing stamp files..." >&2
|
|
[ ! -e "$DO_BUILT" ] ||
|
|
while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
|
|
xargs -0 rm -f 2>/dev/null
|
|
fi
|
|
fi
|