From a213abac10f832a61d133d7436d30b2b10416e22 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sat, 9 Nov 2024 22:08:35 +0900 Subject: [PATCH] Allow `esac` as the first pattern of a case branch POSIX has been changed to allow `esac` as the first pattern of a case branch. This commit implements this change in the parser. --- yash-cli/CHANGELOG-bin.md | 3 +++ yash-cli/tests/scripted_test/case-p.sh | 8 +++++++- yash-syntax/CHANGELOG.md | 10 ++++++++++ yash-syntax/src/parser/case.rs | 14 +++++--------- yash-syntax/src/parser/error.rs | 3 +++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/yash-cli/CHANGELOG-bin.md b/yash-cli/CHANGELOG-bin.md index 2cf49d15..a7cdf0eb 100644 --- a/yash-cli/CHANGELOG-bin.md +++ b/yash-cli/CHANGELOG-bin.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The shell's syntax now allows `esac` as the first pattern of a case branch + as in `case esac in (esac|case) echo ok; esac`. Previously, it was a syntax + error, but POSIX.1-2024 allows it. - The `bg` built-in now updates the `!` special parameter to the process ID of the background job, as required by POSIX.1-2024. - The `exec` built-in no longer exits the shell when the specified command is diff --git a/yash-cli/tests/scripted_test/case-p.sh b/yash-cli/tests/scripted_test/case-p.sh index 1aaca59b..2c5100c4 100644 --- a/yash-cli/tests/scripted_test/case-p.sh +++ b/yash-cli/tests/scripted_test/case-p.sh @@ -333,7 +333,13 @@ test_reserved_word_as_pattern "$LINENO" then test_reserved_word_as_pattern "$LINENO" until test_reserved_word_as_pattern "$LINENO" while -test_oE 'reserved word esac as (non-first) pattern' +test_oE 'esac as first pattern' +case esac in (esac) echo matched;; esac +__IN__ +matched +__OUT__ + +test_oE 'esac as non-first pattern' case esac in -|esac) echo matched;; esac __IN__ matched diff --git a/yash-syntax/CHANGELOG.md b/yash-syntax/CHANGELOG.md index cb776366..302b5b0b 100644 --- a/yash-syntax/CHANGELOG.md +++ b/yash-syntax/CHANGELOG.md @@ -9,9 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- In the `parser::Parser::case_item` method, the parser now accepts an `esac` + token as a pattern after an opening parenthesis, as required by POSIX.1-2024. + The previous version of POSIX did not allow `esac` as the first pattern, so + the method was returning `SyntaxError::EsacAsPattern` in that case. - Internal dependency versions: - futures-util 0.3.28 → 0.3.31 +### Deprecated + +- `parser::SyntaxError::EsacAsPattern` + - This variant is deprecated because this error condition is no longer + possible as described above. + ## [0.12.0] - 2024-09-29 ### Added diff --git a/yash-syntax/src/parser/case.rs b/yash-syntax/src/parser/case.rs index 385de5d6..855a7b72 100644 --- a/yash-syntax/src/parser/case.rs +++ b/yash-syntax/src/parser/case.rs @@ -36,7 +36,6 @@ impl Parser<'_, '_> { pub async fn case_item(&mut self) -> Result> { fn pattern_error_cause(token_id: TokenId) -> SyntaxError { match token_id { - Token(Some(Esac)) => SyntaxError::EsacAsPattern, Token(_) => unreachable!(), Operator(CloseParen) | Operator(Bar) | Operator(Newline) | EndOfInput => { SyntaxError::MissingPattern @@ -63,8 +62,7 @@ impl Parser<'_, '_> { Operator(OpenParen) => { let next_token = self.take_token_auto(&[Esac]).await?; match next_token.id { - // TODO Allow `esac` if not in POSIXly-correct mode - Token(keyword) if keyword != Some(Esac) => next_token.word, + Token(_) => next_token.word, _ => { let cause = pattern_error_cause(next_token.id).into(); let location = next_token.word.location; @@ -318,12 +316,10 @@ mod tests { let mut lexer = Lexer::from_memory("(esac)", Source::Unknown); let mut parser = Parser::new(&mut lexer, &EmptyGlossary); - let e = parser.case_item().now_or_never().unwrap().unwrap_err(); - assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EsacAsPattern)); - assert_eq!(*e.location.code.value.borrow(), "(esac)"); - assert_eq!(e.location.code.start_line_number.get(), 1); - assert_eq!(*e.location.code.source, Source::Unknown); - assert_eq!(e.location.range, 1..5); + let item = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); + assert_eq!(item.patterns.len(), 1); + assert_eq!(item.patterns[0].to_string(), "esac"); + assert_eq!(item.body.0, []); } #[test] diff --git a/yash-syntax/src/parser/error.rs b/yash-syntax/src/parser/error.rs index 945428fe..70115412 100644 --- a/yash-syntax/src/parser/error.rs +++ b/yash-syntax/src/parser/error.rs @@ -135,6 +135,7 @@ pub enum SyntaxError { /// The pattern is not a valid word token. InvalidPattern, /// The first pattern of a case item is `esac`. + #[deprecated = "this error no longer occurs"] EsacAsPattern, /// An `esac` or `;;` appears outside a case command. UnopenedCase, @@ -219,6 +220,7 @@ impl SyntaxError { UnclosedPatternList => "The pattern list is not properly closed by a `)`", MissingPattern => "A pattern is missing in the `case` command", InvalidPattern => "The pattern is not a valid word token", + #[allow(deprecated)] EsacAsPattern => "`esac` cannot be the first of a pattern list", UnclosedCase { .. } => "The `case` command is missing its closing `esac`", UnmatchedParenthesis => "`)` is missing after `(`", @@ -290,6 +292,7 @@ impl SyntaxError { UnopenedIf => "not in an `if` command", UnclosedIf { .. } => "expected `fi`", MissingIn { .. } => "expected `in`", + #[allow(deprecated)] EsacAsPattern => "needs quoting", UnopenedCase => "not in a `case` command", UnclosedCase { .. } => "expected `esac`",