Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions crates/deno_task_shell/src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@ INT = { ("+" | "-")? ~ ASCII_DIGIT+ }
// Basic tokens
QUOTED_WORD = { DOUBLE_QUOTED | SINGLE_QUOTED }

UNQUOTED_PENDING_WORD = ${
(TILDE_PREFIX ~ (!(OPERATOR | WHITESPACE | NEWLINE) ~ (
EXIT_STATUS |
UNQUOTED_ESCAPE_CHAR |
UNQUOTED_PENDING_WORD = ${
(TILDE_PREFIX ~ (BRACE_EXPANSION | !(OPERATOR | WHITESPACE | NEWLINE) ~ (
EXIT_STATUS |
UNQUOTED_ESCAPE_CHAR |
"$" ~ ARITHMETIC_EXPRESSION |
SUB_COMMAND |
VARIABLE_EXPANSION |
UNQUOTED_CHAR |
SUB_COMMAND |
VARIABLE_EXPANSION |
UNQUOTED_CHAR |
QUOTED_WORD
))*)
|
(!(OPERATOR | WHITESPACE | NEWLINE) ~ (
EXIT_STATUS |
UNQUOTED_ESCAPE_CHAR |
|
(BRACE_EXPANSION | !(OPERATOR | WHITESPACE | NEWLINE) ~ (
EXIT_STATUS |
UNQUOTED_ESCAPE_CHAR |
"$" ~ ARITHMETIC_EXPRESSION |
SUB_COMMAND |
VARIABLE_EXPANSION |
UNQUOTED_CHAR |
SUB_COMMAND |
VARIABLE_EXPANSION |
UNQUOTED_CHAR |
QUOTED_WORD
))+
}
Expand Down Expand Up @@ -92,6 +92,26 @@ VARIABLE_EXPANSION = ${
)
}

BRACE_EXPANSION = ${
"{" ~ (BRACE_SEQUENCE | BRACE_LIST) ~ "}"
}

BRACE_SEQUENCE = ${
BRACE_ELEMENT ~ ".." ~ BRACE_ELEMENT ~ (".." ~ BRACE_STEP)?
}

BRACE_LIST = ${
BRACE_ELEMENT ~ ("," ~ BRACE_ELEMENT)+
}

BRACE_ELEMENT = ${
(!(OPERATOR | WHITESPACE | NEWLINE | "," | "}" | "{" | "." | "$" | "\\" | "\"" | "'") ~ ANY)*
}

BRACE_STEP = ${
("+" | "-")? ~ ASCII_DIGIT+
}

SPECIAL_PARAM = ${ ARGNUM | "@" | "#" | "?" | "$" | "*" }
ARGNUM = ${ ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* | "0" }
VARIABLE = ${ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
Expand Down
56 changes: 56 additions & 0 deletions crates/deno_task_shell/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,19 @@ pub enum VariableModifier {
AlternateValue(Word),
}

#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
#[derive(Debug, PartialEq, Eq, Clone, Error)]
#[error("Invalid brace expansion")]
pub enum BraceExpansion {
List(Vec<String>),
Sequence {
start: String,
end: String,
step: Option<i32>,
},
}

#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
#[cfg_attr(
feature = "serialization",
Expand All @@ -439,6 +452,8 @@ pub enum WordPart {
Arithmetic(Arithmetic),
#[error("Invalid exit status")]
ExitStatus,
#[error("Invalid brace expansion")]
BraceExpansion(BraceExpansion),
}

#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
Expand Down Expand Up @@ -1432,6 +1447,10 @@ fn parse_word(pair: Pair<Rule>) -> Result<Word> {
parse_variable_expansion(part)?;
parts.push(variable_expansion);
}
Rule::BRACE_EXPANSION => {
let brace_expansion = parse_brace_expansion(part)?;
parts.push(WordPart::BraceExpansion(brace_expansion));
}
Rule::QUOTED_WORD => {
let quoted = parse_quoted_word(part)?;
parts.push(quoted);
Expand Down Expand Up @@ -1822,6 +1841,43 @@ fn parse_variable_expansion(part: Pair<Rule>) -> Result<WordPart> {
Ok(WordPart::Variable(variable_name, parsed_modifier))
}

fn parse_brace_expansion(pair: Pair<Rule>) -> Result<BraceExpansion> {
let inner = pair
.into_inner()
.next()
.ok_or_else(|| miette!("Expected brace expansion content"))?;

match inner.as_rule() {
Rule::BRACE_LIST => {
let elements: Vec<String> = inner
.into_inner()
.map(|elem| elem.as_str().to_string())
.collect();
Ok(BraceExpansion::List(elements))
}
Rule::BRACE_SEQUENCE => {
let mut parts = inner.into_inner();
let start = parts
.next()
.ok_or_else(|| miette!("Expected sequence start"))?
.as_str()
.to_string();
let end = parts
.next()
.ok_or_else(|| miette!("Expected sequence end"))?
.as_str()
.to_string();
let step =
parts.next().map(|s| s.as_str().parse::<i32>().unwrap_or(1));
Ok(BraceExpansion::Sequence { start, end, step })
}
_ => Err(miette!(
"Unexpected rule in brace expansion: {:?}",
inner.as_rule()
)),
}
}

fn parse_tilde_prefix(pair: Pair<Rule>) -> Result<WordPart> {
let tilde_prefix_str = pair.as_str();
let user = if tilde_prefix_str.len() > 1 {
Expand Down
104 changes: 104 additions & 0 deletions crates/deno_task_shell/src/shell/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use tokio_util::sync::CancellationToken;

use crate::parser::AssignmentOp;
use crate::parser::BinaryOp;
use crate::parser::BraceExpansion;
use crate::parser::CaseClause;
use crate::parser::Condition;
use crate::parser::ConditionInner;
Expand Down Expand Up @@ -1875,6 +1876,100 @@ pub enum IsQuoted {
No,
}

/// Expands a brace expansion into a vector of strings
fn expand_braces(
brace_expansion: &BraceExpansion,
) -> Result<Vec<String>, Error> {
match brace_expansion {
BraceExpansion::List(elements) => Ok(elements.clone()),
BraceExpansion::Sequence { start, end, step } => {
expand_sequence(start, end, step.as_ref())
}
}
}

/// Expands a sequence like {1..10} or {a..z} or {1..10..2}
fn expand_sequence(
start: &str,
end: &str,
step: Option<&i32>,
) -> Result<Vec<String>, Error> {
// Try to parse as integers first
if let (Ok(start_num), Ok(end_num)) =
(start.parse::<i32>(), end.parse::<i32>())
{
let is_reverse = start_num > end_num;
let mut step_val =
step.copied().unwrap_or(if is_reverse { -1 } else { 1 });

// If step is provided and its sign doesn't match the direction, flip it
if step.is_some() && (is_reverse != (step_val < 0)) {
step_val = -step_val;
}

if step_val == 0 {
return Err(miette!("Invalid step value: 0"));
}

let mut result = Vec::new();
if step_val > 0 {
let mut current = start_num;
while current <= end_num {
result.push(current.to_string());
current += step_val;
}
} else {
let mut current = start_num;
while current >= end_num {
result.push(current.to_string());
current += step_val;
}
}
Ok(result)
}
// Try to parse as single characters
else if start.len() == 1 && end.len() == 1 {
let start_char = start.chars().next().unwrap();
let end_char = end.chars().next().unwrap();
let is_reverse = start_char > end_char;
let mut step_val =
step.copied().unwrap_or(if is_reverse { -1 } else { 1 });

// If step is provided and direction is reverse, negate the step
// Or if step is provided and direction is forward, ensure step is positive
if step.is_some() && (is_reverse && step_val > 0)
|| (!is_reverse && step_val < 0)
{
step_val = -step_val;
}

if step_val == 0 {
return Err(miette!("Invalid step value: 0"));
}

let mut result = Vec::new();
if step_val > 0 {
let mut current = start_char as i32;
let end_val = end_char as i32;
while current <= end_val {
result.push((current as u8 as char).to_string());
current += step_val;
}
} else {
let mut current = start_char as i32;
let end_val = end_char as i32;
while current >= end_val {
result.push((current as u8 as char).to_string());
current += step_val;
}
}
Ok(result)
} else {
// If it's not a valid sequence, return it as-is (bash behavior)
Ok(vec![format!("{{{}..{}}}", start, end)])
}
}

fn evaluate_word_parts(
parts: Vec<WordPart>,
state: &mut ShellState,
Expand Down Expand Up @@ -2101,6 +2196,15 @@ fn evaluate_word_parts(
.push(TextPart::Text(exit_code.to_string()));
continue;
}
WordPart::BraceExpansion(brace_expansion) => {
let expanded = expand_braces(&brace_expansion)?;
Ok(Some(Text::new(
expanded
.into_iter()
.map(TextPart::Text)
.collect(),
)))
}
};

if let Ok(Some(text)) = evaluation_result_text {
Expand Down
39 changes: 39 additions & 0 deletions crates/tests/test-data/brace_expansion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Basic comma-separated list
> echo {a,b,c}
a b c

# Numeric sequence
> echo {1..5}
1 2 3 4 5

# Character sequence
> echo {a..e}
a b c d e

# Reverse numeric sequence
> echo {5..1}
5 4 3 2 1

# Reverse character sequence
> echo {e..a}
e d c b a

# Numeric sequence with step
> echo {1..10..2}
1 3 5 7 9

# Numeric sequence with step (reverse)
> echo {10..1..2}
10 8 6 4 2

# Empty elements in list
> echo {a,,b}
a b

# Quoted braces - should not expand
> echo "{a,b,c}"
{a,b,c}

# Mixed with other arguments
> echo start {a,b,c} end
start a b c end
Loading