diff options
author | Theo Buehler <tb@cvs.openbsd.org> | 2020-09-13 15:39:10 +0000 |
---|---|---|
committer | Theo Buehler <tb@cvs.openbsd.org> | 2020-09-13 15:39:10 +0000 |
commit | 89bbe9e45ad8deb54d499460a2b47496292969b0 (patch) | |
tree | 2480869f6ea83179e589d23506a960372e407237 | |
parent | 4b1e401314d2ed1b7a892705f5a410aed727d963 (diff) |
Fix "$@" splitting with empty IFS
One uncommon but useful way of writing shell scripts is to start off by
disabling field/word splitting (IFS='') and pathname expansion/globbing
(set -f), re-enabling either or both only for the commands that need
them, e.g. within a subshell. This helps avoid a lot of snags with field
splitting and globbing if you forget to quote a variable somewhere,
adding to the general robustness of a script. (In fact it eliminates
much of the need to quote variable/parameter expansions, with empty
removal remaining as the only issue.)
Unfortunately OpenBSD ksh (like all pdksh variants except mksh) has a
POSIX compliance bug that is a show stopper for this approach: "$@" does
not generate words (arguments) if IFS is empty. As a result, the
separate command arguments represented by "$@" become a single argument.
So passing on an intact set of positional parameters to a command or
function is impossible with field splitting disabled.
Of course this is illogical: the quoted special parameter "$@" generates
zero or more words, it doesn't split any words, so the contents of IFS
(or lack thereof) should be neither here nor there. It's old ksh88
behaviour copied by the original pdksh, but it violates POSIX and it has
been fixed many years ago in ksh93 and all other POSIX shells.
From Martijn Dekker (who also wrote the above paragraphs) back in 2016.
Thanks to Avi Halachmi for reminding us of the issue.
ok czarkoff deraadt kn
-rw-r--r-- | bin/ksh/eval.c | 65 |
1 files changed, 48 insertions, 17 deletions
diff --git a/bin/ksh/eval.c b/bin/ksh/eval.c index ba4b73f404e..2f22c2ee2eb 100644 --- a/bin/ksh/eval.c +++ b/bin/ksh/eval.c @@ -1,4 +1,4 @@ -/* $OpenBSD: eval.c,v 1.65 2019/06/28 13:34:59 deraadt Exp $ */ +/* $OpenBSD: eval.c,v 1.66 2020/09/13 15:39:09 tb Exp $ */ /* * Expansion - quoting, separation, substitution, globbing @@ -47,6 +47,8 @@ typedef struct Expand { #define IFS_WORD 0 /* word has chars (or quotes) */ #define IFS_WS 1 /* have seen IFS white-space */ #define IFS_NWS 2 /* have seen IFS non-white-space */ +#define IFS_IWS 3 /* beginning of word, ignore IFS white-space */ +#define IFS_QUOTE 4 /* beg.w/quote, becomes IFS_WORD unless "$@" */ static int varsub(Expand *, char *, char *, int *, int *); static int comsub(Expand *, char *); @@ -217,7 +219,17 @@ expand(char *cp, /* input word */ c = *sp++; break; case OQUOTE: - word = IFS_WORD; + switch (word) { + case IFS_QUOTE: + /* """something */ + word = IFS_WORD; + break; + case IFS_WORD: + break; + default: + word = IFS_QUOTE; + break; + } tilde_ok = 0; quote = 1; continue; @@ -297,6 +309,8 @@ expand(char *cp, /* input word */ if (f&DOBLANK) doblank++; tilde_ok = 0; + if (word == IFS_QUOTE && type != XNULLSUB) + word = IFS_WORD; if (type == XBASE) { /* expand? */ if (!st->next) { SubType *newst; @@ -358,6 +372,11 @@ expand(char *cp, /* input word */ f |= DOTEMP_; /* FALLTHROUGH */ default: + /* '-' '+' '?' */ + if (quote) + word = IFS_WORD; + else if (dp == Xstring(ds, dp)) + word = IFS_IWS; /* Enable tilde expansion */ tilde_ok = 1; f |= DOTILDE; @@ -387,10 +406,17 @@ expand(char *cp, /* input word */ */ x.str = trimsub(str_val(st->var), dp, st->stype); - if (x.str[0] != '\0' || st->quote) + if (x.str[0] != '\0') { + word = IFS_IWS; type = XSUB; - else + } else if (quote) { + word = IFS_WORD; + type = XSUB; + } else { + if (dp == Xstring(ds, dp)) + word = IFS_IWS; type = XNULLSUB; + } if (f&DOBLANK) doblank++; st = st->prev; @@ -422,6 +448,10 @@ expand(char *cp, /* input word */ if (f&DOBLANK) doblank++; st = st->prev; + if (quote || !*x.str) + word = IFS_WORD; + else + word = IFS_IWS; continue; case '?': { @@ -463,12 +493,8 @@ expand(char *cp, /* input word */ type = XBASE; if (f&DOBLANK) { doblank--; - /* not really correct: x=; "$x$@" should - * generate a null argument and - * set A; "${@:+}" shouldn't. - */ - if (dp == Xstring(ds, dp)) - word = IFS_WS; + if (dp == Xstring(ds, dp) && word != IFS_WORD) + word = IFS_IWS; } continue; @@ -503,7 +529,12 @@ expand(char *cp, /* input word */ if (c == 0) { if (quote && !x.split) continue; + if (!quote && word == IFS_WS) + continue; + /* this is so we don't terminate */ c = ' '; + /* now force-emit a word */ + goto emit_word; } if (quote && x.split) { /* terminate word for "$@" */ @@ -554,15 +585,15 @@ expand(char *cp, /* input word */ * ----------------------------------- * IFS_WORD w/WS w/NWS w * IFS_WS -/WS w/NWS - - * IFS_NWS -/NWS w/NWS w + * IFS_NWS -/NWS w/NWS - + * IFS_IWS -/WS w/NWS - * (w means generate a word) - * Note that IFS_NWS/0 generates a word (at&t ksh - * doesn't do this, but POSIX does). */ - if (word == IFS_WORD || - (!ctype(c, C_IFSWS) && c && word == IFS_NWS)) { - char *p; - + if ((word == IFS_WORD) || (word == IFS_QUOTE) || (c && + (word == IFS_IWS || word == IFS_NWS) && + !ctype(c, C_IFSWS))) { + char *p; + emit_word: *dp++ = '\0'; p = Xclose(ds, dp); if (fdo & DOBRACE_) |