Use mkstemp() to create the stdout temp file, and simplify $3 path.

Previously, we'd try to put the stdout temp file in the same dir as the
target, if that dir exists.  Otherwise we'd walk up the directory tree
looking for a good place.  But this would go wrong if the directory we
chose got *deleted* during the run of the .do file.

Instead, we switch to an entirely new design: we use mkstemp() to
generate a temp file in the standard temp file location (probably
/tmp), then open it and immediately delete it, so the .do file can't
cause any unexpected behaviour.  After the .do file exits, we use our
still-open fd to the stdout file to read the content back out.

In the old implementation, we also put the $3 in the "adjusted"
location that depended whether the target dir already existed, just for
consistency.  But that was never necessary: we didn't create the $3
file, and if the .do script wants to write to $3, it should create the
target dir first anyway.  So change it to *always* use a $3 temp
filename in the target dir, which is much simpler and so has fewer edge
cases.

Add t/202-del/deltest4 with some tests for all these edge cases.

Reported-by: Jeff Stearns <jeff.stearns@gmail.com>
This commit is contained in:
Avery Pennarun 2018-12-12 03:37:44 +00:00
commit d95277d121
10 changed files with 177 additions and 119 deletions

View file

@ -4,7 +4,7 @@
# 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 11)
# the public domain. (2010 12 14; updated 2018 12 13)
#
USAGE="
usage: do [-d] [-x] [-v] [-c] <targets...>
@ -77,6 +77,7 @@ if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then
if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then
set all # only toplevel redo has a default target
fi
export DO_STARTDIR="$PWD"
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
@ -89,7 +90,7 @@ if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then
done <"$DO_BUILT.new" |
xargs -0 rm -f 2>/dev/null
mv "$DO_BUILT.new" "$DO_BUILT"
DO_PATH=$DO_BUILT.dir
export DO_PATH="$DO_BUILT.dir"
export PATH="$DO_PATH:$PATH"
rm -rf "$DO_PATH"
mkdir "$DO_PATH"
@ -128,11 +129,23 @@ _endswith()
}
# 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="$(command pwd)" there="$1" out= hadslash=
local here="$2" there="$1" out= hadslash=
#echo "RP start '$there' hs='$hadslash'" >&2
_startswith "$there" "/" || { echo "$there" && return; }
[ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/
@ -158,12 +171,12 @@ _relpath()
# For example, a/b/../c will be reduced to just a/c.
_normpath()
(
local path="$1" out= isabs=
local path="$1" relto="$2" out= isabs=
#echo "NP start '$path'" >&2
if _startswith "$path" "/"; then
isabs=1
else
path="${PWD%/}/$path"
path="${relto%/}/$path"
fi
set -f
IFS=/
@ -180,7 +193,7 @@ _normpath()
if [ -n "$isabs" ]; then
echo "${out:-/}"
else
_relpath "${out:-/}"
_relpath "${out:-/}" "$relto"
fi
)
@ -222,7 +235,7 @@ _find_dofiles()
[ -n "$dodir" ] && dodir=${dodir%/}/
#echo "_find_dofiles: '$dodir' '$dofile'" >&2
_find_dofiles_pwd "$dodir" "$dofile" && return 0
newdir=$(_normpath "${dodir}..")
newdir=$(_normpath "${dodir}.." "$PWD")
[ "$newdir" = "$dodir" ] && break
dodir=$newdir
done
@ -257,25 +270,26 @@ _run_dofile()
cmd=${line1#"#!/"}
if [ "$cmd" != "$line1" ]; then
set -$_do_opt_verbose$_do_opt_exec
exec /$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
exec /$cmd "$PWD/$dofile" "$@"
else
set -$_do_opt_verbose$_do_opt_exec
:; . "$PWD/$dofile" >"$tmp.tmp2"
:; . "$PWD/$dofile"
fi
}
# 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
# 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="$2" tmp="$3" dopath= dodir= dofile= ext=
local dir="$1" target="$1$2" tmp="$1$2.redo.tmp"
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" "$dir$target" "$plain" >&2
"$green" "$DO_DEPTH" "$bold" "$target" "$plain" >&2
dopath=$(_find_dofile "$target")
if [ ! -e "$dopath" ]; then
echo "do: $target: no .do file ($PWD)" >&2
@ -292,56 +306,60 @@ _do()
target=$PWD/$target
tmp=$PWD/$tmp
cd "$dodir" || return 99
target=$(_relpath "$target") || return 98
tmp=$(_relpath "$tmp") || return 97
target=$(_relpath "$target" "$PWD") || return 98
tmp=$(_relpath "$tmp" "$PWD") || return 97
base=${target%$ext}
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] ||
: >>"$target.did.tmp"
( _run_dofile "$target" "$base" "$tmp.tmp" )
rv=$?
if [ $rv != 0 ]; then
printf "do: %s%s\n" "$DO_DEPTH" \
"$dir$target: got exit code $rv" >&2
rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did"
return $rv
fi
echo "$PWD/$target" >>"$DO_BUILT"
mv "$tmp.tmp" "$target" 2>/dev/null ||
! test -s "$tmp.tmp2" ||
mv "$tmp.tmp2" "$target" 2>/dev/null
# $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 || return
mv "$tmp" "$target" 2>/dev/null
[ -e "$target.did.tmp" ] &&
mv "$target.did.tmp" "$target.did" ||
: >>"$target.did"
rm -f "$tmp.tmp2"
else
_debug "do $DO_DEPTH$target exists." >&2
fi
}
# Make corrections for directories that don't actually exist yet.
_dir_shovel()
{
local dir base
xdir=$1 xbase=$2 xbasetmp=$2
while [ ! -d "$xdir" -a -n "$xdir" ]; do
_dirsplit "${xdir%/}"
xbasetmp=${_dirsplit_base}__$xbasetmp
xdir=$_dirsplit_dir xbase=$_dirsplit_base/$xbase
done
_debug "xbasetmp='$xbasetmp'" >&2
}
# Implementation of the "redo" command.
_redo()
{
local i startdir="$PWD" dir base
set +e
for i in "$@"; do
_dirsplit "$i"
_dir_shovel "$_dirsplit_dir" "$_dirsplit_base"
dir=$xdir base=$xbase basetmp=$xbasetmp
( cd "$dir" && _do "$dir" "$base" "$basetmp" )
i=$(_abspath "$i" "$startdir")
(
cd "$DO_STARTDIR" || return 99
i=$(_normpath "$(_relpath "$i" "$PWD")" "$PWD")
_dirsplit "$i"
dir=$_dirsplit_dir base=$_dirsplit_base
_do "$dir" "$base"
)
[ "$?" = 0 ] || return 1
done
}

View file

@ -102,31 +102,32 @@ 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)
x=$PWD/x
check "a/b/c" _relpath "$x/a/b/c" "$x"
check "../a/b/c" _relpath "$x/../a/b/c" "$x"
check "" _relpath "$x" "$x"
check "a/b/c" _relpath a/b/c "/"
check "a/b/c" _relpath /a/b/c "/"
check "" _relpath / "/"
check "../lib" _relpath /usr/lib "/usr/bin"
check "../lib/" _relpath /usr/lib/ "/usr/bin"
check "../.." _relpath / "/usr/bin"
check ".." _relpath .. "$PWD/fakedir"
check "../" _relpath ../ "$PWD/fakedir"
check "../fakedir" _relpath ../fakedir "$PWD/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 ..
check "/usr/lib" _normpath /usr/../usr/bin/../lib "$x"
check "/" _normpath /a/b/c/../../.. "$x"
check "/" _normpath / "$x"
check "../a" _normpath ../a "$x"
check "../a/b" _normpath ../a/b "$x/fakedir"
check ".." _normpath .. "$x/fakedir"
check "tuv" _normpath a/b/../../tuv "/"
check "" _normpath a/b/../.. "/"
check ".." _normpath ../ "$x"
check ".." _normpath .. "$x"
SECTION _find_dofile