Skip to content

Commit

Permalink
feat: implement support for ;;& and ;& in case items
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno committed Oct 24, 2024
1 parent eb4fd32 commit 5d794f4
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 27 deletions.
2 changes: 0 additions & 2 deletions brush-core/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ pub type CommandExecuteFunc = fn(
pub type CommandContentFunc = fn(&str, ContentType) -> Result<String, error::Error>;

/// Trait implemented by built-in shell commands.
pub trait Command: Parser {
/// Instantiates the built-in command with the given arguments.
///
Expand Down Expand Up @@ -238,7 +237,6 @@ pub trait Command: Parser {

/// Trait implemented by built-in shell commands that take specially handled declarations
/// as arguments.
pub trait DeclarationCommand: Command {
/// Stores the declarations within the command instance.
///
Expand Down
38 changes: 26 additions & 12 deletions brush-core/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,28 +574,42 @@ impl Execute for ast::CaseClauseCommand {
}

let expanded_value = expansion::basic_expand_word(shell, &self.value).await?;
let mut result: ExecutionResult = ExecutionResult::success();
let mut force_execute_next_case = false;

for case in &self.cases {
let mut matches = false;
if force_execute_next_case {
force_execute_next_case = false;
} else {
let mut matches = false;
for pattern in &case.patterns {
let expanded_pattern = expansion::basic_expand_pattern(shell, pattern).await?;
if expanded_pattern.exactly_matches(expanded_value.as_str())? {
matches = true;
break;
}
}

for pattern in &case.patterns {
let expanded_pattern = expansion::basic_expand_pattern(shell, pattern).await?;
if expanded_pattern.exactly_matches(expanded_value.as_str())? {
matches = true;
break;
if !matches {
continue;
}
}

if matches {
if let Some(case_cmd) = &case.cmd {
return case_cmd.execute(shell, params).await;
} else {
break;
result = if let Some(case_cmd) = &case.cmd {
case_cmd.execute(shell, params).await?
} else {
ExecutionResult::success()
};

match case.post_action {
ast::CaseItemPostAction::ExitCase => break,
ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
force_execute_next_case = true;
}
ast::CaseItemPostAction::ContinueEvaluatingCases => (),
}
}

let result = ExecutionResult::success();
shell.last_exit_status = result.exit_code;

Ok(result)
Expand Down
9 changes: 5 additions & 4 deletions brush-core/src/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ impl Pattern {
}

if self.multiline {
// Set option for multiline matching + set option for allowing '.' pattern to match newline.
// Set option for multiline matching + set option for allowing '.' pattern to match
// newline.
regex_str.push_str("(?ms)");
}

Expand Down Expand Up @@ -522,19 +523,19 @@ mod tests {
#[test]
fn test_pattern_word_translation() -> Result<()> {
assert_eq!(
pattern_to_exact_regex_str(&vec![PatternPiece::Pattern("a*".to_owned())])?.as_str(),
pattern_to_exact_regex_str(vec![PatternPiece::Pattern("a*".to_owned())])?.as_str(),
"^a.*$"
);
assert_eq!(
pattern_to_exact_regex_str(&vec![
pattern_to_exact_regex_str(vec![
PatternPiece::Pattern("a*".to_owned()),
PatternPiece::Literal("b".to_owned()),
])?
.as_str(),
"^a.*b$"
);
assert_eq!(
pattern_to_exact_regex_str(&vec![
pattern_to_exact_regex_str(vec![
PatternPiece::Literal("a*".to_owned()),
PatternPiece::Pattern("b".to_owned()),
])?
Expand Down
1 change: 0 additions & 1 deletion brush-interactive/src/interactive_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ pub struct InteractivePrompt {
}

/// Represents a shell capable of taking commands from standard input.
pub trait InteractiveShell {
/// Returns an immutable reference to the inner shell object.
fn shell(&self) -> impl AsRef<brush_core::Shell> + Send;
Expand Down
28 changes: 27 additions & 1 deletion brush-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ pub struct CaseItem {
pub patterns: Vec<Word>,
/// The commands to execute if this case branch is selected.
pub cmd: Option<CompoundList>,
/// When the case branch is selected, the action to take after the command is executed.
pub post_action: CaseItemPostAction,
}

impl Display for CaseItem {
Expand All @@ -451,7 +453,31 @@ impl Display for CaseItem {
write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{}", cmd)?;
}
writeln!(f)?;
write!(f, ";;")
write!(f, "{}", self.post_action)
}
}

/// Describes the action to take after executing the body command of a case clause.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
pub enum CaseItemPostAction {
/// The containing case should be exited.
ExitCase,
/// If one is present, the command body of the succeeding case item should be
/// executed (without evaluating its pattern).
UnconditionallyExecuteNextCaseItem,
/// The case should continue evaluating the remaining case items, as if this
/// item had not been executed.
ContinueEvaluatingCases,
}

impl Display for CaseItemPostAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CaseItemPostAction::ExitCase => write!(f, ";;"),
CaseItemPostAction::UnconditionallyExecuteNextCaseItem => write!(f, ";&"),
CaseItemPostAction::ContinueEvaluatingCases => write!(f, ";;&"),
}
}
}

Expand Down
23 changes: 17 additions & 6 deletions brush-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,18 +446,29 @@ peg::parser! {

pub(crate) rule case_item_ns() -> ast::CaseItem =
specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() {
ast::CaseItem { patterns: p, cmd: Some(c) }
ast::CaseItem { patterns: p, cmd: Some(c), post_action: ast::CaseItemPostAction::ExitCase }
} /
specific_operator("(")? p:pattern() specific_operator(")") linebreak() {
ast::CaseItem { patterns: p, cmd: None }
ast::CaseItem { patterns: p, cmd: None, post_action: ast::CaseItemPostAction::ExitCase }
}

pub(crate) rule case_item() -> ast::CaseItem =
specific_operator("(")? p:pattern() specific_operator(")") linebreak() specific_operator(";;") linebreak() {
ast::CaseItem { patterns: p, cmd: None }
specific_operator("(")? p:pattern() specific_operator(")") linebreak() post_action:case_item_post_action() linebreak() {
ast::CaseItem { patterns: p, cmd: None, post_action }
} /
specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() specific_operator(";;") linebreak() {
ast::CaseItem { patterns: p, cmd: Some(c) }
specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() post_action:case_item_post_action() linebreak() {
ast::CaseItem { patterns: p, cmd: Some(c), post_action }
}

rule case_item_post_action() -> ast::CaseItemPostAction =
specific_operator(";;") {
ast::CaseItemPostAction::ExitCase
} /
non_posix_extensions_enabled() specific_operator(";;&") {
ast::CaseItemPostAction::ContinueEvaluatingCases
} /
non_posix_extensions_enabled() specific_operator(";&") {
ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem
}

// TODO: validate if this should call non_reserved_word() or word()
Expand Down
2 changes: 1 addition & 1 deletion brush-parser/src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ impl<'a, R: ?Sized + std::io::BufRead> Tokenizer<'a, R> {

fn is_operator(&self, s: &str) -> bool {
// Handle non-POSIX operators.
if !self.options.posix_mode && matches!(s, "<<<" | "&>" | "&>>") {
if !self.options.posix_mode && matches!(s, "<<<" | "&>" | "&>>" | ";;&" | ";&") {
return true;
}

Expand Down
52 changes: 52 additions & 0 deletions brush-shell/tests/cases/compound_cmds/case.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,55 @@ cases:
a) echo "a";;
b) echo "b"
esac
- name: "Case with fall-through"
stdin: |
case "b" in
a) echo "a";;
b) echo "b";&
c) echo "c";;
d) echo "d";;
esac
- name: "Case with resuming switch"
stdin: |
case "b" in
a) echo "a";;
b) echo "b";;&
c) echo "c";;
*) echo "*";;
d) echo "d";;
esac
- name: "Case status values"
stdin: |
function yield() {
return $1
}
case "a" in
x) yield 10;;
a) yield 11;;
b) yield 12;;
esac
echo "1: $?"
case "a" in
b) yield 10;;
c) yield 11;;
esac
echo "2: $?"
case "a" in
a) yield 10;&
b) yield 11;;
c) yield 12;;
esac
echo "3: $?"
case "a" in
a) yield 10;;&
*) yield 11;;
x) yield 12;;
esac
echo "4: $?"

0 comments on commit 5d794f4

Please sign in to comment.