From d2241f59022c38d4b171e56eea42e216ecccfdd9 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Sat, 31 Oct 2020 03:34:07 +0100 Subject: shell: better support of [[ ]] bashism Still rather rudimentary for ash function old new delta binop 433 589 +156 check_operator 65 101 +36 done_word 736 769 +33 test_main 405 418 +13 parse_stream 2227 2238 +11 ops_texts 124 133 +9 ops_table 80 86 +6 run_pipe 1557 1562 +5 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 8/0 up/down: 269/0) Total: 269 bytes Signed-off-by: Denys Vlasenko --- coreutils/test.c | 82 ++++++++++++++++++++++++++++++++ shell/ash.c | 15 +++--- shell/hush.c | 57 +++++++++++++++------- shell/hush_test/hush-test2/andor1.right | 6 +++ shell/hush_test/hush-test2/andor1.tests | 7 +++ shell/hush_test/hush-test2/noglob1.right | 2 + shell/hush_test/hush-test2/noglob1.tests | 3 ++ shell/hush_test/hush-test2/strops1.right | 8 ++++ shell/hush_test/hush-test2/strops1.tests | 15 ++++++ shell/hush_test/hush-test2/strops2.right | 6 +++ shell/hush_test/hush-test2/strops2.tests | 12 +++++ shell/hush_test/hush-test2/strops3.right | 7 +++ shell/hush_test/hush-test2/strops3.tests | 13 +++++ 13 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 shell/hush_test/hush-test2/andor1.right create mode 100755 shell/hush_test/hush-test2/andor1.tests create mode 100644 shell/hush_test/hush-test2/noglob1.right create mode 100755 shell/hush_test/hush-test2/noglob1.tests create mode 100644 shell/hush_test/hush-test2/strops1.right create mode 100755 shell/hush_test/hush-test2/strops1.tests create mode 100644 shell/hush_test/hush-test2/strops2.right create mode 100755 shell/hush_test/hush-test2/strops2.tests create mode 100644 shell/hush_test/hush-test2/strops3.right create mode 100755 shell/hush_test/hush-test2/strops3.tests diff --git a/coreutils/test.c b/coreutils/test.c index a08986130..ac7b546a3 100644 --- a/coreutils/test.c +++ b/coreutils/test.c @@ -76,6 +76,8 @@ //usage: "1\n" #include "libbb.h" +#include +#include /* This is a NOFORK applet. Be very careful! */ @@ -146,6 +148,14 @@ #define TEST_DEBUG 0 +#if ENABLE_TEST2 \ + || (ENABLE_ASH_BASH_COMPAT && ENABLE_ASH_TEST) \ + || (ENABLE_HUSH_BASH_COMPAT && ENABLE_HUSH_TEST) +# define BASH_TEST2 1 +#else +# define BASH_TEST2 0 +#endif + enum token { EOI, @@ -184,6 +194,10 @@ enum token { STRLT, STRGT, +#if BASH_TEST2 + REGEX, +#endif + INTEQ, /* int ops */ INTNE, INTGE, @@ -257,6 +271,9 @@ static const char *const TOKSTR[] = { "STRNE", "STRLT", "STRGT", +#if BASH_TEST2 + "REGEX", +#endif "INTEQ", "INTNE", "INTGE", @@ -320,6 +337,9 @@ static const struct operator_t ops_table[] = { { /* "!=" */ STRNE , BINOP }, { /* "<" */ STRLT , BINOP }, { /* ">" */ STRGT , BINOP }, +#if BASH_TEST2 + { /* "=~" */ REGEX , BINOP }, +#endif { /* "-eq"*/ INTEQ , BINOP }, { /* "-ne"*/ INTNE , BINOP }, { /* "-ge"*/ INTGE , BINOP }, @@ -332,6 +352,10 @@ static const struct operator_t ops_table[] = { { /* "!" */ UNOT , BUNOP }, { /* "-a" */ BAND , BBINOP }, { /* "-o" */ BOR , BBINOP }, +#if BASH_TEST2 + { /* "&&" */ BAND , BBINOP }, + { /* "||" */ BOR , BBINOP }, +#endif { /* "(" */ LPAREN , PAREN }, { /* ")" */ RPAREN , PAREN }, }; @@ -365,6 +389,9 @@ static const char ops_texts[] ALIGN1 = "!=" "\0" "<" "\0" ">" "\0" +#if BASH_TEST2 + "=~" "\0" +#endif "-eq" "\0" "-ne" "\0" "-ge" "\0" @@ -377,6 +404,10 @@ static const char ops_texts[] ALIGN1 = "!" "\0" "-a" "\0" "-o" "\0" +#if BASH_TEST2 + "&&" "\0" + "||" "\0" +#endif "(" "\0" ")" "\0" ; @@ -397,6 +428,9 @@ struct test_statics { const struct operator_t *last_operator; gid_t *group_array; int ngroups; +#if BASH_TEST2 + bool bash_test2; +#endif jmp_buf leaving; }; @@ -408,6 +442,7 @@ extern struct test_statics *const test_ptr_to_statics; #define last_operator (S.last_operator) #define group_array (S.group_array ) #define ngroups (S.ngroups ) +#define bash_test2 (S.bash_test2 ) #define leaving (S.leaving ) #define INIT_S() do { \ @@ -501,6 +536,20 @@ static enum token check_operator(const char *s) n = index_in_strings(ops_texts, s); if (n < 0) return OPERAND; + +#if BASH_TEST2 + if (ops_table[n].op_num == REGEX && !bash_test2) { + /* =~ is only for [[ ]] */ + return OPERAND; + } + if (ops_table[n].op_num == BAND || ops_table[n].op_num == BOR) { + /* [ ] accepts -a and -o but not && and || */ + /* [[ ]] accepts && and || but not -a and -o */ + if (bash_test2 == (s[0] == '-')) + return OPERAND; + } +#endif + last_operator = &ops_table[n]; return ops_table[n].op_num; } @@ -536,6 +585,29 @@ static int binop(void) /*if (op->op_num == INTLT)*/ return val1 < val2; } +#if BASH_TEST2 + if (bash_test2) { + if (op->op_num == STREQ) { + val1 = fnmatch(opnd2, opnd1, 0); + return val1 == 0; + } + if (op->op_num == STRNE) { + val1 = fnmatch(opnd2, opnd1, 0); + return val1 != 0; + } + if (op->op_num == REGEX) { + regex_t re_buffer; + memset(&re_buffer, 0, sizeof(re_buffer)); + if (regcomp(&re_buffer, opnd2, REG_EXTENDED)) { // REG_NEWLINE? + /* Bad regex */ + longjmp(leaving, 2); /* [[ a =~ * ]]; echo $? - prints 2 (silently, no error msg) */ + } + val1 = regexec(&re_buffer, opnd1, 0, NULL, 0); + regfree(&re_buffer); + return val1 == 0; + } + } +#endif if (is_str_op(op->op_num)) { val1 = strcmp(opnd1, opnd2); if (op->op_num == STREQ) @@ -824,6 +896,9 @@ int test_main(int argc, char **argv) { int res; const char *arg0; +#if BASH_TEST2 + bool bt2 = 0; +#endif arg0 = bb_basename(argv[0]); if ((ENABLE_TEST1 || ENABLE_TEST2 || ENABLE_ASH_TEST || ENABLE_HUSH_TEST) @@ -840,6 +915,9 @@ int test_main(int argc, char **argv) bb_simple_error_msg("missing ]]"); return 2; } +#if BASH_TEST2 + bt2 = 1; +#endif } argv[argc] = NULL; } @@ -848,6 +926,10 @@ int test_main(int argc, char **argv) /* We must do DEINIT_S() prior to returning */ INIT_S(); +#if BASH_TEST2 + bash_test2 = bt2; +#endif + res = setjmp(leaving); if (res) goto ret; diff --git a/shell/ash.c b/shell/ash.c index 58da0a2a0..cfcc0b818 100644 --- a/shell/ash.c +++ b/shell/ash.c @@ -207,17 +207,17 @@ #define IF_BASH_SUBSTR IF_ASH_BASH_COMPAT /* BASH_TEST2: [[ EXPR ]] * Status of [[ support: - * We replace && and || with -a and -o + * && and || work as they should + * = is glob match operator, not equality operator: STR = GLOB + * (in GLOB, quoting is significant on char-by-char basis: a*cd"*") + * == same as = + * add =~ regex match operator: STR =~ REGEX * TODO: * singleword+noglob expansion: * v='a b'; [[ $v = 'a b' ]]; echo 0:$? * [[ /bin/n* ]]; echo 0:$? - * -a/-o are not AND/OR ops! (they are just strings) * quoting needs to be considered (-f is an operator, "-f" and ""-f are not; etc) - * = is glob match operator, not equality operator: STR = GLOB - * (in GLOB, quoting is significant on char-by-char basis: a*cd"*") - * == same as = - * add =~ regex match operator: STR =~ REGEX + * ( ) < > should not have special meaning */ #define BASH_TEST2 (ENABLE_ASH_BASH_COMPAT * ENABLE_ASH_TEST) #define BASH_SOURCE ENABLE_ASH_BASH_COMPAT @@ -11823,7 +11823,8 @@ simplecmd(void) tokpushback = 1; goto out; } - wordtext = (char *) (t == TAND ? "-a" : "-o"); + /* pass "&&" or "||" to [[ ]] as literal args */ + wordtext = (char *) (t == TAND ? "&&" : "||"); #endif case TWORD: n = stzalloc(sizeof(struct narg)); diff --git a/shell/hush.c b/shell/hush.c index bc6e6014f..7c1e1d748 100644 --- a/shell/hush.c +++ b/shell/hush.c @@ -84,13 +84,12 @@ * [[ args ]] are CMD_SINGLEWORD_NOGLOB: * v='a b'; [[ $v = 'a b' ]]; echo 0:$? * [[ /bin/n* ]]; echo 0:$? + * = is glob match operator, not equality operator: STR = GLOB + * (in GLOB, quoting is significant on char-by-char basis: a*cd"*") + * == same as = + * =~ is regex match operator: STR =~ REGEX * TODO: - * &&/|| are AND/OR ops, -a/-o are not * quoting needs to be considered (-f is an operator, "-f" and ""-f are not; etc) - * = is glob match operator, not equality operator: STR = GLOB - * (in GLOB, quoting is significant on char-by-char basis: a*cd"*") - * == same as = - * add =~ regex match operator: STR =~ REGEX */ //config:config HUSH //config: bool "hush (68 kb)" @@ -651,14 +650,16 @@ struct command { smallint cmd_type; /* CMD_xxx */ #define CMD_NORMAL 0 #define CMD_SUBSHELL 1 -#if BASH_TEST2 || ENABLE_HUSH_LOCAL || ENABLE_HUSH_EXPORT || ENABLE_HUSH_READONLY -/* used for "[[ EXPR ]]", and to prevent word splitting and globbing in - * "export v=t*" - */ -# define CMD_SINGLEWORD_NOGLOB 2 +#if BASH_TEST2 +/* used for "[[ EXPR ]]" */ +# define CMD_TEST2_SINGLEWORD_NOGLOB 2 +#endif +#if ENABLE_HUSH_LOCAL || ENABLE_HUSH_EXPORT || ENABLE_HUSH_READONLY +/* used to prevent word splitting and globbing in "export v=t*" */ +# define CMD_SINGLEWORD_NOGLOB 3 #endif #if ENABLE_HUSH_FUNCTIONS -# define CMD_FUNCDEF 3 +# define CMD_FUNCDEF 4 #endif smalluint cmd_exitcode; @@ -4111,6 +4112,14 @@ static int done_word(struct parse_context *ctx) /* ctx->ctx_res_w = RES_MATCH; */ ctx->ctx_dsemicolon = 0; } else +# endif +# if defined(CMD_TEST2_SINGLEWORD_NOGLOB) + if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB + && strcmp(ctx->word.data, "]]") == 0 + ) { + /* allow "[[ ]] >file" etc */ + command->cmd_type = CMD_SINGLEWORD_NOGLOB; + } else # endif if (!command->argv /* if it's the first word... */ # if ENABLE_HUSH_LOOPS @@ -4146,11 +4155,13 @@ static int done_word(struct parse_context *ctx) (ctx->ctx_res_w == RES_SNTX)); return (ctx->ctx_res_w == RES_SNTX); } +# if defined(CMD_TEST2_SINGLEWORD_NOGLOB) + if (strcmp(ctx->word.data, "[[") == 0) { + command->cmd_type = CMD_TEST2_SINGLEWORD_NOGLOB; + } else +# endif # if defined(CMD_SINGLEWORD_NOGLOB) if (0 -# if BASH_TEST2 - || strcmp(ctx->word.data, "[[") == 0 -# endif /* In bash, local/export/readonly are special, args * are assignments and therefore expansion of them * should be "one-word" expansion: @@ -4172,7 +4183,8 @@ static int done_word(struct parse_context *ctx) ) { command->cmd_type = CMD_SINGLEWORD_NOGLOB; } - /* fall through */ +# else + { /* empty block to pair "if ... else" */ } # endif } #endif /* HAS_KEYWORDS */ @@ -5354,9 +5366,15 @@ static struct pipe *parse_stream(char **pstring, if (ch != '\n') next = i_peek_and_eat_bkslash_nl(input); - is_special = "{}<>;&|()#" /* special outside of "str" */ + is_special = "{}<>&|();#" /* special outside of "str" */ "$\"" IF_HUSH_TICK("`") /* always special */ SPECIAL_VAR_SYMBOL_STR; +#if defined(CMD_TEST2_SINGLEWORD_NOGLOB) + if (ctx.command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB) { + /* In [[ ]], {}<>&|() are not special */ + is_special += 8; + } else +#endif /* Are { and } special here? */ if (ctx.command->argv /* word [word]{... - non-special */ || ctx.word.length /* word{... - non-special */ @@ -6953,7 +6971,7 @@ static char **expand_strvec_to_strvec(char **argv) return expand_variables(argv, EXP_FLAG_GLOB | EXP_FLAG_ESC_GLOB_CHARS); } -#if defined(CMD_SINGLEWORD_NOGLOB) +#if defined(CMD_SINGLEWORD_NOGLOB) || defined(CMD_TEST2_SINGLEWORD_NOGLOB) static char **expand_strvec_to_strvec_singleword_noglob(char **argv) { return expand_variables(argv, EXP_FLAG_SINGLEWORD); @@ -9133,6 +9151,11 @@ static NOINLINE int run_pipe(struct pipe *pi) } /* Expand the rest into (possibly) many strings each */ +#if defined(CMD_TEST2_SINGLEWORD_NOGLOB) + if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB) + argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt); + else +#endif #if defined(CMD_SINGLEWORD_NOGLOB) if (command->cmd_type == CMD_SINGLEWORD_NOGLOB) argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt); diff --git a/shell/hush_test/hush-test2/andor1.right b/shell/hush_test/hush-test2/andor1.right new file mode 100644 index 000000000..038c7a681 --- /dev/null +++ b/shell/hush_test/hush-test2/andor1.right @@ -0,0 +1,6 @@ +1:YES +2:no +3:YES +4:YES +5:no +6:no diff --git a/shell/hush_test/hush-test2/andor1.tests b/shell/hush_test/hush-test2/andor1.tests new file mode 100755 index 000000000..c449de7e6 --- /dev/null +++ b/shell/hush_test/hush-test2/andor1.tests @@ -0,0 +1,7 @@ +e='' +[[ a && b ]] && echo 1:YES +[[ a && '' ]] || echo 2:no +[[ a || b ]] && echo 3:YES +[[ '' || b ]] && echo 4:YES +[[ "" || "$e" ]] || echo 5:no +[[ "" || $e ]] || echo 6:no diff --git a/shell/hush_test/hush-test2/noglob1.right b/shell/hush_test/hush-test2/noglob1.right new file mode 100644 index 000000000..d0c3f1d8e --- /dev/null +++ b/shell/hush_test/hush-test2/noglob1.right @@ -0,0 +1,2 @@ +1:YES:0 +2:YES:0 diff --git a/shell/hush_test/hush-test2/noglob1.tests b/shell/hush_test/hush-test2/noglob1.tests new file mode 100755 index 000000000..963bacbd3 --- /dev/null +++ b/shell/hush_test/hush-test2/noglob1.tests @@ -0,0 +1,3 @@ +v='*.tests' +[[ *.tests ]]; echo 1:YES:$? +[[ $v ]]; echo 2:YES:$? diff --git a/shell/hush_test/hush-test2/strops1.right b/shell/hush_test/hush-test2/strops1.right new file mode 100644 index 000000000..590496301 --- /dev/null +++ b/shell/hush_test/hush-test2/strops1.right @@ -0,0 +1,8 @@ +1:YES:0 +2:YES:0 +3:YES:0 +4:YES:0 +5:YES:0 +6:YES:0 +7:YES:0 +8:no:1 diff --git a/shell/hush_test/hush-test2/strops1.tests b/shell/hush_test/hush-test2/strops1.tests new file mode 100755 index 000000000..bb24e2a2f --- /dev/null +++ b/shell/hush_test/hush-test2/strops1.tests @@ -0,0 +1,15 @@ +v='*.z' +[[ a.z = *.z ]]; echo 1:YES:$? +[[ a.z == $v ]]; echo 2:YES:$? + +# wildcards can match a slash +[[ a/b = a*b ]]; echo 3:YES:$? +[[ a/b == a?b ]]; echo 4:YES:$? + +# wildcards can match a leading dot +[[ a/.b = a/*b ]]; echo 5:YES:$? +[[ a/.b == a/?b ]]; echo 6:YES:$? + +# wildcards can be escaped +[[ abc = a*c ]]; echo 7:YES:$? +[[ abc == a\*c ]]; echo 8:no:$? diff --git a/shell/hush_test/hush-test2/strops2.right b/shell/hush_test/hush-test2/strops2.right new file mode 100644 index 000000000..8ddb4b0f0 --- /dev/null +++ b/shell/hush_test/hush-test2/strops2.right @@ -0,0 +1,6 @@ +1:ERR2:2 +2:YES:0 +3:YES:0 +4:YES:0 +5:no:1 +6:YES:0 diff --git a/shell/hush_test/hush-test2/strops2.tests b/shell/hush_test/hush-test2/strops2.tests new file mode 100755 index 000000000..ab325bc9f --- /dev/null +++ b/shell/hush_test/hush-test2/strops2.tests @@ -0,0 +1,12 @@ +# malformed regex +[[ a =~ * ]]; echo 1:ERR2:$? + +[[ a/b =~ a.b ]]; echo 2:YES:$? +[[ a/b =~ /*b ]]; echo 3:YES:$? + +v='[]b.-]' +[[ a/.b] =~ $v ]]; echo 4:YES:$? + +v=']b.-' +[[ a/.b] =~ $v ]]; echo 5:no:$? +[[ a/.b] =~ [$v] ]]; echo 6:YES:$? diff --git a/shell/hush_test/hush-test2/strops3.right b/shell/hush_test/hush-test2/strops3.right new file mode 100644 index 000000000..14cc04fdc --- /dev/null +++ b/shell/hush_test/hush-test2/strops3.right @@ -0,0 +1,7 @@ +1:YES:0 +2:YES:0 +3:no:1 +4:YES:0 +2u:YES:0 +3u:YES:0 +4u:YES:0 diff --git a/shell/hush_test/hush-test2/strops3.tests b/shell/hush_test/hush-test2/strops3.tests new file mode 100755 index 000000000..927476671 --- /dev/null +++ b/shell/hush_test/hush-test2/strops3.tests @@ -0,0 +1,13 @@ +# regex should accept '+' operator +[[ abcdef =~ a[b-z]+ ]]; echo 1:YES:$? + +# newline matches by "match any" patterns +v=' +' +[[ "$v" =~ . ]]; echo 2:YES:$? +[[ "$v" =~ "[$v]" ]]; echo 3:no:$? # hmm bash does return 1... why? +[[ "$v" =~ [^a] ]]; echo 4:YES:$? +# should work even without quotes: +[[ $v =~ . ]]; echo 2u:YES:$? +[[ $v =~ [$v] ]]; echo 3u:YES:$? +[[ $v =~ [^a] ]]; echo 4u:YES:$? -- cgit v1.2.3