From 47258fc9a943e3618e8e6fc637eabe83dda3db28 Mon Sep 17 00:00:00 2001 From: Rob Landley Date: Fri, 24 Sep 2021 21:19:19 -0500 Subject: [PATCH] Add += prefix and direct assignment (not in export x+=y and friends yet), add tests, and update TEST_HOST tests that bash 5.x broke. --- tests/sh.test | 51 +++++++++++++++++++++++++++++++++++++---------- toys/pending/sh.c | 27 +++++++++++++++++-------- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/tests/sh.test b/tests/sh.test index 947e3ae7..f886b705 100644 --- a/tests/sh.test +++ b/tests/sh.test @@ -120,9 +120,10 @@ rm -rf sub testing "script file" "chmod +x input; ./input" "hello\n" "#!$C\necho hello" "" testing 'IFS $*' "$SH -c 'IFS=xy; echo \"\$*\"' one two tyree" "twoxtyree\n" \ "" "" +# Note: bash version skew, SHLVL=1 in earlier bash versions testing 'default exports' \ "env -i \"$(which $SH)\" --noprofile --norc -c env | sort" \ - "PWD=$(pwd)\nSHLVL=1\n_=$(which env)\n" "" "" + "PWD=$(pwd)\nSHLVL=0\n_=$(which env)\n" "" "" # toysh order of operations not matching bash #testing "leading assignment fail" \ # "{ \$SH -c 'X=\${a?blah} > walroid';ls walroid;} 2>/dev/null" '' '' '' @@ -136,6 +137,8 @@ export EVAL="$SH -c" testing "smoketest" "echo hello" "hello\n" "" "" testing "line break" $'ec\\\nho hello' 'hello\n' '' '' +testing "assignment" 'x=y; echo $x' 'y\n' '' '' +testing "+= assignment" 'x=abc; y=def; y+=$x; echo $y' 'defabc\n' '' '' testing "eval" "eval echo hello" "hello\n" "" "" testing "eval2" "eval 'echo hello'; echo $?" "hello\n0\n" "" "" testing "eval3" 'X="echo hello"; eval "$X"' "hello\n" "" "" @@ -211,13 +214,18 @@ touch www testing 'wildcards' 'echo w[v-x]w w[x-v]w abc/*/ghi' \ 'www w[x-v]w abc/def/ghi\n' '' '' -#testing "backtick1" 'X=fred; echo `echo $x`' 'fred\n' "" "" -#testing "backtick2" 'X=fred; echo `x=y; echo $x`' 'y\n' "" "" +testing "backtick1" 'x=fred; echo `echo $x`' 'fred\n' "" "" +testing "backtick2" 'x=fred; echo `x=y; echo $x`; echo $x' 'y\nfred\n' "" "" testing '$(( ) )' 'echo ab$((echo hello) | tr e x)cd' "abhxllocd\n" "" "" -testing '$((x=y)) lifetime' 'a=boing; echo $a $a$((a=4))$a $a' 'boing boing44 4\n' '' '' +SKIPNEXT=1 testing '$((x=y)) lifetime' 'a=boing; echo $a $a$((a=4))$a $a' 'boing boing44 4\n' '' '' testing 'quote' "echo \"'\"" "'\n" "" "" +testing "math" 'echo $((1+2))' '3\n' '' '' +testing "[math]" 'echo $[1+2]' '3\n' '' '' +testing "math prio" 'echo $((1+2*3))' '7\n' '' '' +testing "math paren" 'echo $(((1+2)*3))' '9\n' '' '' + # Loops and flow control testing "case" 'for i in A C J B; do case "$i" in A) echo got A ;; B) echo and B ;; C) echo then C ;; *) echo default ;; esac; done' \ "got A\nthen C\ndefault\nand B\n" "" "" @@ -267,12 +275,14 @@ testing "{a..z..-3}" "echo {a..z..-3}" "a d g j m p s v y\n" "" "" mkfifo POIT testing 'background curly block' \ - '{ sed s/ll/xx/ POIT; }& echo hello > POIT; wait' 'hexxo\n' '' '' + '{ sed s/ll/xx/ POIT; }& echo hello > POIT; wait && echo yes' \ + 'hexxo\nyes\n' '' '' rm -f POIT testing 'background pipe block' \ 'if true; then { sleep .25;bzcat "$FILES"/blkid/ntfs.bz2; }& fi | wc -c' \ '8388608\n' '' '' +testing 'background variable assignment' 'X=x; X=y & echo $X' 'x\n' '' '' #$ IFS=x X=xyxz; for i in abc${X}def; do echo =$i=; done #=abc= @@ -346,7 +356,8 @@ testing "! syntax err" '! echo 2>/dev/null < doesnotexist; echo $?' "0\n" "" "" testing "case quoting" 'case a in "a") echo hello;; esac' 'hello\n' "" "" testing "subshell splitting" 'for i in $(true); do echo =$i=; done' "" "" "" -#testing "subshell split 2" +testing "subshell split 2" 'for i in $(echo "one two thr"); do echo =$i=; done'\ + "=one=\n=two=\n=thr=\n" "" "" # variable assignment argument splitting only performed for "$@" testing "assignment nosplit" 'X="one two"; Y=$X; echo $Y' "one two\n" "" "" @@ -425,7 +436,8 @@ testing '${!x*}' 'abcdef=1 abc=2 abcq=; echo "${!abc@}" | tr " " \\n | sort' \ 'abc\nabcdef\nabcq\n' '' '' testing '${!x*} none' 'echo "${!abc*}"' '\n' '' '' testing '${!x*} err' '{ echo "${!abc*x}"; echo boing;} 2>/dev/null' '' '' '' -testing '${!none@Q}' 'echo ${X@Q} ${!X@Q}; X=ABC; echo ${!X@Q}' '\n\n' '' '' +# TODO bash 5.x broke this +#testing '${!none@Q}' 'echo ${X@Q} ${!X@Q}; X=ABC; echo ${!X@Q}' '\n\n' '' '' testing '${!x@Q}' 'ABC=123 X=ABC; echo ${!X@Q}' "'123'\n" '' '' testing '${#@Q}' 'echo ${#@Q}' "'0'\n" '' '' testing '${!*}' 'xx() { echo ${!*};}; fruit=123; xx fruit' '123\n' '' '' @@ -433,7 +445,8 @@ testing '${!*} indirect' 'xx() { echo ${!a@Q};}; a=@; xx one two three' \ "'one' 'two' 'three'\n" '' '' testing '${!x@ } match' \ '{ ABC=def; def=ghi; echo ${!ABC@ }; } 2>&1 | grep -o bad' 'bad\n' '' '' -testing '${!x@ } no match no err' 'echo ${!ABC@ }def' 'def\n' '' '' +# Bash added an error for this between 4.4 and 5.x. +#testing '${!x@ } no match no err' 'echo ${!ABC@ }def' 'def\n' '' '' testing '${!x@ } no match no err2' 'ABC=def; echo ${!ABC@ }ghi' 'ghi\n' '' '' toyonly testing '${#x::}' 'ABC=abcdefghijklmno; echo ${#ABC:1:2}' '5\n' '' '' # TODO: ${!abc@x} does _not_ error? And ${PWD@q} @@ -446,8 +459,9 @@ testing '${a: }' 'ABC=def; echo ${ABC: 1}' 'ef\n' '' '' testing '${a :}' 'ABC=def; { echo ${ABC :1};} 2>&1 | grep -o bad' 'bad\n' '' '' testing '${::}' 'ABC=defghi; echo ${ABC:1:2}' 'ef\n' '' '' testing '${: : }' 'ABC=defghi; echo ${ABC: 1 : 2 }' 'ef\n' '' '' -testing '${::} indirect' 'ABC=defghi:1:2; { echo ${!ABC};} 2>&1 | grep -o bad' \ - 'bad\n' '' '' +testing '${::} indirect' \ + 'ABC=defghi:1:2; ( echo ${!ABC};) 2>input; [ -s input ] && echo yes' \ + 'yes\n' '' '' testing '${::-}' 'ABC=defghi; echo ${ABC:1:-2}' 'efg\n' '' '' testing '${:-:-}' 'ABC=defghi; echo ${ABC:-3:2}' 'defghi\n' '' '' testing '${:-:-}2' 'echo ${ABC:-3:2}' '3:2\n' '' '' @@ -546,13 +560,28 @@ testing 'functions() () different PID' \ testing 'function() just wants any block span' \ 'func() if true; then echo hello; fi; echo one; func; echo two' \ 'one\nhello\ntwo\n' '' '' +testing 'function alternate syntax' \ + 'function func if true; then echo hello; fi; echo one; func; echo two' \ + 'one\nhello\ntwo\n' '' '' +testing 'function syntax 3' \ + 'function func ( ) if true; then echo hello; fi; echo one; func; echo two' \ + 'one\nhello\ntwo\n' '' '' +testing 'function nested parentheses' \ + '( potato() { echo aaa; }; potato )' 'aaa\n' '' '' shxpect 'local creates a whiteout' \ I$'func() { local potato; echo ${potato?bang}; }; potato=123; func\n' \ E E"$P" I$'echo $?\n' O$'1\n' +testing '$$ is parent shell' \ + '{ echo $$; (echo $$) } | sort -u | wc -l' "1\n" "" "" +testing '$PPID is parent shell' \ + '{ echo $PPID; (echo $PPID) } | sort -u | wc -l' "1\n" "" "" +testing '$BASHPID is current PID' \ + '{ echo $BASHPID; (echo $BASHPID) } | sort -u | wc -l' "2\n" "" "" + # TODO finish variable list from shell init -# $# $? $- $$ $! $0 +# $# $? $- $! $0 # $$ # always exported: PWD SHLVL _ # ./bash -c 'echo $_' prints $BASH, but PATH search shows path? Hmmm... # ro: UID PPID EUID $ diff --git a/toys/pending/sh.c b/toys/pending/sh.c index ee8b16cb..6b7736dd 100644 --- a/toys/pending/sh.c +++ b/toys/pending/sh.c @@ -401,9 +401,9 @@ static struct sh_vars *findvar(char *name, struct sh_fcall **pff) if (!var) continue; if (pff) *pff = ff; - while (var-- != ff->vars) + while (var--!=ff->vars) if (pff || !(var->flags&VAR_WHITEOUT)) - if (!strncmp(var->str, name, len) && var->str[len] == '=') return var; + if (!strncmp(var->str, name, len) && var->str[len]=='=') return var; } while ((ff = ff->next)!=TT.ff); return 0; @@ -2379,14 +2379,14 @@ static void sh_exec(char **argv) // Execute a single command at TT.ff->pl static struct sh_process *run_command(void) { - char *s, *sss; + char *s, *ss, *sss; struct sh_arg *arg = TT.ff->pl->arg; int envlen, funk = TT.funcslen, jj = 0, locals = 0; struct sh_process *pp; // Count leading variable assignments for (envlen = 0; envlenc; envlen++) - if ((s = varend(arg->v[envlen])) == arg->v[envlen] || *s != '=') break; + if ((s = varend(arg->v[envlen]))==arg->v[envlen] || s[*s=='+']!='=') break; pp = expand_redir(arg, envlen, 0); // Are we calling a shell function? TODO binary search @@ -2403,7 +2403,7 @@ static struct sh_process *run_command(void) pp->delete = 0; } addvar(0, TT.ff); // function context (not source) so end_function deletes - locals = 1; + locals = 1; // create local variables for function prefix assignment } // perform any assignments @@ -2416,13 +2416,24 @@ static struct sh_process *run_command(void) else if (vv->flags&VAR_READONLY) ff = 0; else if (locals && ff!=TT.ff) vv = 0, ff = TT.ff; - if (!vv&&ff) (vv = addvar(s, ff))->flags = VAR_NOFREE|(VAR_GLOBAL*locals); if (!(sss = expand_one_arg(s, SEMI_IFS, 0))) pp->exit = 1; else { - if (!setvar_found(sss, vv)) continue; + ss = varend(sss); + if (!vv&&ff) + (vv = addvar(s, ff))->flags = VAR_NOFREE|(VAR_GLOBAL*locals); + else if (*ss=='+') { + ss = xmprintf("%s%s", vv->str, ss+2); + free(sss); + sss = ss; + } + + if (!setvar_found(sss, vv)) { + if (sss!=s) free(sss); + continue; + } if (sss==s) { if (!locals) vv->str = xstrdup(sss); - else vv->flags |= VAR_NOFREE; + else vv->flags |= VAR_NOFREE; // argument mem outlives command } cache_ifs(vv->str, ff ? : TT.ff); } -- 2.39.2