Skip to content

Commit 1f2a18f

Browse files
kp992prsabahrami
andauthored
Add support for case (#260)
* Add a basic case test * Update grammar * Add parser and execution logic * Update parser and execution logic * Add a test in test-data * Fix default case and add more tests * update grammar * Use quoted word to avoid pattern execution * Remove star usage --------- Co-authored-by: Parsa Bahraminejad <[email protected]>
1 parent 9514a28 commit 1f2a18f

File tree

6 files changed

+287
-3
lines changed

6 files changed

+287
-3
lines changed

crates/deno_task_shell/src/grammar.pest

+2-2
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ Elif = { "elif" }
164164
Fi = _{ "fi" }
165165
Do = _{ "do" }
166166
Done = _{ "done" }
167-
Case = { "case" }
167+
Case = _{ "case" }
168168
Esac = { "esac" }
169169
While = _{ "while" }
170170
Until = _{ "until" }
@@ -320,7 +320,7 @@ case_item_ns = !{
320320
}
321321

322322
pattern = !{
323-
(Esac | UNQUOTED_PENDING_WORD) ~ ("|" ~ UNQUOTED_PENDING_WORD)*
323+
(UNQUOTED_PENDING_WORD) ~ ("|" ~ UNQUOTED_PENDING_WORD)*
324324
}
325325

326326
if_clause = !{

crates/deno_task_shell/src/parser.rs

+60-1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ pub enum CommandInner {
166166
For(ForLoop),
167167
#[error("Invalid while loop")]
168168
While(WhileLoop),
169+
#[error("Invalid case clause command")]
170+
Case(CaseClause),
169171
#[error("Invalid arithmetic expression")]
170172
ArithmeticExpression(Arithmetic),
171173
}
@@ -227,6 +229,15 @@ pub struct IfClause {
227229
pub else_part: Option<ElsePart>,
228230
}
229231

232+
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
233+
#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
234+
#[derive(Debug, PartialEq, Eq, Clone, Error)]
235+
#[error("Invalid case clause")]
236+
pub struct CaseClause {
237+
pub word: Word,
238+
pub cases: Vec<(Vec<Word>, SequentialList)>,
239+
}
240+
230241
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
231242
#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))]
232243
#[derive(Debug, PartialEq, Eq, Clone, Error)]
@@ -1063,7 +1074,11 @@ fn parse_compound_command(pair: Pair<Rule>) -> Result<Command> {
10631074
})
10641075
}
10651076
Rule::case_clause => {
1066-
Err(miette!("Unsupported compound command case_clause"))
1077+
let case_clause = parse_case_clause(inner)?;
1078+
Ok(Command {
1079+
inner: CommandInner::Case(case_clause),
1080+
redirect: None,
1081+
})
10671082
}
10681083
Rule::if_clause => {
10691084
let if_clause = parse_if_clause(inner)?;
@@ -1125,6 +1140,50 @@ fn parse_if_clause(pair: Pair<Rule>) -> Result<IfClause> {
11251140
})
11261141
}
11271142

1143+
fn wrap_in_quoted(word: Word) -> Word {
1144+
Word::new(vec![WordPart::Quoted(word.into_parts())])
1145+
}
1146+
1147+
fn parse_case_clause(pair: Pair<Rule>) -> Result<CaseClause> {
1148+
let mut inner = pair.into_inner();
1149+
1150+
let word_pair =
1151+
inner.next().ok_or_else(|| miette!("Expected case word"))?;
1152+
let word = parse_word(word_pair)?;
1153+
1154+
let mut cases = Vec::new();
1155+
let case_list =
1156+
inner.next().ok_or_else(|| miette!("Expected case list"))?;
1157+
1158+
for case_item_pair in case_list.into_inner() {
1159+
if case_item_pair.as_rule() == Rule::Esac {
1160+
break;
1161+
}
1162+
1163+
let mut case_inner = case_item_pair.into_inner();
1164+
1165+
// Extract pattern(s) - multiple patterns can exist, separated by `|`
1166+
let mut patterns = Vec::new();
1167+
for pattern_pair in case_inner.by_ref() {
1168+
if pattern_pair.as_rule() == Rule::pattern {
1169+
for p in pattern_pair.into_inner() {
1170+
let parsed_word = parse_word(p)?;
1171+
patterns.push(wrap_in_quoted(parsed_word));
1172+
}
1173+
break;
1174+
}
1175+
}
1176+
1177+
// Extract compound list (command block)
1178+
let mut result = Vec::new();
1179+
if let Some(compound_list_pair) = case_inner.next() {
1180+
parse_compound_list(compound_list_pair, &mut result)?;
1181+
}
1182+
cases.push((patterns, SequentialList { items: result }));
1183+
}
1184+
Ok(CaseClause { word, cases })
1185+
}
1186+
11281187
fn parse_else_part(pair: Pair<Rule>) -> Result<ElsePart> {
11291188
let mut inner = pair.into_inner();
11301189

crates/deno_task_shell/src/shell/command.rs

+1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ async fn parse_shebang_args(
204204
CommandInner::For(_) => return err_unsupported(text),
205205
CommandInner::While(_) => return err_unsupported(text),
206206
CommandInner::ArithmeticExpression(_) => return err_unsupported(text),
207+
CommandInner::Case(_) => return err_unsupported(text),
207208
};
208209
if !cmd.env_vars.is_empty() {
209210
return err_unsupported(text);

crates/deno_task_shell/src/shell/execute.rs

+103
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use tokio_util::sync::CancellationToken;
1717

1818
use crate::parser::AssignmentOp;
1919
use crate::parser::BinaryOp;
20+
use crate::parser::CaseClause;
2021
use crate::parser::Condition;
2122
use crate::parser::ConditionInner;
2223
use crate::parser::ElsePart;
@@ -649,6 +650,10 @@ async fn execute_command(
649650
)
650651
.await
651652
}
653+
CommandInner::Case(case_clause) => {
654+
execute_case_clause(case_clause, &mut state, stdin, stdout, stderr)
655+
.await
656+
}
652657
CommandInner::ArithmeticExpression(arithmetic) => {
653658
// The state can be changed
654659
match execute_arithmetic_expression(arithmetic, &mut state).await {
@@ -742,6 +747,104 @@ async fn execute_while_clause(
742747
}
743748
}
744749

750+
fn pattern_matches(pattern: &str, word_value: &str) -> bool {
751+
let patterns: Vec<&str> = pattern.split('|').collect();
752+
753+
for pat in patterns {
754+
match glob::Pattern::new(pat) {
755+
Ok(glob) if glob.matches(word_value) => return true,
756+
_ => continue,
757+
}
758+
}
759+
760+
false
761+
}
762+
763+
async fn execute_case_clause(
764+
case_clause: CaseClause,
765+
state: &mut ShellState,
766+
stdin: ShellPipeReader,
767+
stdout: ShellPipeWriter,
768+
mut stderr: ShellPipeWriter,
769+
) -> ExecuteResult {
770+
let mut changes = Vec::new();
771+
let mut last_exit_code = 0;
772+
let mut async_handles = Vec::new();
773+
774+
// Evaluate the word to match against
775+
let word_value = match evaluate_word(
776+
case_clause.word.clone(),
777+
state,
778+
stdin.clone(),
779+
stderr.clone(),
780+
)
781+
.await
782+
{
783+
Ok(word_value) => word_value,
784+
Err(err) => return err.into_exit_code(&mut stderr),
785+
};
786+
787+
// Find the first matching case pattern
788+
for (all_pattern, body) in case_clause.cases {
789+
let mut result_matched = false;
790+
for pattern in all_pattern {
791+
let pattern_value = match evaluate_word(
792+
pattern.clone(),
793+
state,
794+
stdin.clone(),
795+
stderr.clone(),
796+
)
797+
.await
798+
{
799+
Ok(pattern_value) => pattern_value,
800+
Err(err) => return err.into_exit_code(&mut stderr),
801+
};
802+
803+
// Check if pattern matches word_value
804+
if pattern_matches(&pattern_value.value, &word_value.value) {
805+
result_matched = true;
806+
let exec_result = execute_sequential_list(
807+
body,
808+
state.clone(),
809+
stdin.clone(),
810+
stdout.clone(),
811+
stderr.clone(),
812+
AsyncCommandBehavior::Yield,
813+
)
814+
.await;
815+
816+
match exec_result {
817+
ExecuteResult::Exit(code, env_changes, handles) => {
818+
state.apply_changes(&env_changes);
819+
changes.extend(env_changes);
820+
async_handles.extend(handles);
821+
last_exit_code = code;
822+
break;
823+
}
824+
ExecuteResult::Continue(code, env_changes, handles) => {
825+
state.apply_changes(&env_changes);
826+
changes.extend(env_changes);
827+
async_handles.extend(handles);
828+
last_exit_code = code;
829+
break;
830+
}
831+
}
832+
}
833+
}
834+
if result_matched {
835+
break;
836+
}
837+
}
838+
839+
state.apply_changes(&changes);
840+
841+
if state.exit_on_error() && last_exit_code != 0 {
842+
ExecuteResult::Exit(last_exit_code, changes, async_handles)
843+
} else {
844+
ExecuteResult::Continue(last_exit_code, changes, async_handles)
845+
}
846+
}
847+
745848
async fn execute_for_clause(
746849
for_clause: ForLoop,
747850
state: &mut ShellState,

crates/tests/test-data/case.sh

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
> fruit="apple"
2+
> case "$fruit" in
3+
> "apple")
4+
> echo "You chose Apple!"
5+
> ;;
6+
> "banana")
7+
> echo "You chose Banana!"
8+
> ;;
9+
> "orange")
10+
> echo "You chose Orange!"
11+
> ;;
12+
> *)
13+
> echo "Unknown fruit!"
14+
> ;;
15+
> esac
16+
You chose Apple!
17+
18+
19+
> number=3
20+
> case "$number" in
21+
> 1)
22+
> echo "Number is one."
23+
> ;;
24+
> 2|3|4)
25+
> echo "Number is between two and four."
26+
> ;;
27+
> *)
28+
> echo "Number is something else."
29+
> ;;
30+
> esac
31+
Number is between two and four.
32+
33+
34+
> number=5
35+
> case "$number" in
36+
> 1)
37+
> echo "Number is one."
38+
> ;;
39+
> 2|3|4)
40+
> echo "Number is between two and four."
41+
> ;;
42+
> *)
43+
> echo "Number is something else."
44+
> ;;
45+
> esac
46+
Number is something else.
47+
48+
49+
> shape="circle"
50+
> case "$shape" in
51+
> (circle)
52+
> echo "It's a circle!"
53+
> ;;
54+
> (square)
55+
> echo "It's a square!"
56+
> ;;
57+
> *)
58+
> echo "Unknown shape!"
59+
> ;;
60+
> esac
61+
It's a circle!
62+
63+
> filename="document.png"
64+
> case "$filename" in
65+
> (*.txt)
66+
> echo "This is a text file."
67+
> ;;
68+
> (*.jpg|*.png)
69+
> echo "This is an image file."
70+
> ;;
71+
> (*)
72+
> echo "Unknown file type."
73+
> ;;
74+
> esac
75+
This is an image file.
76+
77+
78+
> tempname="document.txt"
79+
> filename="tempname"
80+
> case "$filename" in
81+
> (tempname)
82+
> echo "This is a tempname."
83+
> ;;
84+
> (*.jpg|*.png)
85+
> echo "This is an image file."
86+
> ;;
87+
> (*)
88+
> echo "Unknown file type."
89+
> ;;
90+
> esac
91+
This is a tempname.
92+
93+
> letter="c"
94+
> case "$letter" in
95+
> ([a-c])
96+
> echo "Letter is between A and C."
97+
> ;;
98+
> ([d-f])
99+
> echo "Letter is between D and F."
100+
> ;;
101+
> (*)
102+
> echo "Unknown letter."
103+
> ;;
104+
> esac
105+
Letter is between A and C.

scripts/case_1.sh

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
fruit="apple"
2+
3+
case "$fruit" in
4+
"apple")
5+
echo "You chose Apple!"
6+
;;
7+
"banana")
8+
echo "You chose Banana!"
9+
;;
10+
"orange")
11+
echo "You chose Orange!"
12+
;;
13+
*)
14+
echo "Unknown fruit!"
15+
;;
16+
esac

0 commit comments

Comments
 (0)