Skip to content

Commit c549a19

Browse files
committed
Expand "$@" to one word per positional parameter
"$@" expanded to a single word with all positional parameters joined by spaces, so arguments could not be forwarded intact. Expand it to one word per parameter, as Bash does: text around it joins the first and last parameters, and with no parameters "$@" gives no word at all. Unquoted $@ also gives one word per (non-empty) parameter. shift now updates $@ and $* as well. Fixes #291
1 parent 60bdc49 commit c549a19

4 files changed

Lines changed: 140 additions & 5 deletions

File tree

‎crates/deno_task_shell/src/shell/commands/shift.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,14 @@ impl ShellCommand for ShiftCommand {
7070
changes.push(EnvChange::UnsetVar(idx.to_string()));
7171
}
7272

73-
// Update $# (parameter count)
73+
// Update $# (parameter count), $@ and $*
7474
changes.push(EnvChange::SetShellVar(
7575
"#".to_string(),
7676
remaining.len().to_string(),
7777
));
78+
let joined = remaining.join(" ");
79+
changes.push(EnvChange::SetShellVar("@".to_string(), joined.clone()));
80+
changes.push(EnvChange::SetShellVar("*".to_string(), joined));
7881

7982
Box::pin(futures::future::ready(ExecuteResult::Continue(
8083
0,

‎crates/deno_task_shell/src/shell/execute.rs‎

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2553,6 +2553,26 @@ fn expand_brace_word_parts(
25532553
Ok(result)
25542554
}
25552555

2556+
/// Whether a variable expansion is `$@` or `${@}` without a modifier.
2557+
fn is_all_params(
2558+
name: &str,
2559+
modifier: &Option<Box<VariableModifier>>,
2560+
indirect: bool,
2561+
) -> bool {
2562+
name == "@" && modifier.is_none() && !indirect
2563+
}
2564+
2565+
/// The positional parameters `$1` to `${N}`, where N is `$#`.
2566+
fn positional_params(state: &ShellState) -> Vec<String> {
2567+
let count = state
2568+
.get_var("#")
2569+
.and_then(|n| n.parse::<usize>().ok())
2570+
.unwrap_or(0);
2571+
(1..=count)
2572+
.map(|i| state.get_var(&i.to_string()).cloned().unwrap_or_default())
2573+
.collect()
2574+
}
2575+
25562576
fn evaluate_word_parts(
25572577
parts: Vec<WordPart>,
25582578
state: &mut ShellState,
@@ -2676,7 +2696,8 @@ fn evaluate_word_parts(
26762696
async move {
26772697
let mut result = WordPartsResult::new(Vec::new(), Vec::new());
26782698
let mut current_text = Vec::new();
2679-
for part in parts {
2699+
let mut parts = std::collections::VecDeque::from(parts);
2700+
while let Some(part) = parts.pop_front() {
26802701
let evaluation_result_text: Result<Option<Text>, Error> =
26812702
match part {
26822703
WordPart::Text(text) => {
@@ -2694,7 +2715,18 @@ fn evaluate_word_parts(
26942715
} else {
26952716
name.clone()
26962717
};
2697-
if let Some(modifier) = modifier {
2718+
if is_all_params(&resolved_name, &modifier, indirect)
2719+
{
2720+
// Unquoted $@ expands to one word per
2721+
// non-empty positional parameter
2722+
Ok(Some(Text::new(
2723+
positional_params(state)
2724+
.into_iter()
2725+
.filter(|p| !p.is_empty())
2726+
.map(OtherText)
2727+
.collect(),
2728+
)))
2729+
} else if let Some(modifier) = modifier {
26982730
let (text, env_changes) = modifier
26992731
.apply(
27002732
&resolved_name,
@@ -2734,9 +2766,51 @@ fn evaluate_word_parts(
27342766
.await;
27352767
Ok(Some(cmd.into()))
27362768
}
2737-
WordPart::Quoted(parts) => {
2769+
WordPart::Quoted(mut quoted_parts) => {
2770+
// "$@" expands to one word per positional
2771+
// parameter: "pre$@post" with parameters a and b
2772+
// gives "prea" and "bpost", and with no
2773+
// parameters "$@" gives no word at all.
2774+
if let Some(idx) = quoted_parts.iter().position(|p| {
2775+
matches!(p, WordPart::Variable(name, modifier, indirect)
2776+
if is_all_params(name, modifier, *indirect))
2777+
}) {
2778+
let after = quoted_parts.split_off(idx + 1);
2779+
quoted_parts.pop();
2780+
if !quoted_parts.is_empty() {
2781+
let res = evaluate_word_parts_inner(
2782+
quoted_parts,
2783+
IsQuoted::Yes,
2784+
eval_glob,
2785+
state,
2786+
stdin.clone(),
2787+
stderr.clone(),
2788+
)
2789+
.await?;
2790+
result.with_changes(res.changes);
2791+
current_text
2792+
.push(TextPart::Quoted(res.value.join(" ")));
2793+
}
2794+
let params = positional_params(state);
2795+
let count = params.len();
2796+
for (i, param) in params.into_iter().enumerate() {
2797+
current_text.push(TextPart::Quoted(param));
2798+
if i + 1 < count {
2799+
result.extend(evaluate_word_text(
2800+
state,
2801+
std::mem::take(&mut current_text),
2802+
is_quoted,
2803+
eval_glob,
2804+
)?);
2805+
}
2806+
}
2807+
if !after.is_empty() {
2808+
parts.push_front(WordPart::Quoted(after));
2809+
}
2810+
continue;
2811+
}
27382812
let res = evaluate_word_parts_inner(
2739-
parts,
2813+
quoted_parts,
27402814
IsQuoted::Yes,
27412815
eval_glob,
27422816
state,

‎crates/shell/tests/script_args.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,25 @@ echo "all=[$@]"
5252
"#;
5353
assert_eq!(run_script("no_args.sh", script, &[]), "count=0\nall=[]\n");
5454
}
55+
56+
#[test]
57+
fn script_forwards_arguments() {
58+
// "$@" keeps each script argument as a separate word, also after shift
59+
let script = r#"build_dir="${1:-.}"
60+
if [ "$#" -gt 0 ]; then
61+
shift
62+
fi
63+
echo "build_dir=$build_dir"
64+
for a in -S . "$@"; do
65+
echo "[$a]"
66+
done
67+
"#;
68+
assert_eq!(
69+
run_script("forward.sh", script, &["build", "-DA=1", "-DB=x y"]),
70+
"build_dir=build\n[-S]\n[.]\n[-DA=1]\n[-DB=x y]\n"
71+
);
72+
assert_eq!(
73+
run_script("forward_none.sh", script, &[]),
74+
"build_dir=.\n[-S]\n[.]\n"
75+
);
76+
}

‎crates/tests/test-data/functions.sh‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,39 @@ build
7878
> tenth() { echo "${10:-none}"; }
7979
> tenth 1 2 3 4 5 6 7 8 9 ten
8080
ten
81+
82+
# "$@" expands to one word per positional parameter (prefix-dev/shell#291)
83+
> count() { echo $#; }
84+
> fwd() { count "$@"; }
85+
> fwd a 'b c' d
86+
> fwd
87+
> fwd ""
88+
3
89+
0
90+
1
91+
92+
> each() { for a in "$@"; do echo "[$a]"; done }
93+
> each a 'b c'
94+
[a]
95+
[b c]
96+
97+
# Text around "$@" joins the first and last parameters
98+
> words() { for a in "pre$@post"; do echo "[$a]"; done }
99+
> words a b
100+
> words
101+
[prea]
102+
[bpost]
103+
[prepost]
104+
105+
# Unquoted $@ also gives one word per parameter
106+
> count() { echo $#; }
107+
> fwd() { count $@; }
108+
> fwd a b c
109+
3
110+
111+
# shift updates "$@" and "$*"
112+
> rest() { shift; echo "$*"; for a in "$@"; do echo "[$a]"; done }
113+
> rest a b 'c d'
114+
b c d
115+
[b]
116+
[c d]

0 commit comments

Comments
 (0)