diff --git a/yash-syntax/CHANGELOG.md b/yash-syntax/CHANGELOG.md index a8ab614c..9c9aa7b5 100644 --- a/yash-syntax/CHANGELOG.md +++ b/yash-syntax/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to `yash-syntax` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.13.0] - Unreleased + +### Added + +- Extended the case item syntax to allow `;&`, `;|`, and `;;&` as terminators. + - The `SemicolonAnd`, `SemicolonSemicolonAnd`, and `SemicolonBar` variants + are added to the `parser::lex::Operator` enum. + ## [0.12.1] - 2024-11-10 ### Changed @@ -353,6 +361,7 @@ command. - Functionalities to parse POSIX shell scripts - Alias substitution support +[0.13.0]: https://github.com/magicant/yash-rs/releases/tag/yash-syntax-0.13.0 [0.12.1]: https://github.com/magicant/yash-rs/releases/tag/yash-syntax-0.12.1 [0.12.0]: https://github.com/magicant/yash-rs/releases/tag/yash-syntax-0.12.0 [0.11.0]: https://github.com/magicant/yash-rs/releases/tag/yash-syntax-0.11.0 diff --git a/yash-syntax/src/parser/case.rs b/yash-syntax/src/parser/case.rs index 855a7b72..c45c8347 100644 --- a/yash-syntax/src/parser/case.rs +++ b/yash-syntax/src/parser/case.rs @@ -24,6 +24,7 @@ use super::error::SyntaxError; use super::lex::Keyword::{Case, Esac, In}; use super::lex::Operator::{Bar, CloseParen, Newline, OpenParen, SemicolonSemicolon}; use super::lex::TokenId::{self, EndOfInput, Operator, Token}; +use crate::syntax::CaseContinuation; use crate::syntax::CaseItem; use crate::syntax::CompoundCommand; @@ -102,8 +103,13 @@ impl Parser<'_, '_> { } let body = self.maybe_compound_list_boxed().await?; + let continuation = CaseContinuation::default(); - Ok(Some(CaseItem { patterns, body })) + Ok(Some(CaseItem { + patterns, + body, + continuation, + })) } /// Parses a case conditional construct. diff --git a/yash-syntax/src/parser/lex/op.rs b/yash-syntax/src/parser/lex/op.rs index d10717d2..39d89fee 100644 --- a/yash-syntax/src/parser/lex/op.rs +++ b/yash-syntax/src/parser/lex/op.rs @@ -43,8 +43,14 @@ pub enum Operator { CloseParen, /// `;` Semicolon, + /// `;&` + SemicolonAnd, /// `;;` SemicolonSemicolon, + /// `;;&` + SemicolonSemicolonAnd, + /// `;|` + SemicolonBar, /// `<` Less, /// `<&` @@ -89,7 +95,10 @@ impl Operator { OpenParen => "(", CloseParen => ")", Semicolon => ";", + SemicolonAnd => ";&", SemicolonSemicolon => ";;", + SemicolonSemicolonAnd => ";;&", + SemicolonBar => ";|", Less => "<", LessAnd => "<&", LessOpenParen => "<(", @@ -110,13 +119,23 @@ impl Operator { /// Determines if this token can be a delimiter of a clause. /// - /// This function returns `true` for `CloseParen` and `SemicolonSemicolon`, - /// and `false` for others. + /// This function returns `true` for the following operators: + /// + /// - `CloseParen` (`)`) + /// - `SemicolonAnd` (`;&`) + /// - `SemicolonSemicolon` (`;;`) + /// - `SemicolonSemicolonAnd` (`;;&`) + /// - `SemicolonBar` (`;|`) #[must_use] pub const fn is_clause_delimiter(self) -> bool { use Operator::*; match self { - CloseParen | SemicolonSemicolon => true, + CloseParen + | SemicolonAnd + | SemicolonSemicolon + | SemicolonSemicolonAnd + | SemicolonBar => true, + Newline | And | AndAnd | OpenParen | Semicolon | Less | LessAnd | LessOpenParen | LessLess | LessLessDash | LessLessLess | LessGreater | Greater | GreaterAnd | GreaterOpenParen | GreaterGreater | GreaterGreaterBar | GreaterBar | Bar | BarBar => { diff --git a/yash-syntax/src/parser/list.rs b/yash-syntax/src/parser/list.rs index c5246b79..035e7642 100644 --- a/yash-syntax/src/parser/list.rs +++ b/yash-syntax/src/parser/list.rs @@ -51,7 +51,9 @@ fn error_type_for_trailing_token_in_command_line(token_id: TokenId) -> Option Some(InvalidCommandToken), OpenParen => Some(MissingSeparator), CloseParen => Some(UnopenedSubshell), - SemicolonSemicolon => Some(UnopenedCase), + SemicolonAnd | SemicolonSemicolon | SemicolonSemicolonAnd | SemicolonBar => { + Some(UnopenedCase) + } Newline | Less | LessAnd | LessOpenParen | LessLess | LessLessDash | LessLessLess | LessGreater | Greater | GreaterAnd | GreaterOpenParen | GreaterGreater | GreaterGreaterBar | GreaterBar => unreachable!(), diff --git a/yash-syntax/src/syntax.rs b/yash-syntax/src/syntax.rs index 0268b141..25206122 100644 --- a/yash-syntax/src/syntax.rs +++ b/yash-syntax/src/syntax.rs @@ -1220,6 +1220,60 @@ impl fmt::Display for ElifThen { } } +/// Symbol that terminates the body of a case branch and determines what to do +/// after executing it +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum CaseContinuation { + /// `;;` (terminate the case construct) + #[default] + Break, + /// `;&` (unconditionally execute the body of the next case branch) + FallThrough, + /// `;|` or `;;&` (resume with the next case branch, performing pattern matching again) + Continue, +} + +impl TryFrom for CaseContinuation { + type Error = TryFromOperatorError; + + /// Converts an operator into a case continuation. + /// + /// The `SemicolonBar` and `SemicolonSemicolonAnd` operators are converted + /// into `Continue`; you cannot distinguish between the two from the return + /// value. + fn try_from(op: Operator) -> Result { + use CaseContinuation::*; + use Operator::*; + match op { + SemicolonSemicolon => Ok(Break), + SemicolonAnd => Ok(FallThrough), + SemicolonBar | SemicolonSemicolonAnd => Ok(Continue), + _ => Err(TryFromOperatorError {}), + } + } +} + +impl From for Operator { + /// Converts a case continuation into an operator. + /// + /// The `Continue` variant is converted into `SemicolonBar`. + fn from(cc: CaseContinuation) -> Operator { + use CaseContinuation::*; + use Operator::*; + match cc { + Break => SemicolonSemicolon, + FallThrough => SemicolonAnd, + Continue => SemicolonBar, + } + } +} + +impl fmt::Display for CaseContinuation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Operator::from(*self).fmt(f) + } +} + /// Branch item of a `case` compound command #[derive(Clone, Debug, Eq, PartialEq)] pub struct CaseItem { @@ -1230,15 +1284,18 @@ pub struct CaseItem { pub patterns: Vec, /// Commands that are executed if any of the patterns matched pub body: List, + /// What to do after executing the body of this item + pub continuation: CaseContinuation, } impl fmt::Display for CaseItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "({}) {};;", + "({}) {}{}", self.patterns.iter().format(" | "), - self.body + self.body, + self.continuation, ) } } @@ -2192,22 +2249,48 @@ mod tests { assert_eq!(format!("{elif:#}"), "elif c& then b&"); } + #[test] + fn case_continuation_conversions() { + use CaseContinuation::*; + for cc in &[Break, FallThrough, Continue] { + let cc2 = CaseContinuation::try_from(Operator::from(*cc)); + assert_eq!(cc2, Ok(*cc)); + } + assert_eq!( + CaseContinuation::try_from(Operator::SemicolonSemicolonAnd), + Ok(Continue) + ); + } + #[test] fn case_item_display() { - let patterns = vec!["foo".parse().unwrap()]; - let body = "".parse::().unwrap(); - let item = CaseItem { patterns, body }; + let item = CaseItem { + patterns: vec!["foo".parse().unwrap()], + body: "".parse::().unwrap(), + continuation: CaseContinuation::Break, + }; assert_eq!(item.to_string(), "(foo) ;;"); - let patterns = vec!["bar".parse().unwrap()]; - let body = "echo ok".parse::().unwrap(); - let item = CaseItem { patterns, body }; + let item = CaseItem { + patterns: vec!["bar".parse().unwrap()], + body: "echo ok".parse::().unwrap(), + continuation: CaseContinuation::Break, + }; assert_eq!(item.to_string(), "(bar) echo ok;;"); - let patterns = ["a", "b", "c"].iter().map(|s| s.parse().unwrap()).collect(); - let body = "foo; bar&".parse::().unwrap(); - let item = CaseItem { patterns, body }; + let item = CaseItem { + patterns: ["a", "b", "c"].iter().map(|s| s.parse().unwrap()).collect(), + body: "foo; bar&".parse::().unwrap(), + continuation: CaseContinuation::Break, + }; assert_eq!(item.to_string(), "(a | b | c) foo; bar&;;"); + + let item = CaseItem { + patterns: vec!["foo".parse().unwrap()], + body: "bar".parse::().unwrap(), + continuation: CaseContinuation::FallThrough, + }; + assert_eq!(item.to_string(), "(foo) bar;&"); } #[test]