hush: support ${var:EXPR:EXPR}!

function                                             old     new   delta
handle_dollar                                        574     681    +107
expand_and_evaluate_arith                              -      77     +77
expand_vars_to_list                                 2302    2374     +72
add_till_closing_bracket                             359     368      +9
builtin_exit                                          48      47      -1
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 3/1 up/down: 265/-1)            Total: 264 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/hush.c b/shell/hush.c
index 0f0151a..41d5fca 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -171,6 +171,7 @@
 #define debug_printf_env(...)    do {} while (0)
 #define debug_printf_jobs(...)   do {} while (0)
 #define debug_printf_expand(...) do {} while (0)
+#define debug_printf_varexp(...) do {} while (0)
 #define debug_printf_glob(...)   do {} while (0)
 #define debug_printf_list(...)   do {} while (0)
 #define debug_printf_subst(...)  do {} while (0)
@@ -743,6 +744,10 @@
 # define DEBUG_EXPAND 0
 #endif
 
+#ifndef debug_printf_varexp
+# define debug_printf_varexp(...) (indent(), fprintf(stderr, __VA_ARGS__))
+#endif
+
 #ifndef debug_printf_glob
 # define debug_printf_glob(...) (indent(), fprintf(stderr, __VA_ARGS__))
 # define DEBUG_GLOB 1
@@ -1817,11 +1822,11 @@
 	o->data[o->length] = '\0';
 }
 
-#if !BB_MMU
 static void o_addstr(o_string *o, const char *str)
 {
 	o_addblock(o, str, strlen(str));
 }
+#if !BB_MMU
 static void nommu_addchr(o_string *o, int ch)
 {
 	if (o)
@@ -2597,12 +2602,19 @@
 			} else {
 				/* maybe handle parameter expansion */
 				exp_saveptr = var + strcspn(var, "%#:-=+?");
-				exp_save = *exp_saveptr;
-				if (exp_save) {
-					exp_word = exp_saveptr;
-					if (exp_save == ':')
-						exp_word++;
-					exp_op = *exp_word++;
+				exp_op = exp_save = *exp_saveptr;
+				if (exp_op) {
+					exp_word = exp_saveptr + 1;
+					if (exp_op == ':') {
+						exp_op = *exp_word++;
+						if (ENABLE_HUSH_BASH_COMPAT
+						 && (exp_op == '\0' || !strchr("%#:-=+?"+3, exp_op))
+						) {
+							/* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
+							exp_op = ':';
+							exp_word--;
+						}
+					}
 					*exp_saveptr = '\0';
 				}
 			}
@@ -2656,39 +2668,42 @@
 								*loc = '\0';
 						}
 					}
-				} else if (!strchr("%#:-=+?"+3, exp_op)) {
+				} else if (exp_op == ':') {
 #if ENABLE_HUSH_BASH_COMPAT
-	/* exp_op is ':' and next char isn't a subst operator.
-	 * Assuming it's ${var:[N][:M]} bashism.
-	 * TODO: N, M can be expressions similar to $((EXPR)): 2+2, 2+var etc
+	/* It's ${var:N[:M]} bashism.
+	 * Note that in encoded form it has TWO parts:
+	 * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
 	 */
-					char *end;
-					unsigned len = INT_MAX;
-					unsigned beg = 0;
-					end = --exp_word;
-					if (*exp_word != ':') /* not ${var::...} */
-						beg = bb_strtou(exp_word, &end, 0);
-					//bb_error_msg("beg:'%s'=%u end:'%s'", exp_word, beg, end);
-					if (*end == ':') {
-						if (end[1] != '\0') /* not ${var:NUM:} */
-							len = bb_strtou(end + 1, &end, 0);
-						else {
-							len = 0;
-							end++;
-						}
-						//bb_error_msg("len:%u end:'%s'", len, end);
-					}
-					if (*end == '\0') {
-						//bb_error_msg("from val:'%s'", val);
+					arith_t beg, len;
+					int errcode = 0;
+
+					beg = expand_and_evaluate_arith(exp_word, &errcode);
+					debug_printf_varexp("beg:'%s'=%lld\n", exp_word, (long long)beg);
+					*p++ = SPECIAL_VAR_SYMBOL;
+					exp_word = p;
+					p = strchr(p, SPECIAL_VAR_SYMBOL);
+					*p = '\0';
+					len = expand_and_evaluate_arith(exp_word, &errcode);
+					debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
+
+					if (errcode >= 0 && len >= 0) { /* bash compat: len < 0 is illegal */
+						if (beg < 0) /* bash compat */
+							beg = 0;
+						debug_printf_varexp("from val:'%s'\n", val);
 						if (len == 0 || !val || beg >= strlen(val))
 							val = "";
-						else
+						else {
+							/* Paranoia. What if user entered 9999999999999
+							 * which fits in arith_t but not int? */
+							if (len >= INT_MAX)
+								len = INT_MAX;
 							val = to_be_freed = xstrndup(val + beg, len);
-						//bb_error_msg("val:'%s'", val);
+						}
+						debug_printf_varexp("val:'%s'\n", val);
 					} else
 #endif
 					{
-						die_if_script("malformed ${%s...}", var);
+						die_if_script("malformed ${%s:...}", var);
 						val = "";
 					}
 				} else { /* one of "-=+?" */
@@ -5891,21 +5906,28 @@
  * echo $(echo 'TEST)' BEST)            TEST) BEST
  * echo $(echo \(\(TEST\) BEST)         ((TEST) BEST
  *
- * Also adapted to eat ${var%...} constructs, since ... part
+ * Also adapted to eat ${var%...} and $((...)) constructs, since ... part
  * can contain arbitrary constructs, just like $(cmd).
+ * In bash compat mode, it needs to also be able to stop on '}' or ':'
+ * for ${var:N[:M]} parsing.
  */
 #define DOUBLE_CLOSE_CHAR_FLAG 0x80
-static void add_till_closing_bracket(o_string *dest, struct in_str *input, char end_ch)
+static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsigned end_ch)
 {
+	int ch;
 	char dbl = end_ch & DOUBLE_CLOSE_CHAR_FLAG;
-	end_ch &= (DOUBLE_CLOSE_CHAR_FLAG-1);
+#if ENABLE_HUSH_BASH_COMPAT
+	char end_char2 = end_ch >> 8;
+#endif
+	end_ch &= (DOUBLE_CLOSE_CHAR_FLAG - 1);
+
 	while (1) {
-		int ch = i_getch(input);
+		ch = i_getch(input);
 		if (ch == EOF) {
 			syntax_error_unterm_ch(end_ch);
 			/*xfunc_die(); - redundant */
 		}
-		if (ch == end_ch) {
+		if (ch == end_ch  IF_HUSH_BASH_COMPAT( || ch == end_char2)) {
 			if (!dbl)
 				break;
 			/* we look for closing )) of $((EXPR)) */
@@ -5947,6 +5969,7 @@
 			continue;
 		}
 	}
+	return ch;
 }
 #endif /* ENABLE_HUSH_TICK || ENABLE_SH_MATH_SUPPORT */
 
@@ -6033,22 +6056,45 @@
 				break;
 
 			if (!isalnum(ch) && ch != '_') {
+				unsigned end_ch;
+				unsigned char last_ch;
 				/* handle parameter expansions
 				 * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
 				 */
 				if (!strchr("%#:-=+?", ch)) /* ${var<bad_char>... */
 					goto bad_dollar_syntax;
-				/* Eat everything until closing '}' */
 				o_addchr(dest, ch);
+
+				/* Eat everything until closing '}' (or ':') */
+				end_ch = '}';
+				if (ENABLE_HUSH_BASH_COMPAT
+				 && ch == ':'
+				 && !strchr("%#:-=+?"+3, i_peek(input))
+				) {
+					/* It's ${var:N[:M]} thing */
+					end_ch = '}' * 0x100 + ':';
+				}
+ again:
 				if (!BB_MMU)
 					pos = dest->length;
-				add_till_closing_bracket(dest, input, '}');
-#if !BB_MMU
+				last_ch = add_till_closing_bracket(dest, input, end_ch);
 				if (as_string) {
 					o_addstr(as_string, dest->data + pos);
-					o_addchr(as_string, '}');
+					o_addchr(as_string, last_ch);
 				}
-#endif
+
+				if (ENABLE_HUSH_BASH_COMPAT && (end_ch & 0xff00)) {
+					/* close the first block: */
+					o_addchr(dest, SPECIAL_VAR_SYMBOL);
+					/* while parsing N from ${var:N[:M]}... */
+					if ((end_ch & 0xff) == last_ch) {
+						/* ...got ':' - parse the rest */
+						end_ch = '}';
+						goto again;
+					}
+					/* ...got '}', not ':' - it's ${var:N}! emulate :999999999 */
+					o_addstr(dest, "999999999");
+				}
 				break;
 			}
 		}
diff --git a/shell/hush_test/hush-vars/param_expand_bash_substring.right b/shell/hush_test/hush-vars/param_expand_bash_substring.right
index 6e3eb3b..53b8836 100644
--- a/shell/hush_test/hush-vars/param_expand_bash_substring.right
+++ b/shell/hush_test/hush-vars/param_expand_bash_substring.right
@@ -39,3 +39,13 @@
 f::2 =|01|
 f:1: =||
 f::  =||
+Substrings with expressions
+f            =|01234567|
+f:1+1:2+2    =|2345|
+f:-1:2+2     =|01234567|
+f:1:f        =|1234567|
+f:1:$f       =|1234567|
+f:1:${f}     =|1234567|
+f:1:${f:3:1} =|123|
+f:1:1`echo 1`=|1|
+Done
diff --git a/shell/hush_test/hush-vars/param_expand_bash_substring.tests b/shell/hush_test/hush-vars/param_expand_bash_substring.tests
index eedd435..a80523a 100755
--- a/shell/hush_test/hush-vars/param_expand_bash_substring.tests
+++ b/shell/hush_test/hush-vars/param_expand_bash_substring.tests
@@ -1,8 +1,6 @@
-# do all of these in subshells since it's supposed to error out
-
-export var=0123456789
-
 # first try some invalid patterns
+# do all of these in subshells since it's supposed to error out
+export var=0123456789
 "$THIS_SH" -c 'echo ${:}'
 "$THIS_SH" -c 'echo ${::}'
 "$THIS_SH" -c 'echo ${:1}'
@@ -15,44 +13,56 @@
 # UNFIXED BUG: this should work: "$THIS_SH" -c 'echo ${?:0}'
 
 # now some valid ones
-"$THIS_SH" -c 'set --; echo "1    =|${1}|"'
-"$THIS_SH" -c 'set --; echo "1:1  =|${1:1}|"'
-"$THIS_SH" -c 'set --; echo "1:1:2=|${1:1:2}|"'
-"$THIS_SH" -c 'set --; echo "1::2 =|${1::2}|"'
-"$THIS_SH" -c 'set --; echo "1:1: =|${1:1:}|"'
-"$THIS_SH" -c 'set --; echo "1::  =|${1::}|"'
+set --; echo "1    =|${1}|"
+set --; echo "1:1  =|${1:1}|"
+set --; echo "1:1:2=|${1:1:2}|"
+set --; echo "1::2 =|${1::2}|"
+set --; echo "1:1: =|${1:1:}|"
+set --; echo "1::  =|${1::}|"
 
-"$THIS_SH" -c 'set -- 0123; echo "1    =|${1}|"'
-"$THIS_SH" -c 'set -- 0123; echo "1:1  =|${1:1}|"'
-"$THIS_SH" -c 'set -- 0123; echo "1:1:2=|${1:1:2}|"'
-"$THIS_SH" -c 'set -- 0123; echo "1::2 =|${1::2}|"'
-"$THIS_SH" -c 'set -- 0123; echo "1:1: =|${1:1:}|"'
-"$THIS_SH" -c 'set -- 0123; echo "1::  =|${1::}|"'
+set -- 0123; echo "1    =|${1}|"
+set -- 0123; echo "1:1  =|${1:1}|"
+set -- 0123; echo "1:1:2=|${1:1:2}|"
+set -- 0123; echo "1::2 =|${1::2}|"
+set -- 0123; echo "1:1: =|${1:1:}|"
+set -- 0123; echo "1::  =|${1::}|"
 
-"$THIS_SH" -c 'unset f; echo "f    =|$f|"'
-"$THIS_SH" -c 'unset f; echo "f:1  =|${f:1}|"'
-"$THIS_SH" -c 'unset f; echo "f:1:2=|${f:1:2}|"'
-"$THIS_SH" -c 'unset f; echo "f::2 =|${f::2}|"'
-"$THIS_SH" -c 'unset f; echo "f:1: =|${f:1:}|"'
-"$THIS_SH" -c 'unset f; echo "f::  =|${f::}|"'
+unset f; echo "f    =|$f|"
+unset f; echo "f:1  =|${f:1}|"
+unset f; echo "f:1:2=|${f:1:2}|"
+unset f; echo "f::2 =|${f::2}|"
+unset f; echo "f:1: =|${f:1:}|"
+unset f; echo "f::  =|${f::}|"
 
-"$THIS_SH" -c 'f=; echo "f    =|$f|"'
-"$THIS_SH" -c 'f=; echo "f:1  =|${f:1}|"'
-"$THIS_SH" -c 'f=; echo "f:1:2=|${f:1:2}|"'
-"$THIS_SH" -c 'f=; echo "f::2 =|${f::2}|"'
-"$THIS_SH" -c 'f=; echo "f:1: =|${f:1:}|"'
-"$THIS_SH" -c 'f=; echo "f::  =|${f::}|"'
+f=; echo "f    =|$f|"
+f=; echo "f:1  =|${f:1}|"
+f=; echo "f:1:2=|${f:1:2}|"
+f=; echo "f::2 =|${f::2}|"
+f=; echo "f:1: =|${f:1:}|"
+f=; echo "f::  =|${f::}|"
 
-"$THIS_SH" -c 'f=a; echo "f    =|$f|"'
-"$THIS_SH" -c 'f=a; echo "f:1  =|${f:1}|"'
-"$THIS_SH" -c 'f=a; echo "f:1:2=|${f:1:2}|"'
-"$THIS_SH" -c 'f=a; echo "f::2 =|${f::2}|"'
-"$THIS_SH" -c 'f=a; echo "f:1: =|${f:1:}|"'
-"$THIS_SH" -c 'f=a; echo "f::  =|${f::}|"'
+f=a; echo "f    =|$f|"
+f=a; echo "f:1  =|${f:1}|"
+f=a; echo "f:1:2=|${f:1:2}|"
+f=a; echo "f::2 =|${f::2}|"
+f=a; echo "f:1: =|${f:1:}|"
+f=a; echo "f::  =|${f::}|"
 
-"$THIS_SH" -c 'f=0123456789; echo "f    =|$f|"'
-"$THIS_SH" -c 'f=0123456789; echo "f:1  =|${f:1}|"'
-"$THIS_SH" -c 'f=0123456789; echo "f:1:2=|${f:1:2}|"'
-"$THIS_SH" -c 'f=0123456789; echo "f::2 =|${f::2}|"'
-"$THIS_SH" -c 'f=0123456789; echo "f:1: =|${f:1:}|"'
-"$THIS_SH" -c 'f=0123456789; echo "f::  =|${f::}|"'
+f=0123456789; echo "f    =|$f|"
+f=0123456789; echo "f:1  =|${f:1}|"
+f=0123456789; echo "f:1:2=|${f:1:2}|"
+f=0123456789; echo "f::2 =|${f::2}|"
+f=0123456789; echo "f:1: =|${f:1:}|"
+f=0123456789; echo "f::  =|${f::}|"
+
+echo "Substrings with expressions"
+f=01234567; echo 'f            '"=|$f|"
+f=01234567; echo 'f:1+1:2+2    '"=|${f:1+1:2+2}|"
+f=01234567; echo 'f:-1:2+2     '"=|${f:-1:2+2}|"
+f=01234567; echo 'f:1:f        '"=|${f:1:f}|"
+f=01234567; echo 'f:1:$f       '"=|${f:1:$f}|"
+f=01234567; echo 'f:1:${f}     '"=|${f:1:${f}}|"
+f=01234567; echo 'f:1:${f:3:1} '"=|${f:1:${f:3:1}}|"
+f=01234567; echo 'f:1:1`echo 1`'"=|${f:1:`echo 1`}|"
+
+echo Done