Blogged about many of these:
"${a[i+1]}"${} language ambiguity with ${####} and ${x///}, etc.exec {fd}< input.txt is a terrible syntax for fd = open('input.txt')test builtin ambiguityOther:
unset a readonly variable just causes a status 1, which can be ignored. Need errexit
to make it a hard failure.continue, it prints an error, but might continue anyway (dash/mksh/zsh) or exit the shell (bash)errexit problems -- subshell/command sub, local -- two different issuesgetopts builtin is implemented in all shells, but OPTIND is a global variable with wildly diverging behavior. There's no reliable way to tell when it should be reset, because getopts is called in a loop. This is a fundamental design flaw.
OPTARG and the second opt argumenteval and echo shouldn't implicitly join multiple args -- this is a confusion of strings and arrays
trap shouldn't take a string to be eval'd? Why not the name of a function?(( a = b )) is assignment of variable names(( a == b )) is equality of variable names[ a = b ](-a-=-b-.html) is equality of strings, like [ 'a' == 'b' ](-'a'-==-'b'-.html)[ a == b ](-a-==-b-.html) is equality of strings, like [ 'a' == 'b' ](-'a'-==-'b'-.html)0 in the arithmetic contexta+=b vs. (( a += b ))type-compat.test.sh -- horrible runtime parsing of array declarations pointed out by Nix devs
a=() and declare +a myarray=()shopt -s simple_word_eval fixes this in Oil.$* "$*" $@ "$@" are not orthogonal. You never need $* and $@. "$*" joins by IFS?EOF vs 'EOF' / "EOF" / \EOF -- this is a very hacky rule. The thing that's easiest to implement.getopts leading : for error handling is hackyread shouldn't return 1 on lack of newline -- it still modified the variable[ foo.py == *.py ](-foo.py-==-*.py-.html) shouldn't do globbing, should be a different operator[ foo =~ (.*) ](-foo-=~-(.*)-.html) -- no quotes needed, in fact no quotes allowed!
( ) | chars are special${myarray} is the same as ${myarray[0]}${mystr[@]} is silently allowed[[ "${a[@]}" == "${b[@]}" ]] doesn't workmksh.* in *(a*|b)[ !(a == a) ](-!(a-==-a)-.html) -- is it a negation of an equality test, or an extended glob? See doc/osh-manual.md.*.py without *_test.py with extended glob: echo */!(*_test).py
!(*_test) is like a negative lookahead and then .* ?${!ref} quirks: set -u is respected with strings, but not arrays! and @ syntax:
${!ref} is a var ref, ${a[@]} is an array, but ${!a[@]} is not a ref to an array! It means something totally different.set -eou pipefail is a very confusing syntax to parse. set -oo or set -ee.[[, and then at runtime test / [IFS is used with two different algorithms: splitting a line for read, and "splicing" an unquoted word into
an argv array. POSIX says thay are related, but in practice they seem different? At the very least, one supports
backslash escaping and the other doesn't (read -r). Or you can look at it a different way: one supports
quotes AND backslashes; the other supports just backslashes.echo -e '\0377' and echo $'\377'. FWIW C is the latter -- don't need
a leading zero, and Python uses it.$PS4 is treated differently$IFS are treated differently, depending on whether they're whitespace or not.break or continue in a subshell in a loop is syntactically valid, but doesn't do what it looks like because of the process boundary. (from Connor at RC, see spec/loop.test.sh)FOO=$(ls /OOPS 2>/dev/null) vs.FOO=$(ls /OOPS) 2>/dev/nullFOO=bar cd and FOO=bar eval (special)exec even appears to be a special casetest/syscall);. But it doesn't abort across lines.$(true). From help-bash@.echo "${x:-"a*b"}""${x:-'default'}" -- single quotes are literals"${x#'glob'}" and "${x//'glob'}" -- single quotes are processed by the shell{1..7..2} and {1..7..-2} are the same thing, but not
in zsh.EvalWordSequence()):
EvalWordToString())
x=$wordcase $word in $pat) ...echo hi > $word@words in the "sequence of strings" context, but not in the "string" contextset without args shows VARIABLES, even though the set builtin sets shell options, not variables!
set -o shows the options, with confusing/inconsistent syntaxprintf
%c to get a char doesn't respect unicode; it will slice a UTF-8 character, producing binary garbage%6.4s is overspecified -- %6s is the same%6.4d does something weird -- it pads with zeros AND spaces. It doesn't mean "width" and "precision".[[] is a single left bracket. Conflicts with [:alpha:](:alpha:.html). User should write [\[] instead.
[]] should be [\]].${#s} or ${s:1:3}, invalid utf-8 causes nonsensical results to be returned. No errors are reported.BASH_SOURCE is off by one from FUNCNAME and BASH_LINENO This is documented but makes no sense! Sort of like the parsing of regexes after =~.${!indirect}, !(foo|bar), etc.
bashline.c I believe${arr[@]::} means length zero, while ${arr[@]: } means length N -- empty expression is zero, unset expression is N
TODO: organize the criticisms in these categories:
(( a == b )) vs [ a == b ](-a-==-b-.html) (although they differ slightly)s=1+2; [ $s -eq 3 ](-$s--eq-3-.html)echo -e '\n' and printf '\n' "\n" vs. $'\n'type-compat.test.sh)shopt -s extglob changes the parsing algorithm, and it doesn't work on the same line!!!
bash -c 'shopt -s extglob; echo @(a|b)'nullglob and simple-word-eval)echo -e \x is NUL in mksh and zsh, but \x in bash. It's a syntax error in C. Shell generally has
the "keep going" mindset of JavaScript/PHP/Perl, which makes it hard to use.\1 -- should be a syntax error. Or even \d should be \\d.Escaping constructs: \, 'single quotes', "double quotes", and $'C-style strings'
CompoundWord to glob() or fnmatch() input, which allows \ escaping but not double quoting.CompoundWord to regcomp() input, where characters like [ are special too\ escape in read without -r\n outside of double quotes evalutes to n. Inside double quotes, it's \n (which is the same as the behavior inside single quotes). Note that neither evalutes to a newline! That only happens with $'\n'.$(command subs) is different than that of backticks, e.g. with respect to double quotes and other backticks. This is very confusing and shell behaviors diverge once you have 2 or 3 levels of quoting.BASH_REGEX and REGEX_CHARS lexer modes. This is orthogonal to the regcomp() algorithm
[[ foo =~ [ab]<>(foo|bar) ]] ???set -e vs set +e, declare -a vs. declare +ashopt -s vs shopt -uexport vs. export -n -- remove the export bitalias and unalias are opposites
set and unset aren't opposites! One sets options and argv. The other unsets variables.echo -e vs echo -Eset -- prints functions
readonly, export -- prints vars with those properties-p arg:
declare -pshopt -p -- prints both set and shopt optionsalias -ptest -- no reason for this other than speed?time -- because it should be a block? But you could do this with a more general mechanismkill -- for job specsprintf -- don't see a reason for thisgetopts -- tighter integration, because we want to mutate shell variables. Doesn't behave like a builtin, but has the syntax of one.read -n, echo -n, etc.date +%m vs date +%M -- I can never remember which. I don't know what + means either.tar xvzf foo.tar.gz can just be tar -x -v -z < foo.tar.gz
tar --verbose --extract --gzip < foo.tar.gzSee Unix Tools
A questionable Pattern? These builtins don't behave like external commands because they can mutate memory.
read varnamegetopts SPEC varnameprintf -v name '%s' value