Apparently on some (probably buggy) platforms, when CDPATH is set, the 'cd' command will always print text to stdout. This caused fail 122 and 123 in shelltest.od. CDPATH is an interactive-mode option, so let's clear it when running scripts.
446 lines
11 KiB
Bash
Executable file
446 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 2019 02 24)
|
|
#
|
|
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.
|
|
"
|
|
|
|
# CDPATH apparently causes unexpected 'cd' output on some platforms.
|
|
unset CDPATH
|
|
|
|
# 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
|
|
|
|
# The 'seq' command is not available on all platforms.
|
|
_seq() {
|
|
local x=0 max="$1"
|
|
while [ "$x" -lt "$max" ]; do
|
|
x=$((x + 1))
|
|
echo "$x"
|
|
done
|
|
}
|
|
|
|
# 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
|