Do not escape in %%.

This commit is contained in:
Nicholas Marriott
2026-10-06 18:08:03 +01:00
parent b5865a6d29
commit d65192c6af
3 changed files with 128 additions and 16 deletions

View File

@@ -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;
}

View File

@@ -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'

11
tmux.1
View File

@@ -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