diff --git a/Cargo.lock b/Cargo.lock index 47172675..41751df8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,7 +532,7 @@ dependencies = [ [[package]] name = "yash-builtin" -version = "0.5.0" +version = "0.6.0" dependencies = [ "assert_matches", "either", @@ -551,7 +551,7 @@ dependencies = [ [[package]] name = "yash-cli" -version = "0.2.0" +version = "0.3.0" dependencies = [ "assert_matches", "futures-util", @@ -569,7 +569,7 @@ dependencies = [ [[package]] name = "yash-env" -version = "0.5.0" +version = "0.6.0" dependencies = [ "annotate-snippets", "assert_matches", @@ -593,7 +593,7 @@ dependencies = [ [[package]] name = "yash-env-test-helper" -version = "0.3.0" +version = "0.4.0" dependencies = [ "assert_matches", "futures-executor", @@ -621,7 +621,7 @@ dependencies = [ [[package]] name = "yash-prompt" -version = "0.3.0" +version = "0.4.0" dependencies = [ "futures-util", "yash-env", @@ -636,7 +636,7 @@ version = "1.1.1" [[package]] name = "yash-semantics" -version = "0.5.0" +version = "0.6.0" dependencies = [ "assert_matches", "enumset", @@ -654,7 +654,7 @@ dependencies = [ [[package]] name = "yash-syntax" -version = "0.13.0" +version = "0.14.0" dependencies = [ "annotate-snippets", "assert_matches", diff --git a/yash-builtin/CHANGELOG.md b/yash-builtin/CHANGELOG.md index 86325c7f..5970f626 100644 --- a/yash-builtin/CHANGELOG.md +++ b/yash-builtin/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to `yash-builtin` 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.6.0] - Unreleased + +### Changed + +- External dependency versions: + - yash-env 0.5.0 → 0.6.0 + - yash-semantics 0.5.0 → 0.6.0 (optional) + - yash-syntax 0.13.0 → 0.14.0 +- Internal dependency versions: + - yash-prompt 0.3.0 → 0.4.0 (optional) + ## [0.5.0] - 2024-12-14 ### Changed @@ -148,6 +159,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial implementation of the `yash-builtin` crate +[0.6.0]: https://github.com/magicant/yash-rs/releases/tag/yash-builtin-0.6.0 [0.5.0]: https://github.com/magicant/yash-rs/releases/tag/yash-builtin-0.5.0 [0.4.1]: https://github.com/magicant/yash-rs/releases/tag/yash-builtin-0.4.1 [0.4.0]: https://github.com/magicant/yash-rs/releases/tag/yash-builtin-0.4.0 diff --git a/yash-builtin/Cargo.toml b/yash-builtin/Cargo.toml index df7a4cab..cff2791f 100644 --- a/yash-builtin/Cargo.toml +++ b/yash-builtin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-builtin" -version = "0.5.0" +version = "0.6.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,6 +12,7 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities"] +publish = false [features] default = ["yash-prompt", "yash-semantics"] @@ -24,15 +25,15 @@ either = "1.9.0" enumset = { version = "1.1.2", optional = true } itertools = "0.13.0" thiserror = "2.0.4" -yash-env = { path = "../yash-env", version = "0.5.0" } -yash-prompt = { path = "../yash-prompt", version = "0.3.0", optional = true } +yash-env = { path = "../yash-env", version = "0.6.0" } +yash-prompt = { path = "../yash-prompt", version = "0.4.0", optional = true } yash-quote = { path = "../yash-quote", version = "1.1.1" } -yash-semantics = { path = "../yash-semantics", version = "0.5.0", optional = true } -yash-syntax = { path = "../yash-syntax", version = "0.13.0" } +yash-semantics = { path = "../yash-semantics", version = "0.6.0", optional = true } +yash-syntax = { path = "../yash-syntax", version = "0.14.0" } [dev-dependencies] assert_matches = "1.5.0" futures-executor = "0.3.31" futures-util = { version = "0.3.31", features = ["channel"] } -yash-env-test-helper = { path = "../yash-env-test-helper", version = "0.3.0" } -yash-semantics = { path = "../yash-semantics", version = "0.5.0" } +yash-env-test-helper = { path = "../yash-env-test-helper", version = "0.4.0" } +yash-semantics = { path = "../yash-semantics", version = "0.6.0" } diff --git a/yash-builtin/src/command/identify.rs b/yash-builtin/src/command/identify.rs index a0462074..d6708a98 100644 --- a/yash-builtin/src/command/identify.rs +++ b/yash-builtin/src/command/identify.rs @@ -343,6 +343,7 @@ mod tests { let builtin = Builtin { r#type: Type::Substitutive, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; let mut builtin_target = Target::Builtin { builtin, @@ -492,6 +493,7 @@ mod tests { builtin: Builtin { r#type: Type::Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, path: None, }; @@ -512,6 +514,7 @@ mod tests { builtin: Builtin { r#type: Type::Substitutive, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, path: Some(c"/bin/echo".to_owned()), }; diff --git a/yash-builtin/src/command/invoke.rs b/yash-builtin/src/command/invoke.rs index dd188568..193c8493 100644 --- a/yash-builtin/src/command/invoke.rs +++ b/yash-builtin/src/command/invoke.rs @@ -114,6 +114,7 @@ mod tests { Builtin { r#type: Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, ); let invoke = Invoke { @@ -152,6 +153,7 @@ mod tests { make_result() }) }, + is_declaration_utility: Some(false), }, path: None, }; @@ -181,6 +183,7 @@ mod tests { ) }) }, + is_declaration_utility: Some(false), }, ); let body: FullCompoundCommand = "{ : \"$@\"; }".parse().unwrap(); diff --git a/yash-builtin/src/command/search.rs b/yash-builtin/src/command/search.rs index 13e1c7ea..179df3d9 100644 --- a/yash-builtin/src/command/search.rs +++ b/yash-builtin/src/command/search.rs @@ -194,6 +194,7 @@ mod tests { let builtin = Builtin { r#type: Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert(":", builtin); let params = &Search { @@ -212,6 +213,7 @@ mod tests { let builtin = Builtin { r#type: Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert(":", builtin); let params = &Search { diff --git a/yash-builtin/src/lib.rs b/yash-builtin/src/lib.rs index dc86d454..b945c8cc 100644 --- a/yash-builtin/src/lib.rs +++ b/yash-builtin/src/lib.rs @@ -115,6 +115,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(source::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -122,6 +123,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(ready(colon::main(env, args))), + is_declaration_utility: Some(false), }, ), ( @@ -129,6 +131,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(alias::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -136,6 +139,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(bg::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -143,6 +147,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(r#break::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -150,6 +155,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(cd::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -158,6 +164,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(command::main(env, args)), + is_declaration_utility: None, }, ), ( @@ -165,6 +172,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(r#continue::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -173,6 +181,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(eval::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -181,6 +190,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(exec::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -188,6 +198,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(exit::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -195,6 +206,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(export::main(env, args)), + is_declaration_utility: Some(true), }, ), ( @@ -202,6 +214,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(r#false::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -209,6 +222,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(fg::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -216,6 +230,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(getopts::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -223,6 +238,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(jobs::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -230,6 +246,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(kill::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -237,6 +254,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(pwd::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -245,6 +263,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(read::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -252,6 +271,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(readonly::main(env, args)), + is_declaration_utility: Some(true), }, ), ( @@ -259,6 +279,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(r#return::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -266,6 +287,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(set::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -273,6 +295,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(shift::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -281,6 +304,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(source::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -288,6 +312,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(times::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -295,6 +320,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(trap::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -302,6 +328,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(r#true::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -310,6 +337,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(r#type::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -317,6 +345,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Elective, execute: |env, args| Box::pin(typeset::main(env, args)), + is_declaration_utility: Some(true), }, ), ( @@ -324,6 +353,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(ulimit::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -331,6 +361,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(umask::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -338,6 +369,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(unalias::main(env, args)), + is_declaration_utility: Some(false), }, ), ( @@ -345,6 +377,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Special, execute: |env, args| Box::pin(unset::main(env, args)), + is_declaration_utility: Some(false), }, ), #[cfg(feature = "yash-semantics")] @@ -353,6 +386,7 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ Builtin { r#type: Mandatory, execute: |env, args| Box::pin(wait::main(env, args)), + is_declaration_utility: Some(false), }, ), ]; diff --git a/yash-builtin/src/set.rs b/yash-builtin/src/set.rs index 6afdf2de..96f3a59f 100644 --- a/yash-builtin/src/set.rs +++ b/yash-builtin/src/set.rs @@ -355,6 +355,7 @@ xtrace off Builtin { r#type: Special, execute: |env, args| Box::pin(main(env, args)), + is_declaration_utility: Some(false), }, ); env.options = Default::default(); diff --git a/yash-cli/CHANGELOG-bin.md b/yash-cli/CHANGELOG-bin.md index 3825b287..efcfe893 100644 --- a/yash-cli/CHANGELOG-bin.md +++ b/yash-cli/CHANGELOG-bin.md @@ -8,6 +8,12 @@ implementing library crate are in [CHANGELOG-lib.md](CHANGELOG-lib.md). 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.3.0] - Unreleased + +### Added + +- The shell now supports declaration utilities as defined in POSIX. + ## [0.2.0] - 2024-12-14 ### Added @@ -97,6 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of the shell +[0.3.0]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.3.0 [0.2.0]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.2.0 [0.1.0]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.1.0 [0.1.0-beta.2]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.1.0-beta.2 diff --git a/yash-cli/CHANGELOG-lib.md b/yash-cli/CHANGELOG-lib.md index 0fa26981..8b89c4d0 100644 --- a/yash-cli/CHANGELOG-lib.md +++ b/yash-cli/CHANGELOG-lib.md @@ -9,6 +9,18 @@ For changes to the shell binary as a whole, see [CHANGELOG-bin.md](CHANGELOG-bin 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.3.0] - Unreleased + +### Changed + +- External dependency versions: + - yash-env 0.5.0 → 0.6.0 + - yash-syntax 0.13.0 → 0.14.0 +- Internal dependency versions: + - yash-builtin 0.5.0 → 0.6.0 + - yash-prompt 0.3.0 → 0.4.0 + - yash-semantics 0.5.0 → 0.6.0 + ## [0.2.0] - 2024-12-14 ### Changed @@ -135,6 +147,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial implementation of the `yash-cli` crate +[0.3.0]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.3.0 [0.2.0]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.2.0 [0.1.0]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.1.0 [0.1.0-beta.2]: https://github.com/magicant/yash-rs/releases/tag/yash-cli-0.1.0-beta.2 diff --git a/yash-cli/Cargo.toml b/yash-cli/Cargo.toml index 8a2813ba..79a6b37d 100644 --- a/yash-cli/Cargo.toml +++ b/yash-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-cli" -version = "0.2.0" +version = "0.3.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,6 +12,7 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities"] +publish = false [[bin]] name = "yash3" @@ -19,12 +20,12 @@ path = "src/main.rs" [dependencies] thiserror = "2.0.4" -yash-builtin = { path = "../yash-builtin", version = "0.5.0" } -yash-env = { path = "../yash-env", version = "0.5.0" } +yash-builtin = { path = "../yash-builtin", version = "0.6.0" } +yash-env = { path = "../yash-env", version = "0.6.0" } yash-executor = { path = "../yash-executor", version = "1.0.0" } -yash-prompt = { path = "../yash-prompt", version = "0.3.0" } -yash-semantics = { path = "../yash-semantics", version = "0.5.0" } -yash-syntax = { path = "../yash-syntax", version = "0.13.0" } +yash-prompt = { path = "../yash-prompt", version = "0.4.0" } +yash-semantics = { path = "../yash-semantics", version = "0.6.0" } +yash-syntax = { path = "../yash-syntax", version = "0.14.0" } [dev-dependencies] assert_matches = "1.5.0" diff --git a/yash-cli/tests/scripted_test.rs b/yash-cli/tests/scripted_test.rs index 679fc86e..1188f8de 100644 --- a/yash-cli/tests/scripted_test.rs +++ b/yash-cli/tests/scripted_test.rs @@ -180,6 +180,16 @@ fn continue_builtin() { run("continue-p.sh") } +#[test] +fn declaration_utilities() { + run("declutil-p.sh") +} + +#[test] +fn declaration_utilities_ex() { + run("declutil-y.sh") +} + #[test] fn errexit_option() { run("errexit-p.sh") diff --git a/yash-cli/tests/scripted_test/declutil-p.sh b/yash-cli/tests/scripted_test/declutil-p.sh new file mode 100644 index 00000000..b74fc081 --- /dev/null +++ b/yash-cli/tests/scripted_test/declutil-p.sh @@ -0,0 +1,75 @@ +# declutil-p.sh: test of declaration utilities for any POSIX-compliant shell + +posix="true" + +# Pathname expansion may match this dummy file in incorrect implementations. +>tmpfile + +test_oE 'no pathname expansion or field splitting in export A=$a' +a="1 * 2" +export A=$a +sh -c 'printf "%s\n" "$A"' +__IN__ +1 * 2 +__OUT__ + +test_oE 'tilde expansions in export A=~:~' +HOME=/foo +export A=~:~ +sh -c 'printf "%s\n" "$A"' +__IN__ +/foo:/foo +__OUT__ + +test_oE 'pathname expansion and field splitting in export $a' +A=foo B=bar a='A B' +export $a +sh -c 'printf "%s\n" "$A" "$B"' +__IN__ +foo +bar +__OUT__ + +test_oE 'no pathname expansion or field splitting in readonly A=$a' +a="1 * 2" +readonly A=$a +printf "%s\n" "$A" +__IN__ +1 * 2 +__OUT__ + +test_oE 'tilde expansions in readonly A=~:~' +HOME=/foo +readonly A=~:~ +printf "%s\n" "$A" +__IN__ +/foo:/foo +__OUT__ + +test_oE 'pathname expansion and field splitting in readonly $a' +A=foo B=bar a='A B' +readonly $a +printf "%s\n" "$A" "$B" +__IN__ +foo +bar +__OUT__ + +test_oE 'command command export' +a="1 * 2" +command command export A=$a +sh -c 'printf "%s\n" "$A"' +__IN__ +1 * 2 +__OUT__ + +test_oE 'command command readonly' +a="1 * 2" +command command readonly A=$a +printf "%s\n" "$A" +__IN__ +1 * 2 +__OUT__ + +# POSIX allows any utility to be a declaration utility as an extension, +# so there are no tests to check that a utility is not a declaration utility. diff --git a/yash-cli/tests/scripted_test/declutil-y.sh b/yash-cli/tests/scripted_test/declutil-y.sh new file mode 100644 index 00000000..759b516d --- /dev/null +++ b/yash-cli/tests/scripted_test/declutil-y.sh @@ -0,0 +1,40 @@ +# declutil-y.sh: yash-specific test of declaration utilities + +>tmpfile + +# typeset is a declaration utility in yash +test_oE 'no pathname expansion or field splitting in typeset A=$a' +a="1 * 2" +typeset A=$a +printf "%s\n" "$A" +__IN__ +1 * 2 +__OUT__ + +# printf is not a declaration utility in yash +test_oE 'pathname expansion and field splitting in printf A=$a' +a='1 tmp* 2' +printf "%s\n" A=$a +__IN__ +A=1 +tmpfile +2 +__OUT__ + +# printf is not a declaration utility in yash +test_oE 'tilde expansions in printf A=~:~' +HOME=/foo +printf "%s\n" A=~:~ +__IN__ +A=~:~ +__OUT__ + +# printf is not a declaration utility in yash +test_oE 'command command printf' +a='1 tmp* 2' +command command printf "%s\n" A=$a +__IN__ +A=1 +tmpfile +2 +__OUT__ diff --git a/yash-env-test-helper/CHANGELOG.md b/yash-env-test-helper/CHANGELOG.md index d3e1e52c..dc372557 100644 --- a/yash-env-test-helper/CHANGELOG.md +++ b/yash-env-test-helper/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to `yash-env-test-helper` 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.4.0] - Unreleased + +### Changed + +- External dependency versions: + - yash-env 0.5.0 → 0.6.0 + ## [0.3.0] - 2024-12-14 ### Changed @@ -30,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial implementation of the `yash-env-test-helper` crate +[0.4.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-test-helper-0.4.0 [0.3.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-test-helper-0.3.0 [0.2.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-test-helper-0.2.0 [0.1.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-test-helper-0.1.0 diff --git a/yash-env-test-helper/Cargo.toml b/yash-env-test-helper/Cargo.toml index a40fb4df..61ddf297 100644 --- a/yash-env-test-helper/Cargo.toml +++ b/yash-env-test-helper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-env-test-helper" -version = "0.3.0" +version = "0.4.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,9 +12,10 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities"] +publish = false [dependencies] assert_matches = "1.5.0" futures-executor = "0.3.31" futures-util = { version = "0.3.31", features = ["channel"] } -yash-env = { path = "../yash-env", version = "0.5.0" } +yash-env = { path = "../yash-env", version = "0.6.0" } diff --git a/yash-env/CHANGELOG.md b/yash-env/CHANGELOG.md index 64d3e39a..875b7566 100644 --- a/yash-env/CHANGELOG.md +++ b/yash-env/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to `yash-env` 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.6.0] - Unreleased + +### Added + +- The `Env` struct now implements `yash_syntax::decl_util::Glossary`. +- The `builtin::Builtin` struct now has the `is_declaration_utility` field. + +### Changed + +- External dependency versions: + - yash-syntax 0.13.0 → 0.14.0 + ## [0.5.0] - 2024-12-14 ### Changed @@ -315,6 +327,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial implementation of the `yash-env` crate +[0.6.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-0.6.0 [0.5.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-0.5.0 [0.4.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-0.4.0 [0.3.0]: https://github.com/magicant/yash-rs/releases/tag/yash-env-0.3.0 diff --git a/yash-env/Cargo.toml b/yash-env/Cargo.toml index bd5a572a..768e7ee3 100644 --- a/yash-env/Cargo.toml +++ b/yash-env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-env" -version = "0.5.0" +version = "0.6.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,6 +12,7 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities"] +publish = false [dependencies] annotate-snippets = "0.11.4" @@ -27,7 +28,9 @@ thiserror = "2.0.4" unix_path = "1.0.1" unix_str = "1.0.0" yash-quote = { path = "../yash-quote", version = "1.1.1" } -yash-syntax = { path = "../yash-syntax", version = "0.13.0", features = ["annotate-snippets"] } +yash-syntax = { path = "../yash-syntax", version = "0.14.0", features = [ + "annotate-snippets", +] } [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["fs", "signal", "user"] } diff --git a/yash-env/src/builtin.rs b/yash-env/src/builtin.rs index e698bc61..baeb7ffc 100644 --- a/yash-env/src/builtin.rs +++ b/yash-env/src/builtin.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Type definitions for built-in utilities. +//! Type definitions for built-in utilities //! //! This module provides data types for defining built-in utilities. //! @@ -33,7 +33,7 @@ use std::pin::Pin; pub mod getopts; -/// Types of built-in utilities. +/// Types of built-in utilities #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Type { /// Special built-in @@ -102,7 +102,7 @@ pub enum Type { Substitutive, } -/// Result of built-in utility execution. +/// Result of built-in utility execution /// /// The result type contains an exit status and optional flags that may affect /// the behavior of the shell following the built-in execution. @@ -234,7 +234,7 @@ impl From for Result { } } -/// Type of functions that implement the behavior of a built-in. +/// Type of functions that implement the behavior of a built-in /// /// The function takes two arguments. /// The first is an environment in which the built-in is executed. @@ -242,13 +242,23 @@ impl From for Result { /// (not including the leading command name word). pub type Main = fn(&mut Env, Vec) -> Pin + '_>>; -/// Built-in utility definition. +/// Built-in utility definition #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Builtin { - /// Type of the built-in. + /// Type of the built-in pub r#type: Type, - /// Function that implements the behavior of the built-in. + + /// Function that implements the behavior of the built-in pub execute: Main, + + /// Whether the built-in is a declaration utility + /// + /// The [`yash_syntax::decl_util::Glossary`] implementation for [`Env`] uses + /// this field to determine whether a command name is a declaration utility. + /// See the [method description] for the value this field should have. + /// + /// [method description]: yash_syntax::decl_util::Glossary::is_declaration_utility + pub is_declaration_utility: Option, } impl Debug for Builtin { diff --git a/yash-env/src/decl_util.rs b/yash-env/src/decl_util.rs new file mode 100644 index 00000000..72c63eed --- /dev/null +++ b/yash-env/src/decl_util.rs @@ -0,0 +1,34 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2024 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Implementation of declaration utility glossary for the environment + +use crate::Env; +use yash_syntax::decl_util::Glossary; + +/// Determines whether a command name is a declaration utility. +/// +/// This implementation looks up the command name in `self.builtins` and returns +/// the value of `is_declaration_utility` if the built-in is found. Otherwise, +/// the command is not a declaration utility. +impl Glossary for Env { + fn is_declaration_utility(&self, name: &str) -> Option { + match self.builtins.get(name) { + Some(builtin) => builtin.is_declaration_utility, + None => Some(false), + } + } +} diff --git a/yash-env/src/lib.rs b/yash-env/src/lib.rs index e55fd063..18f58d98 100644 --- a/yash-env/src/lib.rs +++ b/yash-env/src/lib.rs @@ -465,6 +465,7 @@ impl Env { mod alias; pub mod builtin; +mod decl_util; pub mod function; pub mod input; pub mod io; diff --git a/yash-prompt/CHANGELOG.md b/yash-prompt/CHANGELOG.md index e86c60a5..0ce1b78a 100644 --- a/yash-prompt/CHANGELOG.md +++ b/yash-prompt/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to `yash-prompt` 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.4.0] - Unreleased + +### Changed + +- External dependency versions: + - yash-env 0.5.0 → 0.6.0 + - yash-semantics 0.5.0 → 0.6.0 + - yash-syntax 0.13.0 → 0.14.0 + ## [0.3.0] - 2024-12-14 ### Changed @@ -45,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial implementation of the `yash-prompt` crate +[0.4.0]: https://github.com/magicant/yash-rs/releases/tag/yash-prompt-0.4.0 [0.3.0]: https://github.com/magicant/yash-rs/releases/tag/yash-prompt-0.3.0 [0.2.0]: https://github.com/magicant/yash-rs/releases/tag/yash-prompt-0.2.0 [0.1.0]: https://github.com/magicant/yash-rs/releases/tag/yash-prompt-0.1.0 diff --git a/yash-prompt/Cargo.toml b/yash-prompt/Cargo.toml index 7230fa6e..48543211 100644 --- a/yash-prompt/Cargo.toml +++ b/yash-prompt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-prompt" -version = "0.3.0" +version = "0.4.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,12 +12,13 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities"] +publish = false [dependencies] futures-util = "0.3.31" -yash-env = { path = "../yash-env", version = "0.5.0" } -yash-semantics = { path = "../yash-semantics", version = "0.5.0" } -yash-syntax = { path = "../yash-syntax", version = "0.13.0" } +yash-env = { path = "../yash-env", version = "0.6.0" } +yash-semantics = { path = "../yash-semantics", version = "0.6.0" } +yash-syntax = { path = "../yash-syntax", version = "0.14.0" } [dev-dependencies] -yash-env-test-helper = { path = "../yash-env-test-helper", version = "0.3.0" } +yash-env-test-helper = { path = "../yash-env-test-helper", version = "0.4.0" } diff --git a/yash-semantics/CHANGELOG.md b/yash-semantics/CHANGELOG.md index 7023ad79..420d8d1d 100644 --- a/yash-semantics/CHANGELOG.md +++ b/yash-semantics/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to `yash-semantics` 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.6.0] - Unreleased + +### Added + +- Added the `expand_word_multiple` and `expand_word_with_mode` functions to the + `expansion` module. + +### Changed + +- The execution of a simple command + (`impl command::Command for yash_syntax::syntax::SimpleCommand`) + now honors the `ExpansionMode` specified for the words in the command. +- External dependency versions: + - yash-env 0.5.0 → 0.6.0 + - yash-syntax 0.13.0 → 0.14.0 + ## [0.5.0] - 2024-12-14 ### Changed @@ -150,6 +166,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial implementation of the `yash-semantics` crate +[0.6.0]: https://github.com/magicant/yash-rs/releases/tag/yash-semantics-0.6.0 [0.5.0]: https://github.com/magicant/yash-rs/releases/tag/yash-semantics-0.5.0 [0.4.0]: https://github.com/magicant/yash-rs/releases/tag/yash-semantics-0.4.0 [0.3.0]: https://github.com/magicant/yash-rs/releases/tag/yash-semantics-0.3.0 diff --git a/yash-semantics/Cargo.toml b/yash-semantics/Cargo.toml index c6ef2b1a..3f93adb2 100644 --- a/yash-semantics/Cargo.toml +++ b/yash-semantics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-semantics" -version = "0.5.0" +version = "0.6.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,6 +12,7 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities"] +publish = false [dependencies] assert_matches = "1.5.0" @@ -19,12 +20,12 @@ enumset = "1.1.2" itertools = "0.13.0" thiserror = "2.0.4" yash-arith = { path = "../yash-arith", version = "0.2.1" } -yash-env = { path = "../yash-env", version = "0.5.0" } +yash-env = { path = "../yash-env", version = "0.6.0" } yash-fnmatch = { path = "../yash-fnmatch", version = "1.1.1" } yash-quote = { path = "../yash-quote", version = "1.1.1" } -yash-syntax = { path = "../yash-syntax", version = "0.13.0" } +yash-syntax = { path = "../yash-syntax", version = "0.14.0" } [dev-dependencies] futures-executor = "0.3.31" futures-util = { version = "0.3.31", features = ["channel"] } -yash-env-test-helper = { path = "../yash-env-test-helper", version = "0.3.0" } +yash-env-test-helper = { path = "../yash-env-test-helper", version = "0.4.0" } diff --git a/yash-semantics/src/command/and_or.rs b/yash-semantics/src/command/and_or.rs index 5fe5a87b..acae60d9 100644 --- a/yash-semantics/src/command/and_or.rs +++ b/yash-semantics/src/command/and_or.rs @@ -324,6 +324,7 @@ mod tests { Builtin { r#type: Special, execute: stub_builtin_condition, + is_declaration_utility: Some(false), }, ); env.builtins.insert( @@ -331,6 +332,7 @@ mod tests { Builtin { r#type: Special, execute: stub_builtin_no_condition, + is_declaration_utility: Some(false), }, ); diff --git a/yash-semantics/src/command/compound_command.rs b/yash-semantics/src/command/compound_command.rs index 0038b00c..8bab5c40 100644 --- a/yash-semantics/src/command/compound_command.rs +++ b/yash-semantics/src/command/compound_command.rs @@ -192,6 +192,7 @@ mod tests { Builtin { r#type: Special, execute: stub_builtin, + is_declaration_utility: Some(false), }, ); let condition = "foo".parse().unwrap(); diff --git a/yash-semantics/src/command/compound_command/for_loop.rs b/yash-semantics/src/command/compound_command/for_loop.rs index bf1b190a..a775ba9e 100644 --- a/yash-semantics/src/command/compound_command/for_loop.rs +++ b/yash-semantics/src/command/compound_command/for_loop.rs @@ -218,8 +218,14 @@ mod tests { }) } let mut env = Env::new_virtual(); - let r#type = yash_env::builtin::Type::Mandatory; - env.builtins.insert("check", Builtin { r#type, execute }); + env.builtins.insert( + "check", + Builtin { + r#type: yash_env::builtin::Type::Mandatory, + execute, + is_declaration_utility: Some(false), + }, + ); let command: CompoundCommand = "for i in 1; do check; done".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); diff --git a/yash-semantics/src/command/compound_command/subshell.rs b/yash-semantics/src/command/compound_command/subshell.rs index f1924dac..c830292f 100644 --- a/yash-semantics/src/command/compound_command/subshell.rs +++ b/yash-semantics/src/command/compound_command/subshell.rs @@ -126,6 +126,7 @@ mod tests { yash_env::builtin::Builtin { r#type: yash_env::builtin::Type::Special, execute: exit_builtin, + is_declaration_utility: Some(false), }, ); @@ -238,6 +239,7 @@ mod tests { yash_env::builtin::Builtin { r#type: yash_env::builtin::Type::Special, execute: trap_builtin, + is_declaration_utility: Some(false), }, ); diff --git a/yash-semantics/src/command/compound_command/while_loop.rs b/yash-semantics/src/command/compound_command/while_loop.rs index c56ecd8c..e681d201 100644 --- a/yash-semantics/src/command/compound_command/while_loop.rs +++ b/yash-semantics/src/command/compound_command/while_loop.rs @@ -198,8 +198,14 @@ mod tests { }) } let (mut env, _state) = fixture(); - let r#type = yash_env::builtin::Type::Mandatory; - env.builtins.insert("check", Builtin { r#type, execute }); + env.builtins.insert( + "check", + Builtin { + r#type: yash_env::builtin::Type::Mandatory, + execute, + is_declaration_utility: Some(false), + }, + ); let command: CompoundCommand = "while check; do check; return; done".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); @@ -444,8 +450,14 @@ mod tests { }) } let (mut env, _state) = fixture(); - let r#type = yash_env::builtin::Type::Mandatory; - env.builtins.insert("check", Builtin { r#type, execute }); + env.builtins.insert( + "check", + Builtin { + r#type: yash_env::builtin::Type::Mandatory, + execute, + is_declaration_utility: Some(false), + }, + ); let command: CompoundCommand = "until ! check; do check; return; done".parse().unwrap(); let result = command.execute(&mut env).now_or_never().unwrap(); diff --git a/yash-semantics/src/command/pipeline.rs b/yash-semantics/src/command/pipeline.rs index eb377fdb..25577c22 100644 --- a/yash-semantics/src/command/pipeline.rs +++ b/yash-semantics/src/command/pipeline.rs @@ -563,6 +563,7 @@ mod tests { Builtin { r#type: Special, execute: stub_builtin, + is_declaration_utility: Some(false), }, ); let pipeline: syntax::Pipeline = "foo".parse().unwrap(); @@ -591,6 +592,7 @@ mod tests { Builtin { r#type: Special, execute: stub_builtin, + is_declaration_utility: Some(false), }, ); let pipeline: syntax::Pipeline = "! foo".parse().unwrap(); @@ -614,6 +616,7 @@ mod tests { Builtin { r#type: Special, execute: stub_builtin, + is_declaration_utility: Some(false), }, ); env.options.set(Monitor, On); diff --git a/yash-semantics/src/command/simple_command.rs b/yash-semantics/src/command/simple_command.rs index c1c197cc..0ef85fea 100644 --- a/yash-semantics/src/command/simple_command.rs +++ b/yash-semantics/src/command/simple_command.rs @@ -23,7 +23,7 @@ use crate::command::Command; use crate::command_search::search; -use crate::expansion::expand_words; +use crate::expansion::expand_word_with_mode; use crate::xtrace::XTrace; use crate::Handle; use std::ffi::CString; @@ -31,7 +31,6 @@ use std::ops::ControlFlow::Continue; #[cfg(doc)] use yash_env::semantics::Divert; use yash_env::semantics::ExitStatus; -#[cfg(doc)] use yash_env::semantics::Field; use yash_env::semantics::Result; #[cfg(doc)] @@ -40,6 +39,8 @@ use yash_env::variable::Scope; use yash_env::Env; use yash_syntax::syntax; use yash_syntax::syntax::Assign; +use yash_syntax::syntax::ExpansionMode; +use yash_syntax::syntax::Word; /// Executes the simple command. /// @@ -193,6 +194,21 @@ impl Command for syntax::SimpleCommand { } } +async fn expand_words( + env: &mut Env, + words: &[(Word, ExpansionMode)], +) -> crate::expansion::Result<(Vec, Option)> { + let mut fields = Vec::new(); + let mut last_exit_status = None; + for (word, mode) in words { + let exit_status = expand_word_with_mode(env, word, *mode, &mut fields).await?; + if exit_status.is_some() { + last_exit_status = exit_status; + } + } + Ok((fields, last_exit_status)) +} + async fn perform_assignments( env: &mut Env, assigns: &[Assign], diff --git a/yash-semantics/src/command/simple_command/builtin.rs b/yash-semantics/src/command/simple_command/builtin.rs index d9abbef3..c66daee0 100644 --- a/yash-semantics/src/command/simple_command/builtin.rs +++ b/yash-semantics/src/command/simple_command/builtin.rs @@ -129,6 +129,7 @@ mod tests { ) })) }, + is_declaration_utility: Some(false), }, ); let command: syntax::SimpleCommand = "foo".parse().unwrap(); @@ -184,6 +185,7 @@ mod tests { result }) }, + is_declaration_utility: Some(false), }, ); let command: syntax::SimpleCommand = "exec >/tmp/file".parse().unwrap(); @@ -284,6 +286,7 @@ mod tests { Builtin { r#type: yash_env::builtin::Type::Mandatory, execute: builtin_main, + is_declaration_utility: Some(false), }, ); env.builtins.insert( @@ -291,6 +294,7 @@ mod tests { Builtin { r#type: yash_env::builtin::Type::Special, execute: special_main, + is_declaration_utility: Some(false), }, ); let command: syntax::SimpleCommand = "builtin".parse().unwrap(); diff --git a/yash-semantics/src/command_search.rs b/yash-semantics/src/command_search.rs index b20ce76e..f1a45856 100644 --- a/yash-semantics/src/command_search.rs +++ b/yash-semantics/src/command_search.rs @@ -294,6 +294,7 @@ mod tests { Builtin { r#type: Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, ); let function = Function::new("foo", full_compound_command(""), Location::dummy("")); @@ -309,6 +310,7 @@ mod tests { let builtin = Builtin { r#type: Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); @@ -341,6 +343,7 @@ mod tests { let builtin = Builtin { r#type: Special, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); let function = Function::new( @@ -364,6 +367,7 @@ mod tests { let builtin = Builtin { r#type: Mandatory, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); @@ -381,6 +385,7 @@ mod tests { let builtin = Builtin { r#type: Elective, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); @@ -398,6 +403,7 @@ mod tests { let builtin = Builtin { r#type: Extension, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); @@ -417,6 +423,7 @@ mod tests { Builtin { r#type: Mandatory, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, ); @@ -440,6 +447,7 @@ mod tests { Builtin { r#type: Elective, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, ); @@ -463,6 +471,7 @@ mod tests { Builtin { r#type: Extension, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }, ); @@ -484,6 +493,7 @@ mod tests { let builtin = Builtin { r#type: Substitutive, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); env.path = Expansion::from("/bin"); @@ -504,6 +514,7 @@ mod tests { let builtin = Builtin { r#type: Substitutive, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); @@ -517,6 +528,7 @@ mod tests { let builtin = Builtin { r#type: Substitutive, execute: |_, _| unreachable!(), + is_declaration_utility: Some(false), }; env.builtins.insert("foo", builtin); env.path = Expansion::from("/bin"); diff --git a/yash-semantics/src/expansion.rs b/yash-semantics/src/expansion.rs index f6cf5006..c6bdc9f8 100644 --- a/yash-semantics/src/expansion.rs +++ b/yash-semantics/src/expansion.rs @@ -17,10 +17,11 @@ //! Word expansion. //! //! The word expansion involves many kinds of operations described below. -//! The [`expand_words`] function carries out all of them and produces any -//! number of fields depending on the expanded word. The [`expand_word_attr`] +//! The [`expand_word_multiple`] function performs all of them and produces +//! any number of fields depending on the expanded word. The [`expand_word_attr`] //! and [`expand_word`] functions omit some of them to ensure that the result is -//! a single field. +//! a single field. Other functions in this module are provided for convenience +//! in specific situations. //! //! # Initial expansion //! @@ -43,6 +44,7 @@ //! ## Brace expansion //! //! The brace expansion produces copies of a field containing a pair of braces. +//! (TODO: This feature is not yet implemented.) //! //! ## Field splitting //! @@ -60,10 +62,11 @@ //! //! The [quote removal](self::quote_removal) drops characters quoting other //! characters, and the [attribute stripping](self::attr_strip) converts -//! [`AttrField`]s into bare [`Field`]s. In [`expand_words`], the quote removal -//! is performed between the field splitting and pathname expansion, and the -//! attribute stripping is part of the pathname expansion. In [`expand_word`], -//! they are carried out as the last step of the whole expansion. +//! [`AttrField`]s into bare [`Field`]s. In [`expand_word_multiple`], the quote +//! removal is performed between the field splitting and pathname expansion, and +//! the attribute stripping is part of the pathname expansion. In +//! [`expand_word`], they are carried out as the last step of the whole +//! expansion. pub mod attr; pub mod attr_strip; @@ -98,6 +101,7 @@ use yash_syntax::source::pretty::AnnotationType; use yash_syntax::source::pretty::Footer; use yash_syntax::source::pretty::MessageBase; use yash_syntax::source::Location; +use yash_syntax::syntax::ExpansionMode; use yash_syntax::syntax::Param; use yash_syntax::syntax::Text; use yash_syntax::syntax::Word; @@ -351,6 +355,7 @@ pub async fn expand_word_attr( /// /// To expand a word to an [`AttrField`] without performing quote removal or /// attribute stripping, use [`expand_word_attr`]. +/// To expand a word to multiple fields, use [`expand_word_multiple`]. /// To expand multiple words to multiple fields, use [`expand_words`]. pub async fn expand_word( env: &mut yash_env::Env, @@ -361,32 +366,29 @@ pub async fn expand_word( Ok((field, exit_status)) } -/// Expands words to fields. +/// Expands a word to fields. /// /// This function performs the initial expansion and multi-field expansion, -/// including quote removal and attribute stripping. -/// The second field of the result tuple is the exit status of the last command -/// substitution performed during the expansion, if any. +/// including quote removal and attribute stripping. The results are appended to +/// the given collection. The return value is the exit status of the last +/// command substitution performed during the expansion, if any. /// /// To expand a single word to a single field, use [`expand_word`]. -pub async fn expand_words<'a, I: IntoIterator>( +/// To expand multiple words to fields, use [`expand_words`]. +pub async fn expand_word_multiple( env: &mut yash_env::Env, - words: I, -) -> Result<(Vec, Option)> { + word: &Word, + results: &mut R, +) -> Result> +where + R: Extend, +{ let mut env = initial::Env::new(env); // initial expansion // - let words = words.into_iter(); - let mut fields = Vec::with_capacity(words.size_hint().0); - for word in words { - let phrase = word.expand(&mut env).await?; - fields.extend(phrase.into_iter().map(|chars| AttrField { - chars, - origin: word.location.clone(), - })); - } + let phrase = word.expand(&mut env).await?; - // TODO brace expansion + // TODO brace expansion // // field splitting // let ifs = env @@ -395,18 +397,78 @@ pub async fn expand_words<'a, I: IntoIterator>( .get_scalar(IFS) .map(Ifs::new) .unwrap_or_default(); - let mut split_fields = Vec::with_capacity(fields.len()); - for field in fields { - split::split_into(field, &ifs, &mut split_fields); + let mut split_fields = Vec::with_capacity(phrase.field_count()); + for chars in phrase { + let origin = word.location.clone(); + let attr_field = AttrField { chars, origin }; + split::split_into(attr_field, &ifs, &mut split_fields); } drop(ifs); // pathname expansion (including quote removal and attribute stripping) // - let mut fields = Vec::with_capacity(split_fields.len()); for field in split_fields { - fields.extend(glob(env.inner, field)); + results.extend(glob(env.inner, field)); + } + + Ok(env.last_command_subst_exit_status) +} + +/// Expands a word to fields. +/// +/// This function expands a word to fields using the specified expansion mode +/// and appends the results to the given collection. +/// +/// If the specified mode is [`ExpansionMode::Multiple`], this function performs +/// the initial expansion and multi-field expansion, including quote removal and +/// attribute stripping (see [`expand_word_multiple`]). If the mode is +/// [`ExpansionMode::Single`], this function performs the initial expansion, +/// quote removal, and attribute stripping, but not multi-field expansion (see +/// [`expand_word`]). +/// +/// The results are appended to the given collection. +pub async fn expand_word_with_mode( + env: &mut yash_env::Env, + word: &Word, + mode: ExpansionMode, + results: &mut R, +) -> Result> +where + R: Extend, +{ + match mode { + ExpansionMode::Single => { + let (field, exit_status) = expand_word(env, word).await?; + results.extend(std::iter::once(field)); + Ok(exit_status) + } + ExpansionMode::Multiple => expand_word_multiple(env, word, results).await, + } +} + +/// Expands words to fields. +/// +/// This function performs the initial expansion and multi-field expansion, +/// including quote removal and attribute stripping. +/// The second field of the result tuple is the exit status of the last command +/// substitution performed during the expansion, if any. +/// +/// To expand a single word to a single field, use [`expand_word`]. +/// To expand a single word to multiple fields, use [`expand_word_multiple`]. +pub async fn expand_words<'a, I: IntoIterator>( + env: &mut yash_env::Env, + words: I, +) -> Result<(Vec, Option)> { + let mut fields = Vec::new(); + let mut last_exit_status = None; + + for word in words { + let exit_status = expand_word_multiple(env, word, &mut fields).await?; + if exit_status.is_some() { + last_exit_status = exit_status; + } } - Ok((fields, env.last_command_subst_exit_status)) + + Ok((fields, last_exit_status)) } /// Expands an assignment value. @@ -504,12 +566,15 @@ mod tests { } #[test] - fn expand_words_performs_initial_expansion() { + fn expand_word_multiple_performs_initial_expansion() { in_virtual_system(|mut env, _state| async move { env.builtins.insert("echo", echo_builtin()); env.builtins.insert("return", return_builtin()); - let words = &["[$(echo echoed; return -n 42)]".parse().unwrap()]; - let (fields, exit_status) = expand_words(&mut env, words).await.unwrap(); + let word = "[$(echo echoed; return -n 42)]".parse().unwrap(); + let mut fields = Vec::new(); + let exit_status = expand_word_multiple(&mut env, &word, &mut fields) + .await + .unwrap(); assert_eq!(exit_status, Some(ExitStatus(42))); assert_matches!(fields.as_slice(), [f] => { assert_eq!(f.value, "[echoed]"); @@ -518,15 +583,18 @@ mod tests { } #[test] - fn expand_words_performs_field_splitting_possibly_with_default_ifs() { + fn expand_word_multiple_performs_field_splitting_possibly_with_default_ifs() { let mut env = yash_env::Env::new_virtual(); env.variables .get_or_new("v", Scope::Global) .assign("foo bar ", None) .unwrap(); - let words = &["$v".parse().unwrap()]; - let result = expand_words(&mut env, words).now_or_never().unwrap(); - let (fields, exit_status) = result.unwrap(); + let word = "$v".parse().unwrap(); + let mut fields = Vec::new(); + let exit_status = expand_word_multiple(&mut env, &word, &mut fields) + .now_or_never() + .unwrap() + .unwrap(); assert_eq!(exit_status, None); assert_matches!(fields.as_slice(), [f1, f2] => { assert_eq!(f1.value, "foo"); @@ -535,7 +603,7 @@ mod tests { } #[test] - fn expand_words_performs_field_splitting_with_current_ifs() { + fn expand_word_multiple_performs_field_splitting_with_current_ifs() { let mut env = yash_env::Env::new_virtual(); env.variables .get_or_new("v", Scope::Global) @@ -545,9 +613,12 @@ mod tests { .get_or_new(IFS, Scope::Global) .assign(" o", None) .unwrap(); - let words = &["$v".parse().unwrap()]; - let result = expand_words(&mut env, words).now_or_never().unwrap(); - let (fields, exit_status) = result.unwrap(); + let word = "$v".parse().unwrap(); + let mut fields = Vec::new(); + let exit_status = expand_word_multiple(&mut env, &word, &mut fields) + .now_or_never() + .unwrap() + .unwrap(); assert_eq!(exit_status, None); assert_matches!(fields.as_slice(), [f1, f2, f3] => { assert_eq!(f1.value, "f"); @@ -557,17 +628,49 @@ mod tests { } #[test] - fn expand_words_performs_quote_removal() { + fn expand_word_multiple_performs_quote_removal() { let mut env = yash_env::Env::new_virtual(); - let words = &["\"foo\"'$v'".parse().unwrap()]; - let result = expand_words(&mut env, words).now_or_never().unwrap(); - let (fields, exit_status) = result.unwrap(); + let word = "\"foo\"'$v'".parse().unwrap(); + let mut fields = Vec::new(); + let exit_status = expand_word_multiple(&mut env, &word, &mut fields) + .now_or_never() + .unwrap() + .unwrap(); assert_eq!(exit_status, None); assert_matches!(fields.as_slice(), [f] => { assert_eq!(f.value, "foo$v"); }); } + #[test] + fn expand_words_returns_exit_status_of_last_command_substitution() { + in_virtual_system(|mut env, _state| async move { + env.builtins.insert("return", return_builtin()); + let word1 = "$(return -n 12)".parse().unwrap(); + let word2 = "$(return -n 34)$(return -n 56)".parse().unwrap(); + let (_, exit_status) = expand_words(&mut env, &[word1, word2]).await.unwrap(); + assert_eq!(exit_status, Some(ExitStatus(56))); + }) + } + + #[test] + fn expand_words_performs_field_splitting() { + let mut env = yash_env::Env::new_virtual(); + env.variables + .get_or_new("v", Scope::Global) + .assign(" foo bar ", None) + .unwrap(); + let word = "$v".parse().unwrap(); + let (fields, _) = expand_words(&mut env, &[word]) + .now_or_never() + .unwrap() + .unwrap(); + assert_matches!(fields.as_slice(), [f1, f2] => { + assert_eq!(f1.value, "foo"); + assert_eq!(f2.value, "bar"); + }); + } + #[test] fn expand_value_scalar() { let mut env = yash_env::Env::new_virtual(); diff --git a/yash-semantics/src/runner.rs b/yash-semantics/src/runner.rs index b82d2841..dc9cbb75 100644 --- a/yash-semantics/src/runner.rs +++ b/yash-semantics/src/runner.rs @@ -147,7 +147,12 @@ async fn read_eval_loop_impl( lexer.flush(); } - let command = Parser::new(lexer, env).command_line().await; + let command = Parser::config() + .aliases(env) + .declaration_utilities(env) + .input(lexer) + .command_line() + .await; let env = &mut **env.borrow_mut(); diff --git a/yash-semantics/src/runner_legacy.rs b/yash-semantics/src/runner_legacy.rs index 0944cd99..46717c67 100644 --- a/yash-semantics/src/runner_legacy.rs +++ b/yash-semantics/src/runner_legacy.rs @@ -142,7 +142,10 @@ impl<'a, 'b> ReadEvalLoop<'a, 'b> { verbose.set(self.env.options.get(Verbose)); } - let mut parser = Parser::new(self.lexer, &self.env.aliases); + let mut parser = Parser::config() + .aliases(&self.env) + .declaration_utilities(&self.env) + .input(self.lexer); match parser.command_line().await { Ok(Some(command)) => { run_traps_for_caught_signals(self.env).await?; diff --git a/yash-semantics/src/tests.rs b/yash-semantics/src/tests.rs index 810e85a5..f97f0ab3 100644 --- a/yash-semantics/src/tests.rs +++ b/yash-semantics/src/tests.rs @@ -53,6 +53,7 @@ pub fn exit_builtin() -> Builtin { Builtin { r#type: Special, execute: exit_builtin_main, + is_declaration_utility: Some(false), } } @@ -79,6 +80,7 @@ pub fn return_builtin() -> Builtin { Builtin { r#type: Special, execute: return_builtin_main, + is_declaration_utility: Some(false), } } @@ -99,6 +101,7 @@ pub fn break_builtin() -> Builtin { Builtin { r#type: Special, execute: break_builtin_main, + is_declaration_utility: Some(false), } } @@ -119,6 +122,7 @@ pub fn continue_builtin() -> Builtin { Builtin { r#type: Special, execute: continue_builtin_main, + is_declaration_utility: Some(false), } } @@ -137,6 +141,7 @@ pub fn suspend_builtin() -> Builtin { Builtin { r#type: Special, execute: suspend_builtin_main, + is_declaration_utility: Some(false), } } @@ -175,6 +180,7 @@ pub fn local_builtin() -> Builtin { Builtin { r#type: Mandatory, execute: local_builtin_main, + is_declaration_utility: Some(true), } } @@ -198,6 +204,7 @@ pub fn echo_builtin() -> Builtin { Builtin { r#type: Mandatory, execute: echo_builtin_main, + is_declaration_utility: Some(false), } } @@ -230,5 +237,6 @@ pub fn cat_builtin() -> Builtin { Builtin { r#type: Mandatory, execute: cat_builtin_main, + is_declaration_utility: Some(false), } } diff --git a/yash-semantics/src/trap/exit.rs b/yash-semantics/src/trap/exit.rs index 1383c30e..013772bd 100644 --- a/yash-semantics/src/trap/exit.rs +++ b/yash-semantics/src/trap/exit.rs @@ -101,8 +101,14 @@ mod tests { }) } let mut env = Env::new_virtual(); - let r#type = yash_env::builtin::Type::Mandatory; - env.builtins.insert("check", Builtin { r#type, execute }); + env.builtins.insert( + "check", + Builtin { + r#type: yash_env::builtin::Type::Mandatory, + execute, + is_declaration_utility: Some(false), + }, + ); env.traps .set_action( &mut env.system, diff --git a/yash-semantics/src/trap/signal.rs b/yash-semantics/src/trap/signal.rs index 2c1dad20..3d7643cc 100644 --- a/yash-semantics/src/trap/signal.rs +++ b/yash-semantics/src/trap/signal.rs @@ -211,8 +211,14 @@ mod tests { } let system = VirtualSystem::default(); let mut env = Env::with_system(Box::new(system.clone())); - let r#type = yash_env::builtin::Type::Mandatory; - env.builtins.insert("check", Builtin { r#type, execute }); + env.builtins.insert( + "check", + Builtin { + r#type: yash_env::builtin::Type::Mandatory, + execute, + is_declaration_utility: Some(false), + }, + ); env.traps .set_action( &mut env.system, diff --git a/yash-syntax/CHANGELOG.md b/yash-syntax/CHANGELOG.md index 5f1e4ab4..71f698c0 100644 --- a/yash-syntax/CHANGELOG.md +++ b/yash-syntax/CHANGELOG.md @@ -5,6 +5,32 @@ 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.14.0] - Unreleased + +This version adds support for declaration utilities. It also reorganizes how the +parser is configured on construction, so that the parser can be constructed with +more flexible and readable configurations. + +### Added + +- The `syntax::ExpansionMode` enum is added to represent how a word is expanded. +- The `decl_util` module is added, which contains the `Glossary` trait and the + `EmptyGlossary` and `PosixGlossary` structs. +- Added the `Config` struct to the `parser` module. Currently, it allows + setting glossaries for aliases and declaration utilities. +- The `syntax::Word::parse_tilde_everywhere_after` method is added. + +### Changed + +- The `syntax::SimpleCommand::words` field is now a `Vec<(Word, ExpansionMode)>` + instead of a `Vec`. +- The `parser::Parser::new` function now only takes a `&mut Lexer` argument. + The `&dyn alias::Glossary` argument has been removed in favor of the `Config` + struct. +- When a simple command is parsed, the parser now checks if the command name is + a declaration utility. If it is, following words in an assignment form are + parsed like assignments. + ## [0.13.0] - 2024-12-14 ### Added @@ -404,6 +430,7 @@ command. - Functionalities to parse POSIX shell scripts - Alias substitution support +[0.14.0]: https://github.com/magicant/yash-rs/releases/tag/yash-syntax-0.14.0 [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 diff --git a/yash-syntax/Cargo.toml b/yash-syntax/Cargo.toml index f208decf..fa709540 100644 --- a/yash-syntax/Cargo.toml +++ b/yash-syntax/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yash-syntax" -version = "0.13.0" +version = "0.14.0" authors = ["WATANABE Yuki "] edition = "2021" rust-version = "1.82.0" @@ -12,6 +12,7 @@ repository = "https://github.com/magicant/yash-rs" license = "GPL-3.0-or-later" keywords = ["posix", "shell"] categories = ["command-line-utilities", "parser-implementations"] +publish = false [dependencies] annotate-snippets = { version = "0.11.4", optional = true } diff --git a/yash-syntax/src/decl_util.rs b/yash-syntax/src/decl_util.rs new file mode 100644 index 00000000..5bb2ab73 --- /dev/null +++ b/yash-syntax/src/decl_util.rs @@ -0,0 +1,214 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2024 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Defining declaration utilities +//! +//! This module contains the [`Glossary`] trait, which is used by the parser to +//! determine whether a command name is a declaration utility. It also provides +//! two implementations of the `Glossary` trait: [`EmptyGlossary`] and +//! [`PosixGlossary`]. +//! +//! # What are declaration utilities? +//! +//! A [declaration utility] is a type of command that causes its argument words +//! to be expanded in a manner slightly different from other commands. Usually, +//! command word expansion includes field splitting and pathname expansion. For +//! declaration utilities, however, those expansions are not performed on the +//! arguments that have a form of variable assignments. +//! +//! [declaration utility]: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap03.html#tag_03_100 +//! +//! Generally, a simple command consists of assignments, redirections, and command +//! words. The shell syntax allows the redirections to be placed anywhere in the +//! command, but the assignments must come before the command words. An assignment +//! token has the form `name=value`, the first token that does not match this +//! form is considered the command name, and the rest are arguments regardless of +//! whether they match the form. For example, in the command `a=1 b=2 echo c=3`, +//! `a=1` and `b=2` are assignments, `echo` is the command name, and `c=3` is an +//! argument. +//! +//! All assignments and command words are expanded when the command is executed, +//! but the expansions are different. The expansions of assignments are performed +//! in a way that does not include field splitting and pathname expansion. This +//! ensures that the values of the assignments are not split or expanded into +//! multiple fields. The expansions of command words, on the other hand, are +//! performed in a way that includes field splitting and pathname expansion, +//! which may expand a single word into multiple fields. +//! +//! The assignments specified in a simple command are performed by the shell +//! before the utility specified by the command name is invoked. However, some +//! utilities perform their own assignments based on their arguments. For such +//! a utility, the tokens that specify the assigned variable names and values +//! are given as arguments to the utility as in the command `export a=1 b=2`. +//! +//! By default, such arguments are expanded in the same way as usual command +//! words, which means that the assignments are subject to field splitting and +//! pathname expansion even though they are effectively assignments. To prevent +//! this, the shell recognizes certain command names as declaration utilities +//! and expands their arguments differently. The shell does not perform field +//! splitting and pathname expansion on the arguments of declaration utilities +//! that have the form of variable assignments. +//! +//! # Example +//! +//! POSIX requires the `export` utility to be recognized as a declaration +//! utility. In the command `v='1 b=2'; export a=$v`, the word `a=$v` is not +//! subject to field splitting because `export` is a declaration utility, so the +//! expanded word `a=1 b=2` is passed to `export` as an argument, so `export` +//! assigns the value `1 b=2` to the variable `a`. If `export` were not a +//! declaration utility, the word `a=$v` would be subject to field splitting, +//! and the expanded word `a=1 b=2` would be split into two fields `a=1` and +//! `b=2`, so `export` would assign the value `1` to the variable `a` and the +//! value `2` to the variable `b`. +//! +//! # Which command names are declaration utilities? +//! +//! The POSIX standard specifies that the following command names are declaration +//! utilities: +//! +//! - `export` and `readonly` are declaration utilities. +//! - `command` is neutral; it delegates to the next command word to determine +//! whether it is a declaration utility. +//! +//! It is unspecified whether other command names are declaration utilities. +//! +//! The syntax parser in this crate uses the [`Glossary`] trait to determine +//! whether a command name is a declaration utility. The parser calls its +//! [`is_declaration_utility`] method when it encounters a command name, and +//! changes how the following arguments are parsed based on the result. +//! +//! [`is_declaration_utility`]: Glossary::is_declaration_utility +//! +//! This module provides two implementations of the `Glossary` trait: +//! +//! - [`PosixGlossary`] recognizes the declaration utilities defined by POSIX +//! (and no others). This is the default glossary used by the parser. +//! - [`EmptyGlossary`] recognizes no command name as a declaration utility. +//! The parse result does not conform to POSIX when this glossary is used. +//! +//! You can implement the `Glossary` trait for your own glossary if you want to +//! recognize additional command names as declaration utilities. (In yash-rs, +//! the `yash-env` crate provides a shell environment that implements `Glossary` +//! based on the built-ins defined in the environment.) +//! +//! The glossary can be set to the parser with [`Config::declaration_utilities`]. +//! +//! [`Config::declaration_utilities`]: crate::parser::Config::declaration_utilities +//! +//! # Parser behavior +//! +//! When the [parser] recognizes a command name as a declaration utility, +//! command words that follow the command name are tested for the form of +//! variable assignments. If a word is a variable assignment, it is parsed as +//! such: the word is split into a variable name and a value, and tilde expansions +//! are parsed with the [`parse_tilde_everywhere_after`] method in the value part. +//! The result word is marked with [`ExpansionMode::Single`] in +//! [`SimpleCommand::words`] to indicate that the word is not subject to field +//! splitting and pathname expansion. If a word is not a variable assignment, it +//! is parsed as a normal command word with [`parse_tilde_front`] and marked with +//! [`ExpansionMode::Multiple`]. +//! +//! The shell is expected to change the expansion behavior of the words based on +//! the [`ExpansionMode`] of the words. In yash-rs, the semantics is implemented +//! in the `yash-semantics` crate. +//! +//! [parser]: crate::parser +//! [`parse_tilde_front`]: crate::syntax::Word::parse_tilde_front +//! [`parse_tilde_everywhere_after`]: crate::syntax::Word::parse_tilde_everywhere_after +//! [`ExpansionMode`]: crate::syntax::ExpansionMode +//! [`ExpansionMode::Multiple`]: crate::syntax::ExpansionMode::Multiple +//! [`ExpansionMode::Single`]: crate::syntax::ExpansionMode::Single +//! [`SimpleCommand::words`]: crate::syntax::SimpleCommand::words + +use std::cell::RefCell; +use std::fmt::Debug; + +/// Interface used by the parser to tell if a command name is a declaration utility +/// +/// The parser uses this trait to determine whether a command name is a declaration +/// utility. See the [module-level documentation](self) for details. +pub trait Glossary: Debug { + /// Returns whether the given command name is a declaration utility. + /// + /// If the command name is a declaration utility, this method should return + /// `Some(true)`. If the command name is not a declaration utility, this + /// method should return `Some(false)`. If the return value is `None`, this + /// method is called again with the next command word in the simple command + /// being parsed, effectively delegating the decision to the next command word. + /// + /// To meet the POSIX standard, the method should return `Some(true)` for the + /// command names `export` and `readonly`, and `None` for the command name + /// `command`. + fn is_declaration_utility(&self, name: &str) -> Option; +} + +/// Empty glossary that does not recognize any command name as a declaration utility +/// +/// When this glossary is used, the parser recognizes no command name as a +/// declaration utility. Note that this does not conform to POSIX. +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct EmptyGlossary; + +impl Glossary for EmptyGlossary { + #[inline(always)] + fn is_declaration_utility(&self, _name: &str) -> Option { + Some(false) + } +} + +/// Glossary that recognizes declaration utilities defined by POSIX +/// +/// This glossary recognizes the declaration utilities defined by POSIX and no +/// others. The `is_declaration_utility` method returns `Some(true)` for the +/// command names `export` and `readonly`, and `None` for the command name +/// `command`. +/// +/// This is the minimal glossary that conforms to POSIX, and is the default +/// glossary used by the parser. +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct PosixGlossary; + +impl Glossary for PosixGlossary { + fn is_declaration_utility(&self, name: &str) -> Option { + match name { + "export" | "readonly" => Some(true), + "command" => None, + _ => Some(false), + } + } +} + +impl Glossary for &T { + fn is_declaration_utility(&self, name: &str) -> Option { + (**self).is_declaration_utility(name) + } +} + +impl Glossary for &mut T { + fn is_declaration_utility(&self, name: &str) -> Option { + (**self).is_declaration_utility(name) + } +} + +/// Allows a glossary to be wrapped in a `RefCell`. +/// +/// This implementation's methods immutably borrow the inner glossary. +/// If the inner glossary is mutably borrowed at the same time, it panics. +impl Glossary for RefCell { + fn is_declaration_utility(&self, name: &str) -> Option { + self.borrow().is_declaration_utility(name) + } +} diff --git a/yash-syntax/src/lib.rs b/yash-syntax/src/lib.rs index e00593cd..2e042332 100644 --- a/yash-syntax/src/lib.rs +++ b/yash-syntax/src/lib.rs @@ -39,6 +39,7 @@ //! aliases that are recognized while parsing. pub mod alias; +pub mod decl_util; pub mod input; pub mod parser; pub mod source; diff --git a/yash-syntax/src/parser.rs b/yash-syntax/src/parser.rs index c7e803c6..295c6788 100644 --- a/yash-syntax/src/parser.rs +++ b/yash-syntax/src/parser.rs @@ -45,8 +45,7 @@ //! //! // Then, create a new parser borrowing the lexer. //! use yash_syntax::parser::Parser; -//! use yash_syntax::alias::EmptyGlossary; -//! let mut parser = Parser::new(&mut lexer, &EmptyGlossary); +//! let mut parser = Parser::new(&mut lexer); //! //! // Lastly, call the parser's function to get an AST. //! use futures_executor::block_on; @@ -89,6 +88,7 @@ mod while_loop; pub mod lex; +pub use self::core::Config; pub use self::core::Parser; pub use self::core::Rec; pub use self::core::Result; diff --git a/yash-syntax/src/parser/and_or.rs b/yash-syntax/src/parser/and_or.rs index 57ba3d40..956ffa37 100644 --- a/yash-syntax/src/parser/and_or.rs +++ b/yash-syntax/src/parser/and_or.rs @@ -75,14 +75,13 @@ mod tests { use super::super::error::ErrorCause; use super::super::lex::Lexer; use super::*; - use crate::alias::EmptyGlossary; use crate::source::Source; use futures_util::FutureExt; #[test] fn parser_and_or_list_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.and_or_list().now_or_never().unwrap(); assert_eq!(result, Ok(Rec::Parsed(None))); @@ -91,7 +90,7 @@ mod tests { #[test] fn parser_and_or_list_one() { let mut lexer = Lexer::from_memory("foo", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.and_or_list().now_or_never().unwrap(); let aol = result.unwrap().unwrap().unwrap(); @@ -102,7 +101,7 @@ mod tests { #[test] fn parser_and_or_list_many() { let mut lexer = Lexer::from_memory("first && second || \n\n third;", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.and_or_list().now_or_never().unwrap(); let aol = result.unwrap().unwrap().unwrap(); @@ -117,7 +116,7 @@ mod tests { #[test] fn parser_and_or_list_missing_command_after_and_and() { let mut lexer = Lexer::from_memory("foo &&", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.and_or_list().now_or_never().unwrap().unwrap_err(); assert_eq!( diff --git a/yash-syntax/src/parser/case.rs b/yash-syntax/src/parser/case.rs index e0109041..5317446b 100644 --- a/yash-syntax/src/parser/case.rs +++ b/yash-syntax/src/parser/case.rs @@ -191,7 +191,7 @@ mod tests { use super::super::error::ErrorCause; use super::super::lex::Lexer; use super::*; - use crate::alias::{AliasSet, EmptyGlossary, HashEntry}; + use crate::alias::{AliasSet, HashEntry}; use crate::source::Location; use crate::source::Source; use crate::syntax::CaseContinuation; @@ -216,7 +216,7 @@ mod tests { true, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let option = parser.case_item().now_or_never().unwrap().unwrap(); assert_eq!(option, None); @@ -228,7 +228,7 @@ mod tests { #[test] fn parser_case_item_minimum() { let mut lexer = Lexer::from_memory("foo)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -244,7 +244,7 @@ mod tests { #[test] fn parser_case_item_with_open_paren() { let mut lexer = Lexer::from_memory("(foo)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -260,7 +260,7 @@ mod tests { #[test] fn parser_case_item_many_patterns() { let mut lexer = Lexer::from_memory("1 | esac | $three)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 3); @@ -278,7 +278,7 @@ mod tests { #[test] fn parser_case_item_non_empty_body() { let mut lexer = Lexer::from_memory("foo)\necho ok\n:&\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -296,7 +296,7 @@ mod tests { #[test] fn parser_case_item_with_double_semicolon() { let mut lexer = Lexer::from_memory("foo);;", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -312,7 +312,7 @@ mod tests { #[test] fn parser_case_item_with_non_empty_body_and_double_semicolon() { let mut lexer = Lexer::from_memory("foo):;\n;;", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -329,7 +329,7 @@ mod tests { #[test] fn parser_case_item_with_semicolon_and() { let mut lexer = Lexer::from_memory("foo);&", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -345,7 +345,7 @@ mod tests { #[test] fn parser_case_item_missing_pattern_without_open_paren() { let mut lexer = Lexer::from_memory(")", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.case_item().now_or_never().unwrap().unwrap_err(); assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingPattern)); @@ -358,7 +358,7 @@ mod tests { #[test] fn parser_case_item_esac_after_paren() { let mut lexer = Lexer::from_memory("(esac)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(item.patterns.len(), 1); @@ -371,7 +371,7 @@ mod tests { #[test] fn parser_case_item_first_pattern_not_word_after_open_paren() { let mut lexer = Lexer::from_memory("(&", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.case_item().now_or_never().unwrap().unwrap_err(); assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidPattern)); @@ -384,7 +384,7 @@ mod tests { #[test] fn parser_case_item_missing_pattern_after_bar() { let mut lexer = Lexer::from_memory("(foo| |", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.case_item().now_or_never().unwrap().unwrap_err(); assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingPattern)); @@ -397,7 +397,7 @@ mod tests { #[test] fn parser_case_item_missing_close_paren() { let mut lexer = Lexer::from_memory("(foo bar", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.case_item().now_or_never().unwrap().unwrap_err(); assert_eq!( @@ -413,7 +413,7 @@ mod tests { #[test] fn parser_case_command_minimum() { let mut lexer = Lexer::from_memory("case foo in esac", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -445,7 +445,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -480,7 +480,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -516,7 +516,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -535,7 +535,7 @@ mod tests { #[test] fn parser_case_command_one_item() { let mut lexer = Lexer::from_memory("case foo in bar) esac", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -555,7 +555,7 @@ mod tests { "case x in\n\na) ;; (b|c):&:; ;;\n d)echo\nesac", Source::Unknown, ); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -574,7 +574,7 @@ mod tests { #[test] fn parser_case_command_many_items_with_final_double_semicolon() { let mut lexer = Lexer::from_memory("case x in(1);; 2)echo\n\n;;\n\nesac", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -592,7 +592,7 @@ mod tests { #[test] fn parser_case_command_missing_subject() { let mut lexer = Lexer::from_memory(" case ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -606,7 +606,7 @@ mod tests { #[test] fn parser_case_command_invalid_subject() { let mut lexer = Lexer::from_memory(" case ; ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -620,7 +620,7 @@ mod tests { #[test] fn parser_case_command_missing_in() { let mut lexer = Lexer::from_memory(" case x esac", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -640,7 +640,7 @@ mod tests { #[test] fn parser_case_command_missing_esac() { let mut lexer = Lexer::from_memory("case x in a) }", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); diff --git a/yash-syntax/src/parser/command.rs b/yash-syntax/src/parser/command.rs index 644a32fd..0f97bea8 100644 --- a/yash-syntax/src/parser/command.rs +++ b/yash-syntax/src/parser/command.rs @@ -49,7 +49,6 @@ mod tests { use super::super::lex::Lexer; use super::super::lex::TokenId::EndOfInput; use super::*; - use crate::alias::EmptyGlossary; use crate::source::Source; use assert_matches::assert_matches; use futures_util::FutureExt; @@ -57,7 +56,7 @@ mod tests { #[test] fn parser_command_simple() { let mut lexer = Lexer::from_memory("foo < bar", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.command().now_or_never().unwrap(); let command = result.unwrap().unwrap().unwrap(); @@ -72,7 +71,7 @@ mod tests { #[test] fn parser_command_compound() { let mut lexer = Lexer::from_memory("(foo) < bar", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.command().now_or_never().unwrap(); let command = result.unwrap().unwrap().unwrap(); @@ -87,7 +86,7 @@ mod tests { #[test] fn parser_command_function() { let mut lexer = Lexer::from_memory("fun () ( echo )", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.command().now_or_never().unwrap(); let command = result.unwrap().unwrap().unwrap(); @@ -102,7 +101,7 @@ mod tests { #[test] fn parser_command_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.command().now_or_never().unwrap().unwrap(); assert_eq!(result, Rec::Parsed(None)); diff --git a/yash-syntax/src/parser/compound_command.rs b/yash-syntax/src/parser/compound_command.rs index 92fed476..34bfe810 100644 --- a/yash-syntax/src/parser/compound_command.rs +++ b/yash-syntax/src/parser/compound_command.rs @@ -96,10 +96,11 @@ mod tests { use super::super::lex::Operator::Semicolon; use super::super::lex::TokenId::EndOfInput; use super::*; - use crate::alias::{AliasSet, EmptyGlossary, HashEntry}; + use crate::alias::{AliasSet, HashEntry}; use crate::source::Location; use crate::source::Source; use crate::syntax::Command; + use crate::syntax::ExpansionMode; use crate::syntax::SimpleCommand; use assert_matches::assert_matches; use futures_util::FutureExt; @@ -107,7 +108,7 @@ mod tests { #[test] fn parser_do_clause_none() { let mut lexer = Lexer::from_memory("done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.do_clause().now_or_never().unwrap().unwrap(); assert!(result.is_none(), "result should be none: {result:?}"); @@ -116,7 +117,7 @@ mod tests { #[test] fn parser_do_clause_short() { let mut lexer = Lexer::from_memory("do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(result.to_string(), ":"); @@ -128,7 +129,7 @@ mod tests { #[test] fn parser_do_clause_long() { let mut lexer = Lexer::from_memory("do foo; bar& done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(result.to_string(), "foo; bar&"); @@ -140,7 +141,7 @@ mod tests { #[test] fn parser_do_clause_unclosed() { let mut lexer = Lexer::from_memory(" do not close ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.do_clause().now_or_never().unwrap().unwrap_err(); assert_matches!(e.cause, @@ -159,7 +160,7 @@ mod tests { #[test] fn parser_do_clause_empty_posix() { let mut lexer = Lexer::from_memory("do done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.do_clause().now_or_never().unwrap().unwrap_err(); assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptyDoClause)); @@ -193,7 +194,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(result.to_string(), ":"); @@ -205,7 +206,7 @@ mod tests { #[test] fn parser_compound_command_none() { let mut lexer = Lexer::from_memory("}", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let option = parser.compound_command().now_or_never().unwrap().unwrap(); assert_eq!(option, None); @@ -214,7 +215,7 @@ mod tests { #[test] fn parser_full_compound_command_without_redirections() { let mut lexer = Lexer::from_memory("(:)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.full_compound_command().now_or_never().unwrap(); let FullCompoundCommand { command, redirs } = result.unwrap().unwrap(); @@ -225,7 +226,7 @@ mod tests { #[test] fn parser_full_compound_command_with_redirections() { let mut lexer = Lexer::from_memory("(command) bar ;", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.full_compound_command().now_or_never().unwrap(); let FullCompoundCommand { command, redirs } = result.unwrap().unwrap(); @@ -241,7 +242,7 @@ mod tests { #[test] fn parser_full_compound_command_none() { let mut lexer = Lexer::from_memory("}", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.full_compound_command().now_or_never().unwrap(); assert_eq!(result, Ok(None)); @@ -250,10 +251,10 @@ mod tests { #[test] fn parser_short_function_definition_ok() { let mut lexer = Lexer::from_memory(" ( ) ( : ) > /dev/null ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let c = SimpleCommand { assigns: vec![], - words: vec!["foo".parse().unwrap()], + words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec![].into(), }; diff --git a/yash-syntax/src/parser/core.rs b/yash-syntax/src/parser/core.rs index a281d674..5e605b2c 100644 --- a/yash-syntax/src/parser/core.rs +++ b/yash-syntax/src/parser/core.rs @@ -29,6 +29,7 @@ use crate::alias::Glossary; use crate::parser::lex::is_blank; use crate::syntax::HereDoc; use crate::syntax::MaybeLiteral; +use crate::syntax::Word; use std::rc::Rc; /// Entire result of parsing. @@ -93,9 +94,99 @@ impl Rec { } } -/// The shell syntax parser. +/// Set of parameters for constructing a [parser](Parser). /// -/// This `struct` contains a set of data used in syntax parsing. +/// `Config` is a builder for constructing a parser. A [new](Self::new) +/// configuration starts with default settings. You can customize them by +/// calling methods that can be chained. Finally, you can create a parser by +/// providing the lexer to the [`input`](Self::input) method. +#[derive(Debug)] +#[must_use = "Config must be used to create a parser"] +pub struct Config<'a> { + /// Collection of aliases the parser applies to substitute command words + aliases: &'a dyn crate::alias::Glossary, + + /// Glossary that determines whether a command name is a declaration utility + decl_utils: &'a dyn crate::decl_util::Glossary, +} + +impl<'a> Config<'a> { + /// Creates a new configuration with default settings. + /// + /// You can also call [`Parser::config`] to create a new configuration. + pub fn new() -> Self { + Self { + aliases: &crate::alias::EmptyGlossary, + decl_utils: &crate::decl_util::PosixGlossary, + } + } + + /// Sets the glossary of aliases. + /// + /// The parser uses the glossary to look up aliases and substitute command + /// words. The default glossary is [empty](crate::alias::EmptyGlossary). + #[inline] + pub fn aliases(&mut self, aliases: &'a dyn Glossary) -> &mut Self { + self.aliases = aliases; + self + } + + /// Sets the glossary of declaration utilities. + /// + /// The parser uses the glossary to determine whether a command name is a + /// declaration utility. The default glossary is [`PosixGlossary`], which + /// recognizes the declaration utilities defined by POSIX. You can make + /// arbitrary command names declaration utilities by providing a custom + /// glossary. To meet the POSIX standard, the glossary's + /// [`is_declaration_utility`] method must return: + /// + /// - `Some(true)` for `export` and `readonly` + /// - `None` for `command` + /// + /// For detailed information on declaration utilities, see the + /// [`decl_utils`] module. + /// + /// [`decl_utils`]: crate::decl_util + /// [`PosixGlossary`]: crate::decl_util::PosixGlossary + /// [`is_declaration_utility`]: crate::decl_util::Glossary::is_declaration_utility + #[inline] + pub fn declaration_utilities( + &mut self, + decl_utils: &'a dyn crate::decl_util::Glossary, + ) -> &mut Self { + self.decl_utils = decl_utils; + self + } + + /// Creates a parser with the given lexer. + pub fn input<'b>(&self, lexer: &'a mut Lexer<'b>) -> Parser<'a, 'b> { + Parser { + lexer, + aliases: self.aliases, + decl_utils: self.decl_utils, + token: None, + unread_here_docs: Vec::new(), + } + } +} + +impl Default for Config<'_> { + fn default() -> Self { + Self::new() + } +} + +/// The shell syntax parser +/// +/// A parser manages a set of data used in syntax parsing. It keeps a reference +/// to a [lexer](Lexer) that provides tokens to parse. It also has some +/// parameters that can be set by a [configuration](Config) and affect the +/// parsing process. +/// +/// The [`new`](Self::new) function directly creates a parser with default +/// settings. If you want to customize the settings, you can use the +/// [`config`](Self::config) function to create a configuration and then create a +/// parser with the configuration. /// /// # Parsing here-documents /// @@ -111,20 +202,24 @@ impl Rec { /// Then the [`command_line`](Self::command_line) function is for you. /// See also the [module documentation](super). #[derive(Debug)] +#[must_use = "Parser must be used to parse syntax"] pub struct Parser<'a, 'b> { - /// Lexer that provides tokens. + /// Lexer that provides tokens lexer: &'a mut Lexer<'b>, - /// Collection of aliases the parser applies to substitute command words. - glossary: &'a dyn Glossary, + /// Collection of aliases the parser applies to substitute command words + aliases: &'a dyn crate::alias::Glossary, + + /// Glossary that determines whether a command name is a declaration utility + decl_utils: &'a dyn crate::decl_util::Glossary, - /// Token to parse next. + /// Token to parse next /// /// This value is an option of a result. It is `None` when the next token is not yet parsed by /// the lexer. It is `Some(Err(_))` if the lexer has failed. token: Option>, - /// Here-documents without contents. + /// Here-documents without contents /// /// The here-document is added to this list when the parser finds a /// here-document operator. After consuming the next newline token, the @@ -133,16 +228,22 @@ pub struct Parser<'a, 'b> { } impl<'a, 'b> Parser<'a, 'b> { - /// Creates a new parser based on the given lexer and glossary. + /// Creates a new configuration with default settings. /// - /// The parser uses the lexer to read tokens and the glossary to look up aliases. - pub fn new(lexer: &'a mut Lexer<'b>, glossary: &'a dyn Glossary) -> Parser<'a, 'b> { - Parser { - lexer, - glossary, - token: None, - unread_here_docs: vec![], - } + /// This is a synonym for [`Config::new`]. Customize the settings by calling + /// methods of the returned configuration and then create a parser by calling + /// its [`input`](Config::input) method. + #[inline(always)] + pub fn config() -> Config<'a> { + Config::new() + } + + /// Creates a new parser based on the given lexer. + /// + /// The parser uses the lexer to read tokens. All other settings are default. + /// To customize the settings, use the [`config`](Self::config) function. + pub fn new(lexer: &'a mut Lexer<'b>) -> Parser<'a, 'b> { + Self::config().input(lexer) } /// Reads a next token if the current token is `None`. @@ -182,11 +283,11 @@ impl<'a, 'b> Parser<'a, 'b> { /// [taken](Self::take_token_raw). fn substitute_alias(&mut self, token: Token, is_command_name: bool) -> Rec { // TODO Only POSIXly-valid alias name should be recognized in POSIXly-correct mode. - if !self.glossary.is_empty() { + if !self.aliases.is_empty() { if let Token(_) = token.id { if let Some(name) = token.word.to_string_if_literal() { if !token.word.location.code.source.is_alias_for(&name) { - if let Some(alias) = self.glossary.look_up(&name) { + if let Some(alias) = self.aliases.look_up(&name) { if is_command_name || alias.global || self.lexer.is_after_blank_ending_alias(token.index) @@ -325,6 +426,17 @@ impl<'a, 'b> Parser<'a, 'b> { }), } } + + /// Determines whether a word names a declaration utility. + /// + /// See [`decl_utils`](crate::decl_util) for more information. + pub(super) fn word_names_declaration_utility(&self, word: &Word) -> Option { + if let Some(name) = word.to_string_if_literal() { + self.decl_utils.is_declaration_utility(&name) + } else { + Some(false) + } + } } #[allow(clippy::bool_assert_comparison)] @@ -350,7 +462,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -371,7 +483,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(false).now_or_never().unwrap(); let token = result.unwrap().unwrap(); @@ -395,7 +507,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); let token = result.unwrap().unwrap(); @@ -413,7 +525,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); let token = result.unwrap().unwrap(); @@ -424,9 +536,7 @@ mod tests { #[test] fn parser_take_token_manual_no_match() { let mut lexer = Lexer::from_memory("X", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); let token = result.unwrap().unwrap(); @@ -450,7 +560,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -488,7 +598,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -522,7 +632,7 @@ mod tests { false, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -547,7 +657,7 @@ mod tests { true, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(false).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -568,7 +678,7 @@ mod tests { true, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let token = parser.take_token_auto(&[]).now_or_never().unwrap().unwrap(); assert_eq!(token.to_string(), "x"); @@ -585,7 +695,7 @@ mod tests { true, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let token = parser .take_token_auto(&[Keyword::If]) @@ -606,7 +716,7 @@ mod tests { true, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let token = parser.take_token_auto(&[]).now_or_never().unwrap().unwrap(); assert_eq!(token.to_string(), "x"); @@ -629,7 +739,7 @@ mod tests { true, Location::dummy("?"), )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let token = parser .take_token_auto(&[Keyword::If]) @@ -642,9 +752,7 @@ mod tests { #[test] fn parser_has_blank_true() { let mut lexer = Lexer::from_memory(" ", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let result = parser.has_blank().now_or_never().unwrap(); assert_eq!(result, Ok(true)); } @@ -652,9 +760,7 @@ mod tests { #[test] fn parser_has_blank_false() { let mut lexer = Lexer::from_memory("(", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let result = parser.has_blank().now_or_never().unwrap(); assert_eq!(result, Ok(false)); } @@ -662,9 +768,7 @@ mod tests { #[test] fn parser_has_blank_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let result = parser.has_blank().now_or_never().unwrap(); assert_eq!(result, Ok(false)); } @@ -672,9 +776,7 @@ mod tests { #[test] fn parser_has_blank_true_with_line_continuations() { let mut lexer = Lexer::from_memory("\\\n\\\n ", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let result = parser.has_blank().now_or_never().unwrap(); assert_eq!(result, Ok(true)); } @@ -682,9 +784,7 @@ mod tests { #[test] fn parser_has_blank_false_with_line_continuations() { let mut lexer = Lexer::from_memory("\\\n\\\n\\\n(", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let result = parser.has_blank().now_or_never().unwrap(); assert_eq!(result, Ok(false)); } @@ -693,9 +793,7 @@ mod tests { #[should_panic(expected = "There should be no pending token")] fn parser_has_blank_with_pending_token() { let mut lexer = Lexer::from_memory("foo", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); parser.peek_token().now_or_never().unwrap().unwrap(); let _ = parser.has_blank().now_or_never().unwrap(); } @@ -703,9 +801,7 @@ mod tests { #[test] fn parser_reading_no_here_doc_contents() { let mut lexer = Lexer::from_memory("X", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); parser.here_doc_contents().now_or_never().unwrap().unwrap(); let location = lexer.location().now_or_never().unwrap().unwrap(); @@ -718,9 +814,7 @@ mod tests { let delimiter = "END".parse().unwrap(); let mut lexer = Lexer::from_memory("END\nX", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let remove_tabs = false; let here_doc = Rc::new(HereDoc { delimiter, @@ -745,9 +839,7 @@ mod tests { let delimiter3 = "THREE".parse().unwrap(); let mut lexer = Lexer::from_memory("1\nONE\nTWO\n3\nTHREE\nX", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let here_doc1 = Rc::new(HereDoc { delimiter: delimiter1, remove_tabs: false, @@ -784,9 +876,7 @@ mod tests { let delimiter2 = "TWO".parse().unwrap(); let mut lexer = Lexer::from_memory("1\nONE\n2\nTWO\n", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); let here_doc1 = Rc::new(HereDoc { delimiter: delimiter1, remove_tabs: false, @@ -813,9 +903,7 @@ mod tests { #[should_panic(expected = "No token must be peeked before reading here-doc contents")] fn parser_here_doc_contents_must_be_called_without_pending_token() { let mut lexer = Lexer::from_memory("X", Source::Unknown); - #[allow(clippy::mutable_key_type)] - let aliases = AliasSet::new(); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::new(&mut lexer); parser.peek_token().now_or_never().unwrap().unwrap(); parser.here_doc_contents().now_or_never().unwrap().unwrap(); } diff --git a/yash-syntax/src/parser/for_loop.rs b/yash-syntax/src/parser/for_loop.rs index b1e440f7..08cf5c5f 100644 --- a/yash-syntax/src/parser/for_loop.rs +++ b/yash-syntax/src/parser/for_loop.rs @@ -159,7 +159,7 @@ mod tests { use super::super::error::ErrorCause; use super::super::lex::Lexer; use super::*; - use crate::alias::{AliasSet, EmptyGlossary, HashEntry}; + use crate::alias::{AliasSet, HashEntry}; use crate::source::Source; use assert_matches::assert_matches; use futures_util::FutureExt; @@ -167,7 +167,7 @@ mod tests { #[test] fn parser_for_loop_short() { let mut lexer = Lexer::from_memory("for A do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -184,7 +184,7 @@ mod tests { #[test] fn parser_for_loop_with_semicolon_before_do() { let mut lexer = Lexer::from_memory("for B ; do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -201,7 +201,7 @@ mod tests { #[test] fn parser_for_loop_with_semicolon_and_newlines_before_do() { let mut lexer = Lexer::from_memory("for B ; \n\t\n do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -218,7 +218,7 @@ mod tests { #[test] fn parser_for_loop_with_newlines_before_do() { let mut lexer = Lexer::from_memory("for B \n \\\n \n do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -235,7 +235,7 @@ mod tests { #[test] fn parser_for_loop_with_zero_values_delimited_by_semicolon() { let mut lexer = Lexer::from_memory("for foo in; do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -252,7 +252,7 @@ mod tests { #[test] fn parser_for_loop_with_one_value_delimited_by_semicolon_and_newlines() { let mut lexer = Lexer::from_memory("for foo in bar; \n \n do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -274,7 +274,7 @@ mod tests { #[test] fn parser_for_loop_with_many_values_delimited_by_one_newline() { let mut lexer = Lexer::from_memory("for in in in a b c\ndo :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -296,7 +296,7 @@ mod tests { #[test] fn parser_for_loop_with_zero_values_delimited_by_many_newlines() { let mut lexer = Lexer::from_memory("for foo in \n \n \n do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -313,7 +313,7 @@ mod tests { #[test] fn parser_for_loop_newlines_before_in() { let mut lexer = Lexer::from_memory("for foo\n \n\nin\ndo :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -345,7 +345,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -376,7 +376,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -392,7 +392,7 @@ mod tests { #[test] fn parser_for_loop_missing_name_eof() { let mut lexer = Lexer::from_memory(" for ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -406,7 +406,7 @@ mod tests { #[test] fn parser_for_loop_missing_name_newline() { let mut lexer = Lexer::from_memory(" for\ndo :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -420,7 +420,7 @@ mod tests { #[test] fn parser_for_loop_missing_name_semicolon() { let mut lexer = Lexer::from_memory("for; do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -450,7 +450,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -473,7 +473,7 @@ mod tests { #[test] fn parser_for_loop_semicolon_after_newline() { let mut lexer = Lexer::from_memory("for X\n; do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -509,7 +509,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.take_token_manual(true).now_or_never().unwrap(); assert_matches!(result, Ok(Rec::AliasSubstituted)); @@ -532,7 +532,7 @@ mod tests { #[test] fn parser_for_loop_invalid_token_after_semicolon() { let mut lexer = Lexer::from_memory(" for X; ! do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); diff --git a/yash-syntax/src/parser/from_str.rs b/yash-syntax/src/parser/from_str.rs index 7ec5aaf8..4892019b 100644 --- a/yash-syntax/src/parser/from_str.rs +++ b/yash-syntax/src/parser/from_str.rs @@ -25,7 +25,6 @@ use super::Error; use super::ErrorCause; use super::Parser; use super::SyntaxError; -use crate::alias::EmptyGlossary; use crate::source::Source; use crate::syntax::*; use std::future::Future; @@ -193,7 +192,7 @@ impl FromStr for Assign { if let Some(word) = c.words.pop() { Err(Some(Error { cause: ErrorCause::Syntax(SyntaxError::RedundantToken), - location: word.location, + location: word.0.location, })) } else if let Some(redir) = c.redirs.first() { Err(Some(Error { @@ -249,7 +248,7 @@ impl FromStr for Redir { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let redir = parser.redirection().await?; if redir.is_some() { @@ -278,7 +277,7 @@ impl FromStr for SimpleCommand { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let command = parser.simple_command().await?.unwrap(); if command.is_some() { @@ -304,7 +303,7 @@ impl FromStr for CaseItem { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let item = parser.case_item().await?.map(|(item, _)| item); if item.is_some() { @@ -331,7 +330,7 @@ impl FromStr for CompoundCommand { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let command = parser.compound_command().await?; if command.is_some() { @@ -354,7 +353,7 @@ impl FromStr for FullCompoundCommand { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let command = parser.full_compound_command().await?; if command.is_some() { @@ -377,7 +376,7 @@ impl FromStr for Command { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let command = parser.command().await?.unwrap(); if command.is_some() { @@ -400,7 +399,7 @@ impl FromStr for Pipeline { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let pipeline = parser.pipeline().await?.unwrap(); if pipeline.is_some() { @@ -432,7 +431,7 @@ impl FromStr for AndOrList { fn from_str(s: &str) -> Result> { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); unwrap_ready(async { let list = parser.and_or_list().await?.unwrap(); if list.is_some() { @@ -450,7 +449,7 @@ impl FromStr for List { type Err = Error; fn from_str(s: &str) -> Result { let mut lexer = Lexer::from_memory(s, Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let list = unwrap_ready(parser.maybe_compound_list())?; parser.ensure_no_unread_here_doc()?; Ok(list) diff --git a/yash-syntax/src/parser/function.rs b/yash-syntax/src/parser/function.rs index 1b8d7f1a..020acd8c 100644 --- a/yash-syntax/src/parser/function.rs +++ b/yash-syntax/src/parser/function.rs @@ -54,7 +54,7 @@ impl Parser<'_, '_> { }); } - let name = intro.words.pop().unwrap(); + let name = intro.words.pop().unwrap().0; debug_assert!(intro.is_empty()); // TODO reject invalid name if POSIXly-correct @@ -92,16 +92,17 @@ mod tests { use super::super::lex::Lexer; use super::super::lex::TokenId::EndOfInput; use super::*; - use crate::alias::{AliasSet, EmptyGlossary, HashEntry}; + use crate::alias::{AliasSet, HashEntry}; use crate::source::Location; use crate::source::Source; + use crate::syntax::ExpansionMode; use assert_matches::assert_matches; use futures_util::FutureExt; #[test] fn parser_short_function_definition_not_one_word_name() { let mut lexer = Lexer::from_memory("(", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let c = SimpleCommand { assigns: vec![], words: vec![], @@ -121,10 +122,10 @@ mod tests { #[test] fn parser_short_function_definition_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let c = SimpleCommand { assigns: vec![], - words: vec!["foo".parse().unwrap()], + words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec![].into(), }; @@ -138,10 +139,10 @@ mod tests { #[test] fn parser_short_function_definition_unmatched_parenthesis() { let mut lexer = Lexer::from_memory("( ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let c = SimpleCommand { assigns: vec![], - words: vec!["foo".parse().unwrap()], + words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec![].into(), }; @@ -160,10 +161,10 @@ mod tests { #[test] fn parser_short_function_definition_missing_function_body() { let mut lexer = Lexer::from_memory("( ) ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let c = SimpleCommand { assigns: vec![], - words: vec!["foo".parse().unwrap()], + words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec![].into(), }; @@ -182,10 +183,10 @@ mod tests { #[test] fn parser_short_function_definition_invalid_function_body() { let mut lexer = Lexer::from_memory("() foo ; ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let c = SimpleCommand { assigns: vec![], - words: vec!["foo".parse().unwrap()], + words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec![].into(), }; @@ -225,7 +226,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); parser.simple_command().now_or_never().unwrap().unwrap(); // alias let sc = parser.simple_command().now_or_never().unwrap(); @@ -266,7 +267,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); parser.simple_command().now_or_never().unwrap().unwrap(); // alias let sc = parser.simple_command().now_or_never().unwrap(); @@ -301,10 +302,10 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let c = SimpleCommand { assigns: vec![], - words: vec!["f".parse().unwrap()], + words: vec![("f".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec![].into(), }; diff --git a/yash-syntax/src/parser/grouping.rs b/yash-syntax/src/parser/grouping.rs index 91c3fe6a..f5ffb40f 100644 --- a/yash-syntax/src/parser/grouping.rs +++ b/yash-syntax/src/parser/grouping.rs @@ -98,7 +98,7 @@ mod tests { use super::super::error::ErrorCause; use super::super::lex::Lexer; use super::*; - use crate::alias::{AliasSet, EmptyGlossary, HashEntry}; + use crate::alias::{AliasSet, HashEntry}; use crate::source::Location; use crate::source::Source; use assert_matches::assert_matches; @@ -107,7 +107,7 @@ mod tests { #[test] fn parser_grouping_short() { let mut lexer = Lexer::from_memory("{ :; }", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -119,7 +119,7 @@ mod tests { #[test] fn parser_grouping_long() { let mut lexer = Lexer::from_memory("{ foo; bar& }", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -131,7 +131,7 @@ mod tests { #[test] fn parser_grouping_unclosed() { let mut lexer = Lexer::from_memory(" { oh no ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -151,7 +151,7 @@ mod tests { #[test] fn parser_grouping_empty_posix() { let mut lexer = Lexer::from_memory("{ }", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -186,7 +186,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -198,7 +198,7 @@ mod tests { #[test] fn parser_subshell_short() { let mut lexer = Lexer::from_memory("(:)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -214,7 +214,7 @@ mod tests { #[test] fn parser_subshell_long() { let mut lexer = Lexer::from_memory("( foo& bar; )", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -230,7 +230,7 @@ mod tests { #[test] fn parser_subshell_unclosed() { let mut lexer = Lexer::from_memory(" ( oh no", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -250,7 +250,7 @@ mod tests { #[test] fn parser_subshell_empty_posix() { let mut lexer = Lexer::from_memory("( )", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); diff --git a/yash-syntax/src/parser/if.rs b/yash-syntax/src/parser/if.rs index 9cbf7428..25826a4f 100644 --- a/yash-syntax/src/parser/if.rs +++ b/yash-syntax/src/parser/if.rs @@ -140,7 +140,6 @@ mod tests { use super::super::lex::Lexer; use super::super::lex::TokenId::EndOfInput; use super::*; - use crate::alias::EmptyGlossary; use crate::source::Source; use assert_matches::assert_matches; use futures_util::FutureExt; @@ -148,7 +147,7 @@ mod tests { #[test] fn parser_if_command_minimum() { let mut lexer = Lexer::from_memory("if a; then b; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -169,7 +168,7 @@ mod tests { "if\ntrue\nthen\nfalse\n\nelif x; then y& fi", Source::Unknown, ); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -191,7 +190,7 @@ mod tests { "if a; then b; elif c; then d; elif e 1; e 2& then f 1; f 2& elif g; then h; fi", Source::Unknown, ); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -212,7 +211,7 @@ mod tests { #[test] fn parser_if_command_else() { let mut lexer = Lexer::from_memory("if a; then b; else c; d; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -231,7 +230,7 @@ mod tests { fn parser_if_command_elif_and_else() { let mut lexer = Lexer::from_memory("if 1; then 2; elif 3; then 4; else 5; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -250,7 +249,7 @@ mod tests { #[test] fn parser_if_command_without_then_after_if() { let mut lexer = Lexer::from_memory(" if :; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -269,7 +268,7 @@ mod tests { #[test] fn parser_if_command_without_then_after_elif() { let mut lexer = Lexer::from_memory("if a; then b; elif c; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -289,7 +288,7 @@ mod tests { #[test] fn parser_if_command_without_fi() { let mut lexer = Lexer::from_memory(" if :; then :; }", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -309,7 +308,7 @@ mod tests { #[test] fn parser_if_command_empty_condition() { let mut lexer = Lexer::from_memory(" if then :; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -323,7 +322,7 @@ mod tests { #[test] fn parser_if_command_empty_body() { let mut lexer = Lexer::from_memory("if :; then fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -337,7 +336,7 @@ mod tests { #[test] fn parser_if_command_empty_elif_condition() { let mut lexer = Lexer::from_memory("if :; then :; elif then :; fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -354,7 +353,7 @@ mod tests { #[test] fn parser_if_command_empty_elif_body() { let mut lexer = Lexer::from_memory("if :; then :; elif :; then fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -371,7 +370,7 @@ mod tests { #[test] fn parser_if_command_empty_else() { let mut lexer = Lexer::from_memory("if :; then :; else fi", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); diff --git a/yash-syntax/src/parser/lex/core.rs b/yash-syntax/src/parser/lex/core.rs index f858c639..74af9da6 100644 --- a/yash-syntax/src/parser/lex/core.rs +++ b/yash-syntax/src/parser/lex/core.rs @@ -19,7 +19,6 @@ use super::keyword::Keyword; use super::op::Operator; use crate::alias::Alias; -use crate::alias::EmptyGlossary; use crate::input::Context; use crate::input::InputObject; use crate::input::Memory; @@ -751,7 +750,7 @@ impl<'a> Lexer<'a> { pub async fn inner_program(&mut self) -> Result { let begin = self.index(); - let mut parser = super::super::Parser::new(self, &EmptyGlossary); + let mut parser = super::super::Parser::new(self); parser.maybe_compound_list().await?; let end = parser.peek_token().await?.index; diff --git a/yash-syntax/src/parser/lex/tilde.rs b/yash-syntax/src/parser/lex/tilde.rs index 4380e705..453a89ea 100644 --- a/yash-syntax/src/parser/lex/tilde.rs +++ b/yash-syntax/src/parser/lex/tilde.rs @@ -145,9 +145,49 @@ impl Word { /// ] /// ); /// ``` + /// + /// See also + /// [`parse_tilde_everywhere_after`](Self::parse_tilde_everywhere_after), + /// which allows you to parse tilde expansions only after a specified index. #[inline] pub fn parse_tilde_everywhere(&mut self) { - let mut i = 0; + self.parse_tilde_everywhere_after(0); + } + + /// Parses tilde expansions in the word after the specified index. + /// + /// This function works the same as + /// [`parse_tilde_everywhere`](Self::parse_tilde_everywhere) except that it + /// starts parsing tilde expansions after the specified index of + /// `self.units`. Tilde expansions are parsed at the specified index and + /// after each unquoted colon. + /// + /// ``` + /// # use std::str::FromStr; + /// # use yash_syntax::syntax::{TextUnit::Literal, Word, WordUnit::{Tilde, Unquoted}}; + /// let mut word = Word::from_str("~=~a/b:~c").unwrap(); + /// word.parse_tilde_everywhere_after(2); + /// assert_eq!( + /// word.units, + /// [ + /// // The initial tilde is not parsed because it is before index 2. + /// Unquoted(Literal('~')), + /// Unquoted(Literal('=')), + /// // This tilde is parsed because it is at index 2, + /// // even though it is not after a colon. + /// Tilde("a".to_string()), + /// Unquoted(Literal('/')), + /// Unquoted(Literal('b')), + /// Unquoted(Literal(':')), + /// Tilde("c".to_string()), + /// ] + /// ); + /// ``` + /// + /// Compare [`parse_tilde_everywhere`](Self::parse_tilde_everywhere), which + /// is equivalent to `parse_tilde_everywhere_after(0)`. + pub fn parse_tilde_everywhere_after(&mut self, index: usize) { + let mut i = index; loop { // Parse a tilde expansion at index `i`. if let Some((len, name)) = parse_tilde(&self.units[i..], true) { diff --git a/yash-syntax/src/parser/list.rs b/yash-syntax/src/parser/list.rs index 035e7642..030de057 100644 --- a/yash-syntax/src/parser/list.rs +++ b/yash-syntax/src/parser/list.rs @@ -207,7 +207,6 @@ mod tests { use super::super::error::ErrorCause; use super::super::lex::Lexer; use super::*; - use crate::alias::EmptyGlossary; use crate::source::Source; use crate::syntax::AndOrList; use crate::syntax::Command; @@ -219,7 +218,7 @@ mod tests { #[test] fn parser_list_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let list = parser.list().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(list.0, vec![]); @@ -228,7 +227,7 @@ mod tests { #[test] fn parser_list_one_item_without_last_semicolon() { let mut lexer = Lexer::from_memory("foo", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let list = parser.list().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(list.0.len(), 1); @@ -239,7 +238,7 @@ mod tests { #[test] fn parser_list_one_item_with_last_semicolon() { let mut lexer = Lexer::from_memory("foo;", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let list = parser.list().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(list.0.len(), 1); @@ -250,7 +249,7 @@ mod tests { #[test] fn parser_list_many_items() { let mut lexer = Lexer::from_memory("foo & bar ; baz&", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let list = parser.list().now_or_never().unwrap().unwrap().unwrap(); assert_eq!(list.0.len(), 3); @@ -276,7 +275,7 @@ mod tests { #[test] fn parser_command_line_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.command_line().now_or_never().unwrap().unwrap(); assert!(result.is_none()); @@ -285,7 +284,7 @@ mod tests { #[test] fn parser_command_line_command_and_newline() { let mut lexer = Lexer::from_memory("< /dev/null\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -180,7 +179,7 @@ mod tests { #[test] fn parser_redirection_greater() { let mut lexer = Lexer::from_memory(">/dev/null\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -194,7 +193,7 @@ mod tests { #[test] fn parser_redirection_greater_greater() { let mut lexer = Lexer::from_memory(" >> /dev/null\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -208,7 +207,7 @@ mod tests { #[test] fn parser_redirection_greater_bar() { let mut lexer = Lexer::from_memory(">| /dev/null\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -222,7 +221,7 @@ mod tests { #[test] fn parser_redirection_less_and() { let mut lexer = Lexer::from_memory("<& -\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -236,7 +235,7 @@ mod tests { #[test] fn parser_redirection_greater_and() { let mut lexer = Lexer::from_memory(">& 3\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -250,7 +249,7 @@ mod tests { #[test] fn parser_redirection_greater_greater_bar() { let mut lexer = Lexer::from_memory(">>| 3\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -264,7 +263,7 @@ mod tests { #[test] fn parser_redirection_less_less_less() { let mut lexer = Lexer::from_memory("<<< foo\n", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.redirection().now_or_never().unwrap(); let redir = result.unwrap().unwrap(); @@ -278,7 +277,7 @@ mod tests { #[test] fn parser_redirection_less_less() { let mut lexer = Lexer::from_memory("<", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.redirection().now_or_never().unwrap().unwrap_err(); assert_eq!( @@ -379,7 +378,7 @@ mod tests { #[test] fn parser_redirection_eof_operand() { let mut lexer = Lexer::from_memory(" < ", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.redirection().now_or_never().unwrap().unwrap_err(); assert_eq!( @@ -395,7 +394,7 @@ mod tests { #[test] fn parser_redirection_not_heredoc_delimiter() { let mut lexer = Lexer::from_memory("<< <<", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.redirection().now_or_never().unwrap().unwrap_err(); assert_eq!( @@ -411,7 +410,7 @@ mod tests { #[test] fn parser_redirection_eof_heredoc_delimiter() { let mut lexer = Lexer::from_memory("<<", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.redirection().now_or_never().unwrap().unwrap_err(); assert_eq!( diff --git a/yash-syntax/src/parser/simple_command.rs b/yash-syntax/src/parser/simple_command.rs index e19a3b3d..6a6cc9fd 100644 --- a/yash-syntax/src/parser/simple_command.rs +++ b/yash-syntax/src/parser/simple_command.rs @@ -25,16 +25,41 @@ use super::lex::Operator::{CloseParen, Newline, OpenParen}; use super::lex::TokenId::{Operator, Token}; use crate::syntax::Array; use crate::syntax::Assign; +use crate::syntax::ExpansionMode; +use crate::syntax::MaybeLiteral as _; use crate::syntax::Redir; use crate::syntax::Scalar; use crate::syntax::SimpleCommand; use crate::syntax::Word; +/// Determines the expansion mode of a word. +/// +/// This function converts a raw token into a word-mode pair assuming that the +/// token is a command argument word for a declaration utility. +/// +/// This function tests if the word is in the form of `name=value`. If it is, +/// the expansion mode is `ExpansionMode::Single`, and tilde expansions are +/// parsed after the equal sign. Otherwise, the expansion mode is +/// `ExpansionMode::Multiple`, and the word is returned as is. +fn determine_expansion_mode(word: Word) -> (Word, ExpansionMode) { + use crate::syntax::{TextUnit::Literal, WordUnit::Unquoted}; + if let Some(eq) = word.units.iter().position(|u| *u == Unquoted(Literal('='))) { + if let Some(name) = word.units[..eq].to_string_if_literal() { + if !name.is_empty() { + let mut word = word; + word.parse_tilde_everywhere_after(eq + 1); + return (word, ExpansionMode::Single); + } + } + } + (word, ExpansionMode::Multiple) +} + /// Simple command builder. #[derive(Default)] struct Builder { assigns: Vec, - words: Vec, + words: Vec<(Word, ExpansionMode)>, redirs: Vec, } @@ -93,6 +118,7 @@ impl Parser<'_, '_> { /// If there is no valid command at the current position, this function /// returns `Ok(Rec::Parsed(None))`. pub async fn simple_command(&mut self) -> Result>> { + let mut is_declaration_utility = None; let mut result = Builder::default(); loop { @@ -121,15 +147,34 @@ impl Parser<'_, '_> { Rec::Parsed(token) => token, }; - // Tell assignment from word - if !result.words.is_empty() { - result.words.push(token.word); + // Handle command argument word + if let Some(is_declaration_utility) = is_declaration_utility { + // The word determined (not) to be a declaration utility + // must already be in the words list. + debug_assert!(!result.words.is_empty()); + + result.words.push(if is_declaration_utility { + determine_expansion_mode(token.word) + } else { + (token.word, ExpansionMode::Multiple) + }); continue; } - let mut assign = match Assign::try_from(token.word) { + + // Tell assignment from word + let assign_or_word = if result.words.is_empty() { + // We don't have any words yet, so this token may be an assignment or a word. + Assign::try_from(token.word) + } else { + // We already have some words, so remaining tokens are all words. + Err(token.word) + }; + let mut assign = match assign_or_word { Ok(assign) => assign, Err(word) => { - result.words.push(word); + debug_assert!(is_declaration_utility.is_none()); + is_declaration_utility = self.word_names_declaration_utility(&word); + result.words.push((word, ExpansionMode::Multiple)); continue; } }; @@ -153,11 +198,7 @@ impl Parser<'_, '_> { result.assigns.push(assign); } - Ok(Rec::Parsed(if result.is_empty() { - None - } else { - Some(result.into()) - })) + Ok(Rec::Parsed((!result.is_empty()).then(|| result.into()))) } } @@ -167,17 +208,60 @@ mod tests { use super::super::lex::Lexer; use super::super::lex::TokenId::EndOfInput; use super::*; - use crate::alias::EmptyGlossary; + use crate::decl_util::EmptyGlossary; use crate::source::Source; use crate::syntax::RedirBody; use crate::syntax::RedirOp; + use crate::syntax::TextUnit; + use crate::syntax::WordUnit; use assert_matches::assert_matches; - use futures_util::FutureExt; + use futures_util::FutureExt as _; + + #[test] + fn determine_expansion_mode_empty_name() { + let in_word = "=".parse::().unwrap(); + let (out_word, mode) = determine_expansion_mode(in_word.clone()); + assert_eq!(out_word, in_word); + assert_eq!(mode, ExpansionMode::Multiple); + } + + #[test] + fn determine_expansion_mode_nonempty_name() { + let in_word = "foo=".parse::().unwrap(); + let (out_word, mode) = determine_expansion_mode(in_word.clone()); + assert_eq!(out_word, in_word); + assert_eq!(mode, ExpansionMode::Single); + } + + #[test] + fn determine_expansion_mode_non_literal_name() { + let in_word = "${X}=".parse::().unwrap(); + let (out_word, mode) = determine_expansion_mode(in_word.clone()); + assert_eq!(out_word, in_word); + assert_eq!(mode, ExpansionMode::Multiple); + } + + #[test] + fn determine_expansion_mode_tilde_expansions_after_equal() { + let word = "~=~:~b".parse().unwrap(); + let (word, mode) = determine_expansion_mode(word); + assert_eq!( + word.units, + [ + WordUnit::Unquoted(TextUnit::Literal('~')), + WordUnit::Unquoted(TextUnit::Literal('=')), + WordUnit::Tilde("".to_string()), + WordUnit::Unquoted(TextUnit::Literal(':')), + WordUnit::Tilde("b".to_string()), + ] + ); + assert_eq!(mode, ExpansionMode::Single); + } #[test] fn parser_array_values_no_open_parenthesis() { let mut lexer = Lexer::from_memory(")", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.array_values().now_or_never().unwrap().unwrap(); assert_eq!(result, None); } @@ -185,7 +269,7 @@ mod tests { #[test] fn parser_array_values_empty() { let mut lexer = Lexer::from_memory("()", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.array_values().now_or_never().unwrap(); let words = result.unwrap().unwrap(); assert_eq!(words, []); @@ -197,7 +281,7 @@ mod tests { #[test] fn parser_array_values_many() { let mut lexer = Lexer::from_memory("(a b c)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.array_values().now_or_never().unwrap(); let words = result.unwrap().unwrap(); assert_eq!(words.len(), 3); @@ -215,7 +299,7 @@ mod tests { )", Source::Unknown, ); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.array_values().now_or_never().unwrap(); let words = result.unwrap().unwrap(); assert_eq!(words.len(), 3); @@ -227,7 +311,7 @@ mod tests { #[test] fn parser_array_values_unclosed() { let mut lexer = Lexer::from_memory("(a b", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.array_values().now_or_never().unwrap().unwrap_err(); assert_matches!(e.cause, ErrorCause::Syntax(SyntaxError::UnclosedArrayValue { opening_location }) => { @@ -245,7 +329,7 @@ mod tests { #[test] fn parser_array_values_invalid_word() { let mut lexer = Lexer::from_memory("(a;b)", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let e = parser.array_values().now_or_never().unwrap().unwrap_err(); assert_matches!(e.cause, ErrorCause::Syntax(SyntaxError::UnclosedArrayValue { opening_location }) => { @@ -263,7 +347,7 @@ mod tests { #[test] fn parser_simple_command_eof() { let mut lexer = Lexer::from_memory("", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); assert_eq!(result, Ok(Rec::Parsed(None))); @@ -272,7 +356,7 @@ mod tests { #[test] fn parser_simple_command_keyword() { let mut lexer = Lexer::from_memory("then", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); assert_eq!(result, Ok(Rec::Parsed(None))); @@ -281,7 +365,7 @@ mod tests { #[test] fn parser_simple_command_one_assignment() { let mut lexer = Lexer::from_memory("my=assignment", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -299,7 +383,7 @@ mod tests { #[test] fn parser_simple_command_many_assignments() { let mut lexer = Lexer::from_memory("a= b=! c=X", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -329,35 +413,39 @@ mod tests { #[test] fn parser_simple_command_one_word() { let mut lexer = Lexer::from_memory("word", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); assert_eq!(sc.assigns, []); assert_eq!(*sc.redirs, []); assert_eq!(sc.words.len(), 1); - assert_eq!(sc.words[0].to_string(), "word"); + assert_eq!(sc.words[0].0.to_string(), "word"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); } #[test] fn parser_simple_command_many_words() { let mut lexer = Lexer::from_memory(": if then", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); assert_eq!(sc.assigns, []); assert_eq!(*sc.redirs, []); assert_eq!(sc.words.len(), 3); - assert_eq!(sc.words[0].to_string(), ":"); - assert_eq!(sc.words[1].to_string(), "if"); - assert_eq!(sc.words[2].to_string(), "then"); + assert_eq!(sc.words[0].0.to_string(), ":"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); + assert_eq!(sc.words[1].0.to_string(), "if"); + assert_eq!(sc.words[1].1, ExpansionMode::Multiple); + assert_eq!(sc.words[2].0.to_string(), "then"); + assert_eq!(sc.words[2].1, ExpansionMode::Multiple); } #[test] fn parser_simple_command_one_redirection() { let mut lexer = Lexer::from_memory("two >>three", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -407,7 +495,7 @@ mod tests { #[test] fn parser_simple_command_assignment_word() { let mut lexer = Lexer::from_memory("if=then else", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -416,20 +504,22 @@ mod tests { assert_eq!(sc.words.len(), 1); assert_eq!(sc.assigns[0].name, "if"); assert_eq!(sc.assigns[0].value.to_string(), "then"); - assert_eq!(sc.words[0].to_string(), "else"); + assert_eq!(sc.words[0].0.to_string(), "else"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); } #[test] fn parser_simple_command_word_redirection() { let mut lexer = Lexer::from_memory("word { assert_eq!(operator, &RedirOp::FileIn); @@ -440,7 +530,7 @@ mod tests { #[test] fn parser_simple_command_redirection_assignment() { let mut lexer = Lexer::from_memory(" { assert_eq!(operator, &RedirOp::FileIn); @@ -479,7 +570,7 @@ mod tests { #[test] fn parser_simple_command_array_assignment() { let mut lexer = Lexer::from_memory("a=()", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -498,7 +589,7 @@ mod tests { #[test] fn parser_simple_command_empty_assignment_followed_by_blank_and_parenthesis() { let mut lexer = Lexer::from_memory("a= ()", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -519,7 +610,7 @@ mod tests { #[test] fn parser_simple_command_non_empty_assignment_followed_by_parenthesis() { let mut lexer = Lexer::from_memory("a=b()", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.simple_command().now_or_never().unwrap(); let sc = result.unwrap().unwrap().unwrap(); @@ -536,4 +627,145 @@ mod tests { let next = parser.peek_token().now_or_never().unwrap().unwrap(); assert_eq!(next.id, Operator(OpenParen)); } + + #[test] + fn word_with_single_expansion_mode_in_declaration_utility() { + // "export" is a declaration utility, so the expansion mode of the word + // "a=b" should be single. + let mut lexer = Lexer::from_memory("export a=b", Source::Unknown); + let mut parser = Parser::new(&mut lexer); + + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.assigns, []); + assert_eq!(sc.words.len(), 2); + assert_eq!(*sc.redirs, []); + assert_eq!(sc.words[0].0.to_string(), "export"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); + assert_eq!(sc.words[1].0.to_string(), "a=b"); + assert_eq!(sc.words[1].1, ExpansionMode::Single); + } + + #[test] + fn word_with_multiple_expansion_mode_in_declaration_utility() { + // The expansion mode of the word "foo" should be multiple because it + // cannot be parsed as an assignment. + let mut lexer = Lexer::from_memory("export foo", Source::Unknown); + let mut parser = Parser::new(&mut lexer); + + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.assigns, []); + assert_eq!(sc.words.len(), 2); + assert_eq!(*sc.redirs, []); + assert_eq!(sc.words[0].0.to_string(), "export"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); + assert_eq!(sc.words[1].0.to_string(), "foo"); + assert_eq!(sc.words[1].1, ExpansionMode::Multiple); + } + + #[test] + fn word_with_multiple_expansion_mode_in_non_declaration_utility() { + // "foo" is not a declaration utility, so the expansion mode of the word + // "a=b" should be multiple. + let mut lexer = Lexer::from_memory("foo a=b", Source::Unknown); + let mut parser = Parser::new(&mut lexer); + + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.assigns, []); + assert_eq!(sc.words.len(), 2); + assert_eq!(*sc.redirs, []); + assert_eq!(sc.words[0].0.to_string(), "foo"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); + assert_eq!(sc.words[1].0.to_string(), "a=b"); + assert_eq!(sc.words[1].1, ExpansionMode::Multiple); + } + + #[test] + fn declaration_utility_determined_by_non_first_word() { + // "command" delegates to the next word to determine whether it is a + // declaration utility. + let mut lexer = Lexer::from_memory("command command export foo a=b", Source::Unknown); + let mut parser = Parser::new(&mut lexer); + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.words[4].0.to_string(), "a=b"); + assert_eq!(sc.words[4].1, ExpansionMode::Single); + + let mut lexer = Lexer::from_memory("command command foo export a=b", Source::Unknown); + let mut parser = Parser::new(&mut lexer); + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.words[4].0.to_string(), "a=b"); + assert_eq!(sc.words[4].1, ExpansionMode::Multiple); + } + + #[test] + fn no_declaration_utilities_with_empty_glossary() { + // "export" is not a declaration utility in the empty glossary. + let mut lexer = Lexer::from_memory("export a=b", Source::Unknown); + let mut parser = Parser::config() + .declaration_utilities(&EmptyGlossary) + .input(&mut lexer); + + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.assigns, []); + assert_eq!(sc.words.len(), 2); + assert_eq!(*sc.redirs, []); + assert_eq!(sc.words[0].0.to_string(), "export"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); + assert_eq!(sc.words[1].0.to_string(), "a=b"); + assert_eq!(sc.words[1].1, ExpansionMode::Multiple); + } + + #[test] + fn custom_declaration_utility_glossary() { + // "foo" is a declaration utility in the custom glossary. + #[derive(Debug)] + struct CustomGlossary; + impl crate::decl_util::Glossary for CustomGlossary { + fn is_declaration_utility(&self, name: &str) -> Option { + Some(name == "foo") + } + } + + let mut lexer = Lexer::from_memory("foo a=b", Source::Unknown); + let mut parser = Parser::config() + .declaration_utilities(&CustomGlossary) + .input(&mut lexer); + + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.assigns, []); + assert_eq!(sc.words.len(), 2); + assert_eq!(*sc.redirs, []); + assert_eq!(sc.words[0].0.to_string(), "foo"); + assert_eq!(sc.words[0].1, ExpansionMode::Multiple); + assert_eq!(sc.words[1].0.to_string(), "a=b"); + assert_eq!(sc.words[1].1, ExpansionMode::Single); + } + + #[test] + fn assignment_is_not_considered_for_declaration_utility() { + #[derive(Debug)] + struct CustomGlossary; + impl crate::decl_util::Glossary for CustomGlossary { + fn is_declaration_utility(&self, _name: &str) -> Option { + unreachable!("is_declaration_utility should not be called for assignments"); + } + } + + let mut lexer = Lexer::from_memory("a=b", Source::Unknown); + let mut parser = Parser::config() + .declaration_utilities(&CustomGlossary) + .input(&mut lexer); + + let result = parser.simple_command().now_or_never().unwrap(); + let sc = result.unwrap().unwrap().unwrap(); + assert_eq!(sc.assigns.len(), 1); + assert_eq!(sc.words, []); + assert_eq!(*sc.redirs, []) + } } diff --git a/yash-syntax/src/parser/while_loop.rs b/yash-syntax/src/parser/while_loop.rs index b066c623..1a51958d 100644 --- a/yash-syntax/src/parser/while_loop.rs +++ b/yash-syntax/src/parser/while_loop.rs @@ -98,7 +98,7 @@ mod tests { use super::super::lex::Lexer; use super::super::lex::TokenId::EndOfInput; use super::*; - use crate::alias::{AliasSet, EmptyGlossary, HashEntry}; + use crate::alias::{AliasSet, HashEntry}; use crate::source::Location; use crate::source::Source; use assert_matches::assert_matches; @@ -107,7 +107,7 @@ mod tests { #[test] fn parser_while_loop_short() { let mut lexer = Lexer::from_memory("while true; do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -123,7 +123,7 @@ mod tests { #[test] fn parser_while_loop_long() { let mut lexer = Lexer::from_memory("while false; true& do foo; bar& done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -139,7 +139,7 @@ mod tests { #[test] fn parser_while_loop_unclosed() { let mut lexer = Lexer::from_memory("while :", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -159,7 +159,7 @@ mod tests { #[test] fn parser_while_loop_empty_posix() { let mut lexer = Lexer::from_memory(" while do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -191,7 +191,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -204,7 +204,7 @@ mod tests { #[test] fn parser_until_loop_short() { let mut lexer = Lexer::from_memory("until true; do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -220,7 +220,7 @@ mod tests { #[test] fn parser_until_loop_long() { let mut lexer = Lexer::from_memory("until false; true& do foo; bar& done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); @@ -236,7 +236,7 @@ mod tests { #[test] fn parser_until_loop_unclosed() { let mut lexer = Lexer::from_memory("until :", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -256,7 +256,7 @@ mod tests { #[test] fn parser_until_loop_empty_posix() { let mut lexer = Lexer::from_memory(" until do :; done", Source::Unknown); - let mut parser = Parser::new(&mut lexer, &EmptyGlossary); + let mut parser = Parser::new(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let e = result.unwrap_err(); @@ -288,7 +288,7 @@ mod tests { false, origin, )); - let mut parser = Parser::new(&mut lexer, &aliases); + let mut parser = Parser::config().aliases(&aliases).input(&mut lexer); let result = parser.compound_command().now_or_never().unwrap(); let compound_command = result.unwrap().unwrap(); diff --git a/yash-syntax/src/syntax.rs b/yash-syntax/src/syntax.rs index 8ca27b5f..9114695e 100644 --- a/yash-syntax/src/syntax.rs +++ b/yash-syntax/src/syntax.rs @@ -581,14 +581,31 @@ impl Redir { } } +/// Expansion style of a simple command word +/// +/// This enum specifies how a [`Word`] in a [`SimpleCommand`] should be expanded +/// at runtime. The expansion mode is determined by whether the command name is +/// a declaration utility and whether the word is in the form of an assignment. +/// See the [`decl_util` module](crate::decl_util) for details. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ExpansionMode { + /// Expand the word to a single field + Single, + /// Expand the word to multiple fields + Multiple, +} + /// Command that involves assignments, redirections, and word expansions /// /// In the shell language syntax, a valid simple command must contain at least one of assignments, /// redirections, and words. The parser must not produce a completely empty simple command. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SimpleCommand { + /// Assignments pub assigns: Vec, - pub words: Vec, + /// Command name and arguments + pub words: Vec<(Word, ExpansionMode)>, + /// Redirections pub redirs: Rc>, } @@ -607,7 +624,7 @@ impl SimpleCommand { /// Tests whether the first word of the simple command is a keyword. #[must_use] fn first_word_is_keyword(&self) -> bool { - let Some(word) = self.words.first() else { + let Some((word, _)) = self.words.first() else { return false; }; let Some(literal) = word.to_string_if_literal() else { diff --git a/yash-syntax/src/syntax/impl_display.rs b/yash-syntax/src/syntax/impl_display.rs index c8477efc..ca7c1656 100644 --- a/yash-syntax/src/syntax/impl_display.rs +++ b/yash-syntax/src/syntax/impl_display.rs @@ -237,7 +237,7 @@ impl fmt::Display for Redir { impl fmt::Display for SimpleCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let i1 = self.assigns.iter().map(|x| x as &dyn fmt::Display); - let i2 = self.words.iter().map(|x| x as &dyn fmt::Display); + let i2 = self.words.iter().map(|x| &x.0 as &dyn fmt::Display); let i3 = self.redirs.iter().map(|x| x as &dyn fmt::Display); if !self.assigns.is_empty() || !self.first_word_is_keyword() { @@ -742,10 +742,14 @@ mod tests { .push(Assign::from_str("hello=world").unwrap()); assert_eq!(command.to_string(), "name=value hello=world"); - command.words.push(Word::from_str("echo").unwrap()); + command + .words + .push((Word::from_str("echo").unwrap(), ExpansionMode::Multiple)); assert_eq!(command.to_string(), "name=value hello=world echo"); - command.words.push(Word::from_str("foo").unwrap()); + command + .words + .push((Word::from_str("foo").unwrap(), ExpansionMode::Single)); assert_eq!(command.to_string(), "name=value hello=world echo foo"); Rc::make_mut(&mut command.redirs).push(Redir { @@ -782,7 +786,7 @@ mod tests { fn simple_command_display_with_keyword() { let command = SimpleCommand { assigns: vec![], - words: vec!["if".parse().unwrap()], + words: vec![("if".parse().unwrap(), ExpansionMode::Multiple)], redirs: vec!["