Fix regression expanding \$()

When expanding command substitutions, we use a naïve way of detecting whether
the cmdsub has the optional leading dollar. We check if the last character was
a dollar, which breaks if it's an escaped dollar.  We wrongly expand
\$(echo "") to the empty string. Fix this by checking if the dollar was escaped.

The parse_util_* functions have a bunch of output parameters. We should
return a parameter bag instead (I think I tried once and failed).
This commit is contained in:
Johannes Altmanninger 2022-04-03 15:14:12 +02:00
parent d87bbf9433
commit 3e3f507012
5 changed files with 30 additions and 11 deletions

View File

@ -20,6 +20,7 @@ Deprecations and removed features
Scripting improvements Scripting improvements
---------------------- ----------------------
- Quoted command substitution that directly follow a variable expansion (like ``echo "$var$(echo x)"``) no longer affect the variable expansion (:issue:`8849`). - Quoted command substitution that directly follow a variable expansion (like ``echo "$var$(echo x)"``) no longer affect the variable expansion (:issue:`8849`).
- Fish now correctly expands command substitutions that are preceded by an escaped dollar (like ``echo \$(echo)``). This regressed in version 3.4.0.
- ``math`` can now handle underscores (``_``) as visual separators in numbers (:issue:`8611`, :issue:`8496`):: - ``math`` can now handle underscores (``_``) as visual separators in numbers (:issue:`8611`, :issue:`8496`)::
math 5 + 2_123_252 math 5 + 2_123_252

View File

@ -626,8 +626,9 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
wcstring subcmd; wcstring subcmd;
bool is_quoted = false; bool is_quoted = false;
bool has_dollar = false;
switch (parse_util_locate_cmdsubst_range(input, &cursor, &subcmd, &paren_begin, &paren_end, switch (parse_util_locate_cmdsubst_range(input, &cursor, &subcmd, &paren_begin, &paren_end,
false, &is_quoted)) { false, &is_quoted, &has_dollar)) {
case -1: { case -1: {
append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, L"Mismatched parenthesis"); append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, L"Mismatched parenthesis");
return expand_result_t::make_error(STATUS_EXPAND_ERROR); return expand_result_t::make_error(STATUS_EXPAND_ERROR);
@ -646,8 +647,6 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
} }
} }
bool have_dollar = paren_begin > 0 && input.at(paren_begin - 1) == L'$';
wcstring_list_t sub_res; wcstring_list_t sub_res;
int subshell_status = exec_subshell_for_expand(subcmd, *ctx.parser, ctx.job_group, sub_res); int subshell_status = exec_subshell_for_expand(subcmd, *ctx.parser, ctx.job_group, sub_res);
if (subshell_status != 0) { if (subshell_status != 0) {
@ -757,7 +756,7 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
wcstring whole_item; wcstring whole_item;
whole_item.reserve(paren_begin + 1 + sub_res_joined.size() + 1 + whole_item.reserve(paren_begin + 1 + sub_res_joined.size() + 1 +
tail_item.completion.size()); tail_item.completion.size());
whole_item.append(input, 0, paren_begin - have_dollar); whole_item.append(input, 0, paren_begin - has_dollar);
whole_item.push_back(INTERNAL_SEPARATOR); whole_item.push_back(INTERNAL_SEPARATOR);
whole_item.append(sub_res_joined); whole_item.append(sub_res_joined);
whole_item.push_back(INTERNAL_SEPARATOR); whole_item.push_back(INTERNAL_SEPARATOR);
@ -776,7 +775,7 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
wcstring whole_item; wcstring whole_item;
whole_item.reserve(paren_begin + 1 + sub_item2.size() + 1 + whole_item.reserve(paren_begin + 1 + sub_item2.size() + 1 +
tail_item.completion.size()); tail_item.completion.size());
whole_item.append(input, 0, paren_begin - have_dollar); whole_item.append(input, 0, paren_begin - has_dollar);
whole_item.push_back(INTERNAL_SEPARATOR); whole_item.push_back(INTERNAL_SEPARATOR);
whole_item.append(sub_item2); whole_item.append(sub_item2);
whole_item.push_back(INTERNAL_SEPARATOR); whole_item.push_back(INTERNAL_SEPARATOR);

View File

@ -91,7 +91,8 @@ size_t parse_util_get_offset(const wcstring &str, int line, long line_offset) {
} }
static int parse_util_locate_cmdsub(const wchar_t *in, const wchar_t **begin, const wchar_t **end, static int parse_util_locate_cmdsub(const wchar_t *in, const wchar_t **begin, const wchar_t **end,
bool allow_incomplete, bool *inout_is_quoted) { bool allow_incomplete, bool *inout_is_quoted,
bool *out_has_dollar) {
bool escaped = false; bool escaped = false;
bool is_first = true; bool is_first = true;
bool is_token_begin = true; bool is_token_begin = true;
@ -104,10 +105,12 @@ static int parse_util_locate_cmdsub(const wchar_t *in, const wchar_t **begin, co
assert(in && "null parameter"); assert(in && "null parameter");
const wchar_t *pos = in; const wchar_t *pos = in;
const wchar_t *last_dollar = nullptr;
auto process_opening_quote = [&](wchar_t quote) -> bool /* ok */ { auto process_opening_quote = [&](wchar_t quote) -> bool /* ok */ {
const wchar_t *q_end = quote_end(pos, quote); const wchar_t *q_end = quote_end(pos, quote);
if (!q_end) return false; if (!q_end) return false;
if (*q_end == L'$') { if (*q_end == L'$') {
last_dollar = q_end;
quoted_cmdsubs.push_back(paran_count); quoted_cmdsubs.push_back(paran_count);
} }
// We want to report whether the outermost comand substitution between // We want to report whether the outermost comand substitution between
@ -131,10 +134,15 @@ static int parse_util_locate_cmdsub(const wchar_t *in, const wchar_t **begin, co
escaped = true; escaped = true;
} else if (*pos == L'#' && is_token_begin) { } else if (*pos == L'#' && is_token_begin) {
pos = comment_end(pos) - 1; pos = comment_end(pos) - 1;
} else if (*pos == L'$') {
last_dollar = pos;
} else { } else {
if (*pos == L'(') { if (*pos == L'(') {
if ((paran_count == 0) && (paran_begin == nullptr)) { if ((paran_count == 0) && (paran_begin == nullptr)) {
paran_begin = pos; paran_begin = pos;
if (out_has_dollar) {
*out_has_dollar = last_dollar == pos - 1;
}
} }
paran_count++; paran_count++;
@ -151,7 +159,7 @@ static int parse_util_locate_cmdsub(const wchar_t *in, const wchar_t **begin, co
break; break;
} }
// Check if the ) did complete a quoted command substituion. // Check if the ) did complete a quoted command substitution.
if (!quoted_cmdsubs.empty() && quoted_cmdsubs.back() == paran_count) { if (!quoted_cmdsubs.empty() && quoted_cmdsubs.back() == paran_count) {
quoted_cmdsubs.pop_back(); quoted_cmdsubs.pop_back();
// Quoted command substitutions temporarily close double quotes. // Quoted command substitutions temporarily close double quotes.
@ -244,7 +252,8 @@ long parse_util_slice_length(const wchar_t *in) {
int parse_util_locate_cmdsubst_range(const wcstring &str, size_t *inout_cursor_offset, int parse_util_locate_cmdsubst_range(const wcstring &str, size_t *inout_cursor_offset,
wcstring *out_contents, size_t *out_start, size_t *out_end, wcstring *out_contents, size_t *out_start, size_t *out_end,
bool accept_incomplete, bool *inout_is_quoted) { bool accept_incomplete, bool *inout_is_quoted,
bool *out_has_dollar) {
// Clear the return values. // Clear the return values.
if (out_contents != nullptr) out_contents->clear(); if (out_contents != nullptr) out_contents->clear();
*out_start = 0; *out_start = 0;
@ -261,7 +270,7 @@ int parse_util_locate_cmdsubst_range(const wcstring &str, size_t *inout_cursor_o
const wchar_t *bracket_range_end = nullptr; const wchar_t *bracket_range_end = nullptr;
int ret = parse_util_locate_cmdsub(valid_range_start, &bracket_range_begin, &bracket_range_end, int ret = parse_util_locate_cmdsub(valid_range_start, &bracket_range_begin, &bracket_range_end,
accept_incomplete, inout_is_quoted); accept_incomplete, inout_is_quoted, out_has_dollar);
if (ret <= 0) { if (ret <= 0) {
return ret; return ret;
} }
@ -303,7 +312,7 @@ void parse_util_cmdsubst_extent(const wchar_t *buff, size_t cursor_pos, const wc
const wchar_t *pos = buff; const wchar_t *pos = buff;
for (;;) { for (;;) {
const wchar_t *begin = nullptr, *end = nullptr; const wchar_t *begin = nullptr, *end = nullptr;
if (parse_util_locate_cmdsub(pos, &begin, &end, true, nullptr) <= 0) { if (parse_util_locate_cmdsub(pos, &begin, &end, true, nullptr, nullptr) <= 0) {
// No subshell found, all done. // No subshell found, all done.
break; break;
} }

View File

@ -30,10 +30,12 @@ long parse_util_slice_length(const wchar_t *in);
/// the end of the string if it was incomplete /// the end of the string if it was incomplete
/// \param accept_incomplete whether to permit missing closing parenthesis /// \param accept_incomplete whether to permit missing closing parenthesis
/// \param inout_is_quoted whether the cursor is in a double-quoted context. /// \param inout_is_quoted whether the cursor is in a double-quoted context.
/// \param out_has_dollar whether the command substitution has the optional leading $.
/// \return -1 on syntax error, 0 if no subshells exist and 1 on success /// \return -1 on syntax error, 0 if no subshells exist and 1 on success
int parse_util_locate_cmdsubst_range(const wcstring &str, size_t *inout_cursor_offset, int parse_util_locate_cmdsubst_range(const wcstring &str, size_t *inout_cursor_offset,
wcstring *out_contents, size_t *out_start, size_t *out_end, wcstring *out_contents, size_t *out_start, size_t *out_end,
bool accept_incomplete, bool *inout_is_quoted = nullptr); bool accept_incomplete, bool *inout_is_quoted = nullptr,
bool *out_has_dollar = nullptr);
/// Find the beginning and end of the command substitution under the cursor. If no subshell is /// Find the beginning and end of the command substitution under the cursor. If no subshell is
/// found, the entire string is returned. If the current command substitution is not ended, i.e. the /// found, the entire string is returned. If the current command substitution is not ended, i.e. the

View File

@ -66,3 +66,11 @@ echo "quoted1""quoted2"(echo unquoted3)"$(echo quoted4)_$(echo quoted5)"
var=a echo "$var$(echo b)" var=a echo "$var$(echo b)"
# CHECK: ab # CHECK: ab
# Make sure we don't swallow an escaped dollar.
echo \$(echo 1)
# CHECK: $1
echo "\$(echo 1)"
# CHECK: $(echo 1)
echo "\$$(echo 1)"
# CHECK: $1