From fb388b3ddeb84cb897673f3a22d3b314886716e8 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Tue, 21 Dec 2010 04:19:50 -0800 Subject: [PATCH] Automatically select a good shell instead of relying on /bin/sh. This includes a fairly detailed test of various known shell bugs from the autoconf docs. The idea here is that if redo works on your system, you should be able to rely on a *good* shell to run your .do files; you shouldn't have to work around zillions of bugs like autoconf does. --- .gitignore | 1 + _all.do | 2 + all.do | 2 +- builder.py | 3 +- clean.do | 2 +- install.do | 6 +- redo-sh.do | 45 +++++++++ t/.gitignore | 2 + t/clean.do | 2 +- t/shelltest.do | 8 ++ t/shelltest.od | 261 +++++++++++++++++++++++++++++++++++++++++++++++++ t/test.do | 2 +- test.do | 2 +- vars_init.py | 13 ++- 14 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 _all.do create mode 100644 redo-sh.do create mode 100644 t/shelltest.do create mode 100644 t/shelltest.od diff --git a/.gitignore b/.gitignore index d0aea09..4a9ab4a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ t/LD t/[yb]ellow t/hello t/*.o +/redo-sh diff --git a/_all.do b/_all.do new file mode 100644 index 0000000..6623acb --- /dev/null +++ b/_all.do @@ -0,0 +1,2 @@ +redo-ifchange redo-sh +redo-ifchange Documentation/all diff --git a/all.do b/all.do index 340cd9f..af2187f 100644 --- a/all.do +++ b/all.do @@ -1,2 +1,2 @@ -redo-ifchange Documentation/all +redo-ifchange _all echo "Nothing much to do. Try 'redo t/all' or 'redo test'" >&2 diff --git a/builder.py b/builder.py index 3d942a7..81a6c5c 100644 --- a/builder.py +++ b/builder.py @@ -197,7 +197,8 @@ class BuildJob: after_t = _try_stat(t) st1 = os.fstat(f.fileno()) st2 = _try_stat(self.tmpname2) - if after_t != before_t and not stat.S_ISDIR(after_t.st_mode): + if (after_t and after_t != before_t and + not stat.S_ISDIR(after_t.st_mode)): err('%s modified %s directly!\n' % (self.argv[2], t)) err('...you should update $3 (a temp file) or stdout, not $1.\n') rv = 206 diff --git a/clean.do b/clean.do index 7753332..ec75b2a 100644 --- a/clean.do +++ b/clean.do @@ -6,5 +6,5 @@ fi [ -z "$DO_BUILT" ] && rm -rf .do_built .do_built.dir redo t/clean Documentation/clean rm -f *~ .*~ */*~ */.*~ *.pyc install.wrapper -rm -rf t/.redo +rm -rf t/.redo redo-sh find . -name '*.tmp' -exec rm -fv {} \; diff --git a/install.do b/install.do index 3703131..47889ae 100644 --- a/install.do +++ b/install.do @@ -1,5 +1,5 @@ exec >&2 -redo-ifchange Documentation/all +redo-ifchange _all : ${INSTALL:=install} : ${DESTDIR:=} @@ -28,6 +28,10 @@ for d in *.py; do done python -mcompileall $LIBDIR +# It's important for the file to actually be named 'sh'. Some shells (like +# bash and zsh) only go into POSIX-compatible mode if they have that name. +cp -R redo-sh/sh $LIBDIR/sh + # binaries for dd in redo*.py; do d=$(basename $dd .py) diff --git a/redo-sh.do b/redo-sh.do new file mode 100644 index 0000000..9ca1879 --- /dev/null +++ b/redo-sh.do @@ -0,0 +1,45 @@ +exec >&2 +redo-ifchange t/shelltest.od + +rm -rf $1.new $1/sh +mkdir $1.new + +GOOD= +WARN= + +for sh in dash sh ash ksh pdksh bash zsh busybox; do + printf "Testing %s... " "$sh" + FOUND=`which $sh` || { echo "missing"; continue; } + + # It's important for the file to actually be named 'sh'. Some shells (like + # bash and zsh) only go into POSIX-compatible mode if they have that name. + # If they're not in POSIX-compatible mode, they'll fail the test. + rm -f $1.new/sh + ln -s $FOUND $1.new/sh + + set +e + ( cd t && ../$1.new/sh shelltest.od >/dev/null 2>&1 ) + RV=$? + set -e + + case $RV in + 0) echo "good"; GOOD=$FOUND; break ;; + 42) echo "warnings"; [ -n "$WARN" ] || WARN=$FOUND ;; + *) echo "failed" ;; + esac +done + +rm -rf $1 $1.new $3 + +if [ -n "$GOOD" ]; then + echo "Selected perfect shell: $GOOD" + mkdir $3 + ln -s $GOOD $3/sh +elif [ -n "$WARN" ]; then + echo "Selected mostly good shell: $WARN" + mkdir $3 + ln -s $WARN $3/sh +else + echo "No good shells found! Maybe install dash, bash, or zsh." + exit 1 +fi diff --git a/t/.gitignore b/t/.gitignore index 60b3c7a..375c938 100644 --- a/t/.gitignore +++ b/t/.gitignore @@ -21,3 +21,5 @@ test2.args /ifcreate[12].log /ifcreate[12].dep /ifcreate[12] +/broken +/shellfile diff --git a/t/clean.do b/t/clean.do index 3062fb3..1b3b8b4 100644 --- a/t/clean.do +++ b/t/clean.do @@ -1,6 +1,6 @@ redo example/clean curse/clean deps/clean "space dir/clean" stamp/clean \ defaults-flat/clean -rm -f mode1 makedir.log chdir1 deltest2 \ +rm -f broken shellfile mode1 makedir.log chdir1 deltest2 \ hello [by]ellow *.o *~ .*~ *.log CC LD passfail silence silence.do \ touch1 touch1.do always1 ifcreate[12].dep ifcreate[12] rm -rf makedir diff --git a/t/shelltest.do b/t/shelltest.do new file mode 100644 index 0000000..5c65254 --- /dev/null +++ b/t/shelltest.do @@ -0,0 +1,8 @@ +set +e +( . ./shelltest.od ) +RV=$? +case $RV in + 0) exit 0 ;; + 42) exit 0 ;; + *) exit 1 ;; +esac diff --git a/t/shelltest.od b/t/shelltest.od new file mode 100644 index 0000000..e4cfc1a --- /dev/null +++ b/t/shelltest.od @@ -0,0 +1,261 @@ +# +# Most of these tests were inspired by: +# http://www.gnu.org/software/hello/manual/autoconf/Shell-Substitutions.html +# +exec >&2 +set +e +FAIL= +fail() +{ + echo " failed: $1" + FAIL=41 +} +warn() +{ + echo " warning: $1" + [ -n "$FAIL" ] || FAIL=42 +} + + +name=foo.o.o +ext=.o +[ "${name#foo.o}" = ".o" ] || fail 3 + + +spacey="this has * and spaces" +case $spacey in + *) spaceout=$name$spacey ;; +esac +[ "$spaceout" = "$name$spacey" ] || fail 4 + + +n() { echo "$#$@"; } +f=" - " +out=$(n - ""$f"" -) +[ "$out" = "5- - -" ] || warn 5 + + +n1() { echo $#; } +n2() { n1 "$@"; } +t1=$(n1) +t2=$(n2) +[ "$t1" = "0" ] || fail 6 +[ "$t2" = "0" ] || fail 7 + + +n1() { for i in "$@"; do echo $i; done; } +n2() { for i in ${1+"$@"}; do echo $i; done; } +t1=$(n1 "Hello World" "!") +t2=$(n2 "Hello World" "!") +WANT="Hello World +!" +[ "$t1" = "$WANT" ] || fail 8 +[ "$t2" = "$WANT" ] || fail 9 + + +n() { echo ${10}; } +t1=$(n 1 2 3 4 5 6 7 8 9 xx yy) +[ "$t1" = "xx" ] || fail 10 + + +chicken1=`echo " $spacey" | sed s/a/./g` +chicken2="`echo " $spacey" | sed s/a/./g`" +chicken3=$(echo " $spacey" | sed s/a/./g) +chicken4="$(echo " $spacey" | sed s/a/./g)" +[ "$chicken1" = " this h.s * .nd sp.ces" ] || fail 11 +[ "$chicken2" = " this h.s * .nd sp.ces" ] || fail 12 +[ "$chicken3" = " this h.s * .nd sp.ces" ] || fail 13 +[ "$chicken4" = " this h.s * .nd sp.ces" ] || fail 14 + + +f1= +f2=goo +g1= +g2=goo +out=$(echo ${f1:-foo} ${f2:-foo} ${g1:=foo} ${g2:=foo}) +: ${f1:-roo} ${f2:-roo} ${g1:=roo} ${g2:=roo} +[ "$out" = "foo goo foo goo" ] || fail 16 +[ "$f1$f2$g1$g2" = "gooroogoo" ] || fail 17 + + +unset a +t1=$(echo ${a-b c}) +t2=$(echo ${a-'b c'}) +t3=$(echo "${a-b c}") +t4=$(echo "${a-"b c"}") +t5=$(cat <broken +echo "`printf 'foo\r\n'`"" bar" | diff -q - broken || fail 59 + + +# +# This one is too obnoxious. dash and ash pass the test, but nothing else does, +# and this case is just too dumb to care about. Just don't do that! +# +#t=`echo $(case x in x) echo hello;; esac)` +#[ "$t" = "hello" ] || fail 60 + + +x=5 +t1=$(($x + 4)) +t2=$(echo $(( 010 + 0x10 ))) +[ "$t1" = "9" ] || fail 61 +[ "$t2" = "24" ] || fail 62 + + +t=$(echo hello ^ cat) +[ "$t" = "hello ^ cat" ] || fail 65 + + +t1=$(for d in this-glob-does-*-not-exist; do echo "$d"; done) +t2=$(for d in this-glob-does-*-not-exist; do echo "$d"; done) + + +# http://www.gnu.org/software/hello/manual/autoconf/Assignments.html +false || foo=bar; [ "$?" = 0 ] || fail 71 +foo=`exit 1`; [ "$?" != 0 ] || fail 72 + +# http://www.gnu.org/software/hello/manual/autoconf/Shell-Functions.html +f1() { echo 1; } +f2(){ echo 2;} +f3()(echo 3) +f4()if true; then echo 4; fi +f5() ( exit 5 ) +[ "$(f1)" = 1 ] || fail 81 +[ "$(f2)" = 2 ] || fail 82 +[ "$(f3)" = 3 ] || fail 83 +[ "$(f4)" = 4 ] || fail 84 +f5 && fail 85 +f6() ( + f6b() { return 1; } + set -e + f6b + fail 86 +) +f6 +f7() { :; }; f7=; f7 || fail 87 +a= +f8() { echo $a; }; +t8=$(a=1 f8) +[ "$t8" = "1" ] || fail 88 + + +# http://www.gnu.org/software/hello/manual/autoconf/Limitations-of-Builtins.html +. /dev/null || fail 90 +(! : | :) && fail 91 || true +(! { :; }) && fail 92 || true +t3=none +case frog.c in + (*.c) t3=c ;; + (*) t3=all ;; +esac +[ "$t3" = "c" ] || fail 93 +t4=$(echo '\n' | wc -l) +[ "$t4" -eq 1 ] || warn 94 +f5() { + for arg; do + echo $arg + done +} +t5=$(f5 a=5 b c) +[ "$t5" = "a=5 +b +c" ] || fail 95 +t6=$(printf -- '%d %d' 5 6) +[ "$t6" = "5 6" ] || fail 96 +echo 'word\ game stuff' >shellfile +read t7a t7b &2 -redo deltest deltest2 test.args test2.args passfailtest chdirtest \ +redo shelltest deltest deltest2 test.args test2.args passfailtest chdirtest \ curse/test deps/test "space dir/test" modetest makedir2 \ silencetest touchtest stamp/test alwaystest ifcreate-test diff --git a/test.do b/test.do index 4744d97..1c272d4 100644 --- a/test.do +++ b/test.do @@ -1,2 +1,2 @@ -redo-ifchange Documentation/all +redo-ifchange _all redo t/test diff --git a/vars_init.py b/vars_init.py index 1c78ca4..75a813b 100644 --- a/vars_init.py +++ b/vars_init.py @@ -5,10 +5,17 @@ def init(targets): # toplevel call to redo exenames = [os.path.abspath(sys.argv[0]), os.path.realpath(sys.argv[0])] - if exenames[0] == exenames[1]: - exenames = [exenames[0]] dirnames = [os.path.dirname(p) for p in exenames] - os.environ['PATH'] = ':'.join(dirnames) + ':' + os.environ['PATH'] + trynames = ([os.path.abspath(p+'/../lib/redo') for p in dirnames] + + [p+'/redo-sh' for p in dirnames] + + dirnames) + seen = {} + dirs = [] + for k in trynames: + if not seen.get(k) and os.path.exists('%s/.' % k): + seen[k] = 1 + dirs.append(k) + os.environ['PATH'] = ':'.join(dirs) + ':' + os.environ['PATH'] os.environ['REDO'] = os.path.abspath(sys.argv[0]) if not os.environ.get('REDO_BASE'):