diff --git a/cmd-invoke.c b/cmd-invoke.c index e0fcab448..98da1f131 100644 --- a/cmd-invoke.c +++ b/cmd-invoke.c @@ -47,6 +47,7 @@ struct cmd_invoke_state { int argc; char **argv; + int replaced; }; static void cmd_invoke_push(struct cmd_invoke_state *, @@ -182,6 +183,47 @@ cmd_invoke_tilde(const char *name) return (pw->pw_dir); } +/* Replace template markers in a parsed value without quoting or reparsing. */ +static char * +cmd_invoke_replace(struct cmd_invoke_state *is, const char *s) +{ + const char *cp = s, *value; + char *buf = NULL, ch[2] = { 0 }; + size_t len = 0; + int idx; + + if (is->argc == 0 || strchr(s, '%') == NULL) + return (xstrdup(s)); + + while (*cp != '\0') { + value = NULL; + if (*cp == '%') { + idx = cp[1] - '1'; + if (idx >= 0 && idx < 9 && idx < is->argc) { + value = is->argv[idx]; + cp += 2; + if (*cp == '%') + cp++; + } else if (cp[1] == '%' && !is->replaced) { + is->replaced = 1; + value = is->argv[0]; + cp += 2; + if (*cp == '%') + cp++; + } + } + if (value != NULL) + cmd_invoke_append(&buf, &len, value); + else { + ch[0] = *cp++; + cmd_invoke_append(&buf, &len, ch); + } + } + if (buf == NULL) + buf = xstrdup(""); + return (buf); +} + /* Expand a parsed string node into an argv string. */ static int cmd_invoke_expand_string(struct cmdq_item *item, struct cmd_invoke_state *is, @@ -191,7 +233,6 @@ cmd_invoke_expand_string(struct cmdq_item *item, struct cmd_invoke_state *is, const char *s, *value; char *buf = NULL, *new; size_t len = 0; - int i; child = cmd_parse_node_first_child(node); while (child != NULL) { @@ -214,8 +255,8 @@ cmd_invoke_expand_string(struct cmdq_item *item, struct cmd_invoke_state *is, } if (buf == NULL) buf = xstrdup(""); - for (i = 0; i < is->argc; i++) { - new = cmd_template_replace(buf, is->argv[i], i + 1); + if (is->argc != 0) { + new = cmd_invoke_replace(is, buf); free(buf); buf = new; } diff --git a/regress/cmd-template-replace.sh b/regress/cmd-template-replace.sh index 00730abc3..06e36a2b7 100644 --- a/regress/cmd-template-replace.sh +++ b/regress/cmd-template-replace.sh @@ -1,8 +1,8 @@ #!/bin/sh -# Exercise cmd_template_replace through command-prompt, which passes prompt -# input as argv to args_make_commands. This covers the quoting modes, indexed -# replacements, and cases where replacements are intentionally not made. +# Exercise invocation-time template replacement through command-prompt. String +# and braced templates are parsed before prompting; responses are literal argv +# values and cannot add arguments or commands to the stored tree. PATH=/bin:/usr/bin TERM=screen @@ -115,6 +115,26 @@ $IN bind -n M-i command-prompt -p 'one,two' 'set -g @double_index "%2%"' || $IN bind -n M-n command-prompt -p '(none)' \ "set -g @plain no-template-markers" || exit 1 +$IN bind -n M-u command-prompt -p '(unmatched)' \ + "set -g @plain '%9 %0 %'" || exit 1 + +# Command-valued bodies must have the same replacement behaviour as strings. +cat >"$TMP/bindings.conf" <<'EOF' +bind -n M-b command-prompt -p '(braced)' { set -g @r '%%' } +bind -n M-t command-prompt -p '(first)' { + set -g @first '%%' + set -g @second '%%' +} +bind -n M-v command-prompt -p 'one,two' { + set -g @one %1 + set -g @two %2 + set -g @two_again %2 + set -g @r '%%' +} +bind -n M-q command-prompt -p '(indexed)' { set -g @r %1 } +bind -n M-w command-prompt -p '(within)' { set -g @r '%%/%%' } +EOF +$IN source-file "$TMP/bindings.conf" || exit 1 $OUT new -d -x80 -y24 || exit 1 $OUT set -g status off || exit 1 @@ -125,8 +145,7 @@ sleep 1 reset_options -# %% is for templates already inside single quotes. A single quote in the -# replacement must stay data and must not close the surrounding quotes. +# Quoting has already been parsed. Quotes in a response stay literal data. payload="can't ; set -g @marker changed ; done" accept_prompt M-s "$payload" wait_option @r "$payload" @@ -139,18 +158,19 @@ accept_prompt M-f "$payload" wait_option @first "$payload" wait_option @second %% -# %%% keeps the previous double-quote escaping behaviour. +# %%% is accepted like %%, without adding quotation escapes to the value. reset_options payload='a"$;~\z' accept_prompt M-d "$payload" wait_option @r "$payload" -# %1 is intentionally left raw as an escape hatch. +# Indexed replacements are also data, so semicolons cannot add commands. reset_options $IN set -g @raw_tail unchanged || exit 1 -accept_prompt M-r 'raw ; set -g @raw_tail yes' -wait_option @r raw -wait_option @raw_tail yes +payload='raw ; set -g @raw_tail yes' +accept_prompt M-r "$payload" +wait_option @r "$payload" +wait_option @raw_tail unchanged # All instances of %1 are replaced. reset_options @@ -166,12 +186,56 @@ wait_option @one one wait_option @two two wait_option @two_again two -# %idx% uses double-quote escaping for indexed replacements. +# %idx% is accepted like %idx, with no quotation escaping. reset_options payload='b"$;~\y' accept_two_prompts M-i ignored "$payload" wait_option @double_index "$payload" +# Responses are not scanned again for markers from later prompt values. +reset_options +accept_two_prompts M-m '%2/%%' two +wait_option @one '%2/%%' +wait_option @two two +wait_option @two_again two + +# A missing response and invalid or trailing percent markers stay literal. +reset_options +accept_prompt M-u unused +wait_option @plain '%9 %0 %' + +# Reuse the stored string tree with a different response. Expansion must not +# mutate the tree or carry the first-%% state across separate invocations. +reset_options +payload="another'quote" +accept_prompt M-s "$payload" +wait_option @r "$payload" + +# Braced bodies preserve the same first-%% rule across commands and within an +# argument. Indexed replacements do not consume this first-%% marker. +reset_options +payload="brace'quote ; set -g @marker changed" +accept_prompt M-b "$payload" +wait_option @r "$payload" +wait_option @marker unchanged +accept_prompt M-t "$payload" +wait_option @first "$payload" +wait_option @second %% +accept_prompt M-w value +wait_option @r 'value/%%' + +reset_options +accept_two_prompts M-v '%2/%%' two +wait_option @one '%2/%%' +wait_option @two two +wait_option @two_again two +wait_option @r '%2/%%' +payload="can't ; set -g @marker changed" +payload="$payload"' ; "$HOME" ~ \ %2' +accept_prompt M-q "$payload" +wait_option @r "$payload" +wait_option @marker unchanged + # A template without replacement markers is left alone. reset_options accept_prompt M-n 'unused ; set -g @marker changed' diff --git a/tmux.1 b/tmux.1 index 580d2ba22..93bed36a3 100644 --- a/tmux.1 +++ b/tmux.1 @@ -8089,10 +8089,17 @@ Up to nine prompt responses may be replaced to .Ql %9 .Pc . +The template is parsed before prompting, and responses are substituted literally +into its argument values without being parsed again. +Spaces, quotes and semicolons in a response do not add arguments or commands. .Ql %%% -is like +is accepted like .Ql %% -but any quotation marks are escaped. +and +.Ql %1% +like +.Ql %1 , +without adding escapes. .Pp .Fl 1 makes the prompt only accept one key press, in this case the resulting input