Always treat brace at command start as compound statement

For backwards compatibility, fish does not treat "{echo,hello}" as a compound
statement but as brace expansion (effectively "echo hello").  We interpret
"{X...}" as compound statement only if X is whitespace or ';' (which is an
interesting solution).

A brace expansion at the very start of a command 
is usually pointless (space separation is shorter).
The exception are cases where the command name and the first few arguments
share a suffix.

	$ {,1,2,3,4}echo
	1echo 2echo 3echo 4echo

Not sure if anyone uses anything like that.  Perhaps we want to trade
compatibility for simplicity. I don't have a strong opinion on this.

Always parse the opening brace as first character of a command token as
compound statement.
Brace expansion can still be used with a trick like: «''{echo,foo}»

Closes #11477
This commit is contained in:
Johannes Altmanninger
2025-01-14 23:33:39 +01:00
parent a86a4dfabf
commit 80e30ac756
7 changed files with 38 additions and 44 deletions

View File

@@ -21,7 +21,7 @@ Notable improvements and fixes
Deprecations and removed features
---------------------------------
- Tokens like ``{ echo, echo }`` in command position are no longer interpreted as brace expansion but as compound command.
- Tokens like ``{echo,echo}`` or ``{ echo, echo }`` in command position are no longer interpreted as brace expansion but as compound command.
- Terminfo-style key names (``bind -k``) are no longer supported. They had been superseded by the native notation since 4.0,
and currently they would map back to information from terminfo, which does not match what terminals would send with the kitty keyboard protocol (:issue:`11342`).
- fish no longer reads the terminfo database, so its behavior is no longer affected by the :envvar:`TERM` environment variable (:issue:`11344`).

View File

@@ -917,6 +917,12 @@ If there is nothing between a brace and a comma or two commas, it's interpreted
To use a "," as an element, :ref:`quote <quotes>` or :ref:`escape <escapes>` it.
The very first character of a command token is never interpreted as expanding brace, because it's the beginning of a :ref:`compound statement <cmd-begin>`::
> {echo hello, && echo world}
hello,
world
.. _cartesian-product:
Combining lists

View File

@@ -13,7 +13,6 @@
ast::unescape_keyword,
common::charptr2wcstring,
reader::{get_quote, is_backslashed},
tokenizer::is_brace_statement,
util::wcsfilecmp,
wutil::sprintf,
};
@@ -667,20 +666,7 @@ fn perform_for_commandline_impl(&mut self, cmdline: WString) {
// Get all the arguments.
let mut tokens = Vec::new();
{
let proc_range =
parse_util_process_extent(&cmdline, position_in_statement, Some(&mut tokens));
let start = proc_range.start;
if start != 0
&& cmdline.as_char_slice()[start - 1] == '{'
&& (start == cmdline.len()
|| !is_brace_statement(cmdline.as_char_slice().get(start).copied()))
{
// We don't want to suggest commands here, since this command line parses as
// brace expansion.
return;
}
}
parse_util_process_extent(&cmdline, position_in_statement, Some(&mut tokens));
let actual_token_count = tokens.len();
// Hack: fix autosuggestion by removing prefixing "and"s #6249.

View File

@@ -57,9 +57,8 @@ fn test_tokenizer() {
let s = L!("{echo, foo}");
let mut t = Tokenizer::new(s, TokFlags(0));
let token = t.next().unwrap();
assert_eq!(token.type_, TokenType::string);
assert_eq!(token.length, 11);
assert!(t.next().is_none());
assert_eq!(token.type_, TokenType::left_brace);
assert_eq!(token.length, 1);
}
{
let s = L!("{ echo; foo}");

View File

@@ -290,10 +290,6 @@ pub struct Tokenizer<'c> {
on_quote_toggle: Option<&'c mut dyn FnMut(usize)>,
}
pub(crate) fn is_brace_statement(next_char: Option<char>) -> bool {
next_char.map_or(true, |next| next.is_ascii_whitespace() || next == ';')
}
impl<'c> Tokenizer<'c> {
/// Constructor for a tokenizer. b is the string that is to be tokenized. It is not copied, and
/// should not be freed by the caller until after the tokenizer is destroyed.
@@ -426,9 +422,7 @@ fn next(&mut self) -> Option<Self::Item> {
Some(result)
}
'{' if self.brace_statement_parser.as_ref()
.is_some_and(|parser| parser.at_command_position)
&& is_brace_statement(self.start.as_char_slice().get(self.token_cursor + 1).copied())
=>
.is_some_and(|parser| parser.at_command_position) =>
{
self.brace_statement_parser.as_mut().unwrap().unclosed_brace_statements += 1;
let mut result = Tok::new(TokenType::left_brace);

View File

@@ -67,26 +67,33 @@ e{cho,cho,cho}
{ echo no semi }
# CHECK: no semi
# Ambiguous cases
{echo no space}
# CHECK: no space
# Ambiguous case
{ echo ,comma;}
# CHECK: ,comma
PATH= {echo no space}
# CHECKERR: fish: Unknown command: '{echo no space}'
# CHECKERR: {{.*}}/braces.fish (line {{\d+}}):
# CHECKERR: PATH= {echo no space}
# CHECKERR: ^~~~~~~~~~~~~~^
PATH= {echo comma, no space;}
# CHECKERR: fish: Unknown command: 'echo comma'
# CHECKERR: {{.*}}/braces.fish (line {{\d+}}):
# CHECKERR: PATH= {echo comma, no space;}
# CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~^
# Ambiguous case with no trailing space
{echo comma, no space;}
# CHECK: comma, no space
# Ambiguous case with no space
{echo,hello}
# CHECK: hello
PATH= {echo,hello}
# CHECKERR: fish: Unknown command: echo,hello
# CHECKERR: {{.*}}/braces.fish (line {{\d+}}):
# CHECKERR: PATH= {echo,hello}
# CHECKERR: ^~~~~~~~~^
function foo,
echo foo,
end
function bar
echo bar
end
{foo,;bar}
# CHECK: foo,
# CHECK: bar
# Trailing tokens
set -l fish (status fish-path)
@@ -157,7 +164,7 @@ end
}
# CHECK: while
{ { echo inner}
{{echo inner}
echo outer}
# CHECK: inner
# CHECK: outer
@@ -172,9 +179,8 @@ complete foo -a '123 456'
complete -C 'foo {' | sed 1q
# CHECK: {{\{.*}}
complete -C '{'
echo nothing
# CHECK: nothing
complete -C '{' | grep ^if\t
# CHECK: if{{\t}}Evaluate block if condition is true
complete -C '{ ' | grep ^if\t
# CHECK: if{{\t}}Evaluate block if condition is true

View File

@@ -459,6 +459,9 @@ echo \\
echo '{ { } }'
# CHECK: { { } }
echo '{{}}'
# CHECK: { { } }
echo '
{