+/* expand_strvec_to_strvec() takes a list of strings, expands
+ * all variable references within and returns a pointer to
+ * a list of expanded strings, possibly with larger number
+ * of strings. (Think VAR="a b"; echo $VAR).
+ * This new list is allocated as a single malloc block.
+ * NULL-terminated list of char* pointers is at the beginning of it,
+ * followed by strings themself.
+ * Caller can deallocate entire list by single free(list). */
+
+/* Helpers first:
+ * count_XXX estimates size of the block we need. It's okay
+ * to over-estimate sizes a bit, if it makes code simpler */
+static int count_ifs(const char *str)
+{
+ int cnt = 0;
+ debug_printf_expand("count_ifs('%s') ifs='%s'", str, ifs);
+ while (1) {
+ str += strcspn(str, ifs);
+ if (!*str) break;
+ str++; /* str += strspn(str, ifs); */
+ cnt++; /* cnt += strspn(str, ifs); - but this code is larger */
+ }
+ debug_printf_expand(" return %d\n", cnt);
+ return cnt;
+}
+
+static void count_var_expansion_space(int *countp, int *lenp, char *arg)
+{
+ char first_ch;
+ int i;
+ int len = *lenp;
+ int count = *countp;
+ const char *val;
+ char *p;
+
+ while ((p = strchr(arg, SPECIAL_VAR_SYMBOL))) {
+ len += p - arg;
+ arg = ++p;
+ p = strchr(p, SPECIAL_VAR_SYMBOL);
+ first_ch = arg[0];
+
+ switch (first_ch & 0x7f) {
+ /* high bit in 1st_ch indicates that var is double-quoted */
+ case '$': /* pid */
+ case '!': /* bg pid */
+ case '?': /* exitcode */
+ case '#': /* argc */
+ len += sizeof(int)*3 + 1; /* enough for int */
+ break;
+ case '*':
+ case '@':
+ for (i = 1; i < global_argc; i++) {
+ len += strlen(global_argv[i]) + 1;
+ count++;
+ if (!(first_ch & 0x80))
+ count += count_ifs(global_argv[i]);
+ }
+ break;
+ default:
+ *p = '\0';
+ arg[0] = first_ch & 0x7f;
+ if (isdigit(arg[0])) {
+ i = xatoi_u(arg);
+ val = NULL;
+ if (i < global_argc)
+ val = global_argv[i];
+ } else
+ val = lookup_param(arg);
+ arg[0] = first_ch;
+ *p = SPECIAL_VAR_SYMBOL;
+
+ if (val) {
+ len += strlen(val) + 1;
+ if (!(first_ch & 0x80))
+ count += count_ifs(val);
+ }
+ }
+ arg = ++p;
+ }
+
+ len += strlen(arg) + 1;
+ count++;
+ *lenp = len;
+ *countp = count;
+}
+
+/* Store given string, finalizing the word and starting new one whenever
+ * we encounter ifs char(s). This is used for expanding variable values.
+ * End-of-string does NOT finalize word: think about 'echo -$VAR-' */
+static int expand_on_ifs(char **list, int n, char **posp, const char *str)
+{
+ char *pos = *posp;
+ while (1) {
+ int word_len = strcspn(str, ifs);
+ if (word_len) {
+ memcpy(pos, str, word_len); /* store non-ifs chars */
+ pos += word_len;
+ str += word_len;
+ }
+ if (!*str) /* EOL - do not finalize word */
+ break;
+ *pos++ = '\0';
+ if (n) debug_printf_expand("expand_on_ifs finalized list[%d]=%p '%s' "
+ "strlen=%d next=%p pos=%p\n", n-1, list[n-1], list[n-1],
+ strlen(list[n-1]), list[n-1] + strlen(list[n-1]) + 1, pos);
+ list[n++] = pos;
+ str += strspn(str, ifs); /* skip ifs chars */
+ }
+ *posp = pos;
+ return n;
+}
+
+/* Expand all variable references in given string, adding words to list[]
+ * at n, n+1,... positions. Return updated n (so that list[n] is next one
+ * to be filled). This routine is extremely tricky: has to deal with
+ * variables/parameters with whitespace, $* and $@, and constructs like
+ * 'echo -$*-'. If you play here, you must run testsuite afterwards! */
+/* NB: another bug is that we cannot detect empty strings yet:
+ * "" or $empty"" expands to zero words, has to expand to empty word */
+static int expand_vars_to_list(char **list, int n, char **posp, char *arg, char or_mask)
+{
+ /* or_mask is either 0 (normal case) or 0x80
+ * (expansion of right-hand side of assignment == 1-element expand) */
+
+ char first_ch, ored_ch;
+ int i;
+ const char *val;
+ char *p;
+ char *pos = *posp;
+
+ ored_ch = 0;
+
+ if (n) debug_printf_expand("expand_vars_to_list finalized list[%d]=%p '%s' "
+ "strlen=%d next=%p pos=%p\n", n-1, list[n-1], list[n-1],
+ strlen(list[n-1]), list[n-1] + strlen(list[n-1]) + 1, pos);
+ list[n++] = pos;
+
+ while ((p = strchr(arg, SPECIAL_VAR_SYMBOL))) {
+ memcpy(pos, arg, p - arg);
+ pos += (p - arg);
+ arg = ++p;
+ p = strchr(p, SPECIAL_VAR_SYMBOL);
+
+ first_ch = arg[0] | or_mask; /* forced to "quoted" if or_mask = 0x80 */
+ ored_ch |= first_ch;
+ val = NULL;
+ switch (first_ch & 0x7f) {
+ /* Highest bit in first_ch indicates that var is double-quoted */
+ case '$': /* pid */
+ /* FIXME: (echo $$) should still print pid of main shell */
+ val = utoa(getpid());
+ break;
+ case '!': /* bg pid */
+ val = last_bg_pid ? utoa(last_bg_pid) : (char*)"";
+ break;
+ case '?': /* exitcode */
+ val = utoa(last_return_code);
+ break;
+ case '#': /* argc */
+ val = utoa(global_argc ? global_argc-1 : 0);
+ break;
+ case '*':
+ case '@':
+ i = 1;
+ if (!(first_ch & 0x80)) { /* unquoted $* or $@ */
+ while (i < global_argc) {
+ n = expand_on_ifs(list, n, &pos, global_argv[i]);
+ debug_printf_expand("expand_vars_to_list: argv %d (last %d)\n", i, global_argc-1);
+ if (global_argv[i++][0] && i < global_argc) {
+ /* this argv[] is not empty and not last:
+ * put terminating NUL, start new word */
+ *pos++ = '\0';
+ if (n) debug_printf_expand("expand_vars_to_list 2 finalized list[%d]=%p '%s' "
+ "strlen=%d next=%p pos=%p\n", n-1, list[n-1], list[n-1],
+ strlen(list[n-1]), list[n-1] + strlen(list[n-1]) + 1, pos);
+ list[n++] = pos;
+ }
+ }
+ } else
+ /* If or_mask is nonzero, we handle assignment 'a=....$@.....'
+ * and in this case should theat it like '$*' */
+ if (first_ch == ('@'|0x80) && !or_mask) { /* quoted $@ */
+ while (1) {
+ strcpy(pos, global_argv[i]);
+ pos += strlen(global_argv[i]);
+ if (++i >= global_argc)
+ break;
+ *pos++ = '\0';
+ if (n) debug_printf_expand("expand_vars_to_list 3 finalized list[%d]=%p '%s' "
+ "strlen=%d next=%p pos=%p\n", n-1, list[n-1], list[n-1],
+ strlen(list[n-1]), list[n-1] + strlen(list[n-1]) + 1, pos);
+ list[n++] = pos;
+ }
+ } else { /* quoted $*: add as one word */
+ while (1) {
+ strcpy(pos, global_argv[i]);
+ pos += strlen(global_argv[i]);
+ if (++i >= global_argc)
+ break;
+ if (ifs[0])
+ *pos++ = ifs[0];
+ }
+ }
+ break;
+ default:
+ *p = '\0';
+ arg[0] = first_ch & 0x7f;
+ if (isdigit(arg[0])) {
+ i = xatoi_u(arg);
+ val = NULL;
+ if (i < global_argc)
+ val = global_argv[i];
+ } else
+ val = lookup_param(arg);
+ arg[0] = first_ch;
+ *p = SPECIAL_VAR_SYMBOL;
+ if (!(first_ch & 0x80)) { /* unquoted $VAR */
+ if (val) {
+ n = expand_on_ifs(list, n, &pos, val);
+ val = NULL;
+ }
+ } /* else: quoted $VAR, val will be appended at pos */
+ }
+ if (val) {
+ strcpy(pos, val);
+ pos += strlen(val);
+ }
+ arg = ++p;
+ }
+ debug_printf_expand("expand_vars_to_list adding tail '%s' at %p\n", arg, pos);
+ strcpy(pos, arg);
+ pos += strlen(arg) + 1;
+ if (pos == list[n-1] + 1) { /* expansion is empty */
+ if (!(ored_ch & 0x80)) { /* all vars were not quoted... */
+ debug_printf_expand("expand_vars_to_list list[%d] empty, going back\n", n);
+ pos--;
+ n--;
+ }
+ }
+
+ *posp = pos;
+ return n;
+}
+
+static char **expand_variables(char **argv, char or_mask)
+{
+ int n;
+ int count = 1;
+ int len = 0;
+ char *pos, **v, **list;
+
+ v = argv;
+ if (!*v) debug_printf_expand("count_var_expansion_space: "
+ "argv[0]=NULL count=%d len=%d alloc_space=%d\n",
+ count, len, sizeof(char*) * count + len);
+ while (*v) {
+ count_var_expansion_space(&count, &len, *v);
+ debug_printf_expand("count_var_expansion_space: "
+ "'%s' count=%d len=%d alloc_space=%d\n",
+ *v, count, len, sizeof(char*) * count + len);
+ v++;
+ }
+ len += sizeof(char*) * count; /* total to alloc */
+ list = xmalloc(len);
+ pos = (char*)(list + count);
+ debug_printf_expand("list=%p, list[0] should be %p\n", list, pos);
+ n = 0;
+ v = argv;
+ while (*v)
+ n = expand_vars_to_list(list, n, &pos, *v++, or_mask);
+
+ if (n) debug_printf_expand("finalized list[%d]=%p '%s' "
+ "strlen=%d next=%p pos=%p\n", n-1, list[n-1], list[n-1],
+ strlen(list[n-1]), list[n-1] + strlen(list[n-1]) + 1, pos);
+ list[n] = NULL;
+
+#ifdef DEBUG_EXPAND
+ {
+ int m = 0;
+ while (m <= n) {
+ debug_printf_expand("list[%d]=%p '%s'\n", m, list[m], list[m]);
+ m++;
+ }
+ debug_printf_expand("used_space=%d\n", pos - (char*)list);
+ }
+#endif
+ if (ENABLE_HUSH_DEBUG)
+ if (pos - (char*)list > len)
+ bb_error_msg_and_die("BUG in varexp");
+ return list;
+}
+
+static char **expand_strvec_to_strvec(char **argv)
+{
+ return expand_variables(argv, 0);
+}
+
+static char *expand_string_to_string(const char *str)
+{
+ char *argv[2], **list;
+
+ argv[0] = (char*)str;
+ argv[1] = NULL;
+ list = expand_variables(argv, 0x80); /* 0x80: make one-element expansion */
+ if (ENABLE_HUSH_DEBUG)
+ if (!list[0] || list[1])
+ bb_error_msg_and_die("BUG in varexp2");
+ /* actually, just move string 2*sizeof(char*) bytes back */
+ strcpy((char*)list, list[0]);
+ debug_printf_expand("string_to_string='%s'\n", (char*)list);
+ return (char*)list;
+}
+
+static char* expand_strvec_to_string(char **argv)
+{
+ char **list;
+
+ list = expand_variables(argv, 0x80);
+ /* Convert all NULs to spaces */
+ if (list[0]) {
+ int n = 1;
+ while (list[n]) {
+ if (ENABLE_HUSH_DEBUG)
+ if (list[n-1] + strlen(list[n-1]) + 1 != list[n])
+ bb_error_msg_and_die("BUG in varexp3");
+ list[n][-1] = ' '; /* TODO: or to ifs[0]? */
+ n++;
+ }
+ }
+ strcpy((char*)list, list[0]);
+ debug_printf_expand("strvec_to_string='%s'\n", (char*)list);
+ return (char*)list;
+}
+