Browse Source

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 <vda.linux@googlemail.com>
Denys Vlasenko 3 years ago
parent
commit
d2241f5902

+ 82 - 0
coreutils/test.c

@@ -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;

+ 8 - 7
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));

+ 40 - 17
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);

+ 6 - 0
shell/hush_test/hush-test2/andor1.right

@@ -0,0 +1,6 @@
+1:YES
+2:no
+3:YES
+4:YES
+5:no
+6:no

+ 7 - 0
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

+ 2 - 0
shell/hush_test/hush-test2/noglob1.right

@@ -0,0 +1,2 @@
+1:YES:0
+2:YES:0

+ 3 - 0
shell/hush_test/hush-test2/noglob1.tests

@@ -0,0 +1,3 @@
+v='*.tests'
+[[ *.tests ]]; echo 1:YES:$?
+[[ $v ]]; echo 2:YES:$?

+ 8 - 0
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

+ 15 - 0
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:$?

+ 6 - 0
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

+ 12 - 0
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:$?

+ 7 - 0
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

+ 13 - 0
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:$?