shell: better support of [[ ]] bashism
No binary (build failed)
—
(log yok)
| @@ -76,6 +76,8 @@ | |
| //usage: "1\n" | |
| #include "libbb.h" | |
| + | #include <regex.h> |
| + | #include <fnmatch.h> |
| /* 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; | |
| @@ -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)); | |
| @@ -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); | |
| @@ -0,0 +1,6 @@ | |
| + | 1:YES |
| + | 2:no |
| + | 3:YES |
| + | 4:YES |
| + | 5:no |
| + | 6:no |
| @@ -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 |
| @@ -0,0 +1,2 @@ | |
| + | 1:YES:0 |
| + | 2:YES:0 |
| @@ -0,0 +1,3 @@ | |
| + | v='*.tests' |
| + | [[ *.tests ]]; echo 1:YES:$? |
| + | [[ $v ]]; echo 2:YES:$? |
| @@ -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 |
| @@ -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:$? |
| @@ -0,0 +1,6 @@ | |
| + | 1:ERR2:2 |
| + | 2:YES:0 |
| + | 3:YES:0 |
| + | 4:YES:0 |
| + | 5:no:1 |
| + | 6:YES:0 |
| @@ -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:$? |
| @@ -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 |
| @@ -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:$? |