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>
diff --git a/coreutils/test.c b/coreutils/test.c
index a089861..ac7b546 100644
--- a/coreutils/test.c
+++ b/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 @@
 	STRLT,
 	STRGT,
 
+#if BASH_TEST2
+	REGEX,
+#endif
+
 	INTEQ, /* int ops */
 	INTNE,
 	INTGE,
@@ -257,6 +271,9 @@
 	"STRNE",
 	"STRLT",
 	"STRGT",
+#if BASH_TEST2
+	"REGEX",
+#endif
 	"INTEQ",
 	"INTNE",
 	"INTGE",
@@ -320,6 +337,9 @@
 	{ /* "!=" */ 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 @@
 	{ /* "!"  */ UNOT    , BUNOP  },
 	{ /* "-a" */ BAND    , BBINOP },
 	{ /* "-o" */ BOR     , BBINOP },
+#if BASH_TEST2
+	{ /* "&&" */ BAND    , BBINOP },
+	{ /* "||" */ BOR     , BBINOP },
+#endif
 	{ /* "("  */ LPAREN  , PAREN  },
 	{ /* ")"  */ RPAREN  , PAREN  },
 };
@@ -365,6 +389,9 @@
 	"!="  "\0"
 	"<"   "\0"
 	">"   "\0"
+#if BASH_TEST2
+	"=~"  "\0"
+#endif
 	"-eq" "\0"
 	"-ne" "\0"
 	"-ge" "\0"
@@ -377,6 +404,10 @@
 	"!"   "\0"
 	"-a"  "\0"
 	"-o"  "\0"
+#if BASH_TEST2
+	"&&"  "\0"
+	"||"  "\0"
+#endif
 	"("   "\0"
 	")"   "\0"
 ;
@@ -397,6 +428,9 @@
 	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 @@
 #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 @@
 	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 @@
 		/*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 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 @@
 				bb_simple_error_msg("missing ]]");
 				return 2;
 			}
+#if BASH_TEST2
+			bt2 = 1;
+#endif
 		}
 		argv[argc] = NULL;
 	}
@@ -848,6 +926,10 @@
 	/* We must do DEINIT_S() prior to returning */
 	INIT_S();
 
+#if BASH_TEST2
+	bash_test2 = bt2;
+#endif
+
 	res = setjmp(leaving);
 	if (res)
 		goto ret;