5f018e1bbac8c98e20a4e4692f5f8c25026ee464 — Simon Ser 26 days ago b51626f
shell/task/word: implement positional parameter expansion

My life is a perfect graveyard of buried hopes.
― L.M. Montgomery
2 files changed, 116 insertions(+), 5 deletions(-)

M shell/task/word.c
M test/word.sh
M shell/task/word.c => shell/task/word.c +98 -5
@@ 140,6 140,33 @@ return mrsh_env_get(state, name, NULL);
  }
  
+ static struct mrsh_word *expand_positional_params(struct mrsh_state *state,
+ 		bool quote_args) {
+ 	const char *ifs = mrsh_env_get(state, "IFS", NULL);
+ 	char sep[2] = {0};
+ 	if (ifs == NULL) {
+ 		sep[0] = ' ';
+ 	} else {
+ 		sep[0] = ifs[0];
+ 	}
+ 
+ 	struct mrsh_array words = {0};
+ 	for (int i = 1; i < state->frame->argc; i++) {
+ 		const char *arg = state->frame->argv[i];
+ 		if (i > 1 && sep[0] != '\0') {
+ 			struct mrsh_word_string *ws =
+ 				mrsh_word_string_create(strdup(sep), false);
+ 			mrsh_array_add(&words, &ws->word);
+ 		}
+ 		struct mrsh_word_string *ws =
+ 			mrsh_word_string_create(strdup(arg), quote_args);
+ 		mrsh_array_add(&words, &ws->word);
+ 	}
+ 
+ 	struct mrsh_word_list *wl = mrsh_word_list_create(&words, false);
+ 	return &wl->word;
+ }
+ 
  static struct mrsh_word *create_word_string(const char *str) {
  	struct mrsh_word_string *ws = mrsh_word_string_create(strdup(str), false);
  	return &ws->word;


@@ 357,7 384,8 @@ }
  }
  
- int run_word(struct context *ctx, struct mrsh_word **word_ptr) {
+ static int _run_word(struct context *ctx, struct mrsh_word **word_ptr,
+ 		bool double_quoted) {
  	struct mrsh_word *word = *word_ptr;
  
  	int ret;


@@ 386,8 414,13 @@ case MRSH_PARAM_QMARK:
  		case MRSH_PARAM_PLUS:;
  			struct mrsh_word *value_word;
- 			if (strcmp(wp->name, "@") == 0 || strcmp(wp->name, "*") == 0) {
- 				assert(false); // TODO
+ 			if (strcmp(wp->name, "@") == 0) {
+ 				// $@ expands to quoted fields only if it's inside double quotes
+ 				// TODO: error out if expansion is unspecified
+ 				value_word = expand_positional_params(ctx->state,
+ 					double_quoted);
+ 			} else if (strcmp(wp->name, "*") == 0) {
+ 				value_word = expand_positional_params(ctx->state, false);
  			} else if (value != NULL) {
  				value_word = create_word_string(value);
  			} else {


@@ 482,17 515,77 @@ mrsh_parser_destroy(parser);
  		return ret;
  	case MRSH_WORD_LIST:;
- 		// For word lists, we just need to expand each word
  		struct mrsh_word_list *wl = mrsh_word_get_list(word);
+ 
+ 		struct mrsh_array at_sign_words = {0};
  		for (size_t i = 0; i < wl->children.len; ++i) {
  			struct mrsh_word **child_ptr =
  				(struct mrsh_word **)&wl->children.data[i];
- 			ret = run_word(ctx, child_ptr);
+ 
+ 			bool is_at_sign = false;
+ 			if ((*child_ptr)->type == MRSH_WORD_PARAMETER) {
+ 				struct mrsh_word_parameter *wp =
+ 					mrsh_word_get_parameter(*child_ptr);
+ 				is_at_sign = strcmp(wp->name, "@") == 0;
+ 			}
+ 
+ 			ret = _run_word(ctx, child_ptr,
+ 				double_quoted || wl->double_quoted);
  			if (ret < 0) {
  				return ret;
  			}
+ 
+ 			if (wl->double_quoted && is_at_sign) {
+ 				// Fucking $@ needs special handling: we need to extract the
+ 				// fields it expands to outside of the double quotes
+ 				mrsh_array_add(&at_sign_words, *child_ptr);
+ 			}
  		}
+ 
+ 		if (at_sign_words.len > 0) {
+ 			// We need to put $@ expansions outside of the double quotes.
+ 			// Disclaimer: this is a PITA.
+ 			struct mrsh_array quoted = {0};
+ 			struct mrsh_array unquoted = {0};
+ 			size_t at_sign_idx = 0;
+ 			for (size_t i = 0; i < wl->children.len; i++) {
+ 				struct mrsh_word *child = wl->children.data[i];
+ 				wl->children.data[i] = NULL; // steal the child
+ 				if (at_sign_idx >= at_sign_words.len ||
+ 						child != at_sign_words.data[at_sign_idx]) {
+ 					mrsh_array_add(&quoted, child);
+ 					continue;
+ 				}
+ 
+ 				if (quoted.len > 0) {
+ 					struct mrsh_word_list *quoted_wl =
+ 						mrsh_word_list_create(&quoted, true);
+ 					mrsh_array_add(&unquoted, &quoted_wl->word);
+ 					// `unquoted` has been stolen by mrsh_word_list_create
+ 					quoted = (struct mrsh_array){0};
+ 				}
+ 
+ 				mrsh_array_add(&unquoted, at_sign_words.data[at_sign_idx]);
+ 
+ 				at_sign_idx++;
+ 			}
+ 			if (quoted.len > 0) {
+ 				struct mrsh_word_list *quoted_wl =
+ 					mrsh_word_list_create(&quoted, true);
+ 				mrsh_array_add(&unquoted, &quoted_wl->word);
+ 			}
+ 
+ 			struct mrsh_word_list *unquoted_wl =
+ 				mrsh_word_list_create(&unquoted, false);
+ 			swap_words(word_ptr, &unquoted_wl->word);
+ 		}
+ 		mrsh_array_finish(&at_sign_words);
+ 
  		return 0;
  	}
  	assert(false);
  }
+ 
+ int run_word(struct context *ctx, struct mrsh_word **word_ptr) {
+ 	return _run_word(ctx, word_ptr, false);
+ }

M test/word.sh => test/word.sh +18 -0
@@ 26,6 26,24 @@ echo ${a+GOOD} ${idontexist+BAD} ${null+GOOD} ${null+}
  echo ${#hello} ${#null} ${#idontexist}
  
+ nargs() {
+ 	echo "$#"
+ }
+ 
+ set a 'b   c' d '	e'
+ echo $*
+ nargs $*
+ echo "$*"
+ nargs "$*"
+ echo $@
+ nargs $@
+ echo "$@"
+ nargs "$@"
+ echo "1  $@  2"
+ nargs "1  $@  2"
+ echo ${null:-"$@"}
+ nargs ${null:-"$@"}
+ 
  # Examples from the spec
  # ${parameter}: dash and busybox choke on this
  #a=1