├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appveyor.yml ├── ci ├── coverage.sh └── install-kcov.sh ├── codecov.yml ├── examples ├── analysis.rs └── parsing_input.rs ├── rustfmt.toml ├── src ├── ast.rs ├── ast │ ├── builder.rs │ └── builder │ │ ├── default_builder.rs │ │ └── empty_builder.rs ├── lexer.rs ├── lib.rs ├── parse.rs ├── parse │ └── iter.rs └── token.rs └── tests ├── and_or.rs ├── arithmetic.rs ├── backticked.rs ├── brace.rs ├── case.rs ├── command.rs ├── compound_command.rs ├── for.rs ├── function.rs ├── heredoc.rs ├── if.rs ├── lexer.rs ├── loop.rs ├── parameter.rs ├── parse.rs ├── parse_support.rs ├── pipeline.rs ├── positional.rs ├── redirect.rs ├── simple_command.rs ├── subshell.rs ├── subst.rs └── word.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | .DS_Store 5 | *.swp 6 | .lvimrc 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | #cache: cargo 4 | 5 | # necessary for kcov 6 | addons: 7 | apt: 8 | packages: 9 | - libcurl4-openssl-dev 10 | - libelf-dev 11 | - libdw-dev 12 | - binutils-dev # required for the --verify flag of kcov 13 | - libiberty-dev 14 | 15 | matrix: 16 | include: 17 | - rust: nightly 18 | sudo: required # Work around for travis-ci/travis-ci#9061 19 | #cache: false 20 | env: 21 | - FEATURE_FLAGS="--features nightly" 22 | - RUSTFLAGS="-C link-dead-code" # Enable better code coverage at the cost of binary size 23 | after_success: 24 | - ./ci/install-kcov.sh && ./ci/coverage.sh 25 | 26 | - rust: beta 27 | - rust: stable 28 | - os: osx 29 | rust: stable 30 | 31 | branches: 32 | only: 33 | - master 34 | - /v?\d(\.\d)*/ 35 | 36 | before_script: 37 | - export PATH=$PATH:~/.cargo/bin 38 | 39 | script: 40 | - cargo check $FEATURE_FLAGS 41 | - cargo test $FEATURE_FLAGS 42 | 43 | notifications: 44 | email: 45 | on_success: never 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.1.1] - 2019-05-14 8 | ### Fixed 9 | - Fix building tests by increasing the recursion limit 10 | 11 | ## 0.1.0 12 | - First release 13 | 14 | [0.1.1]: https://github.com/ipetkov/conch-parser/compare/v0.1.0...v0.1.1 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "conch-parser" 3 | version = "0.1.1" 4 | edition = "2018" 5 | authors = ["Ivan Petkov "] 6 | license = "MIT/Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/ipetkov/conch-parser" 9 | homepage = "https://github.com/ipetkov/conch-parser" 10 | documentation = "https://docs.rs/conch-parser/" 11 | keywords = ["shell", "parser", "parsing"] 12 | categories = ["parser-implementations"] 13 | description = """ 14 | A library for parsing programs written in the shell programming language. 15 | """ 16 | 17 | [features] 18 | # FIXME(breaking): technically breaking if we remove any features 19 | nightly = [] 20 | clippy = [] 21 | 22 | [dependencies] 23 | void = "1" 24 | 25 | [dev-dependencies] 26 | owned_chars = "0.3" 27 | 28 | [badges] 29 | travis-ci = { repository = "ipetkov/conch-parser" } 30 | appveyor = { repository = "ipetkov/conch-parser" } 31 | is-it-maintained-issue-resolution = { repository = "ipetkov/conch-parser" } 32 | is-it-maintained-open-issues = { repository = "ipetkov/conch-parser" } 33 | codecov = { repository = "ipetkov/conch-parser" } 34 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ivan Petkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conch-parser 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/conch-parser.svg)](https://crates.io/crates/conch-parser) 4 | [![Documentation](https://docs.rs/conch-parser/badge.svg)](https://docs.rs/conch-parser) 5 | [![Build Status](https://travis-ci.org/ipetkov/conch-parser.svg?branch=master)](https://travis-ci.org/ipetkov/conch-parser) 6 | [![Build Status](https://img.shields.io/appveyor/ci/ipetkov/conch-parser/master.svg)](https://ci.appveyor.com/project/ipetkov/conch-parser) 7 | [![Coverage](https://img.shields.io/codecov/c/github/ipetkov/conch-parser/master.svg)](https://codecov.io/gh/ipetkov/conch-parser) 8 | 9 | A Rust library for parsing Unix shell commands. 10 | 11 | ## Quick Start 12 | First, add this to your `Cargo.toml`: 13 | 14 | ```toml 15 | [dependencies] 16 | conch-parser = "0.1.0" 17 | ``` 18 | 19 | Next, you can get started with: 20 | 21 | ```rust 22 | use conch_parser::lexer::Lexer; 23 | use conch_parser::parse::DefaultParser; 24 | 25 | fn main() { 26 | // Initialize our token lexer and shell parser with the program's input 27 | let lex = Lexer::new("echo foo bar".chars()); 28 | let parser = DefaultParser::new(lex); 29 | 30 | // Parse our input! 31 | for t in parser { 32 | println!("{:?}", t); 33 | } 34 | } 35 | ``` 36 | 37 | ## About 38 | This library offers parsing shell commands as defined by the 39 | [POSIX.1-2008][POSIX] standard. The parser remains agnostic to the final AST 40 | representation by passing intermediate results to an AST `Builder`, allowing 41 | for easy changes to the final AST structure without having to walk and transform 42 | an entire AST produced by the parser. See the documentation for more information. 43 | 44 | [POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/ 45 | 46 | ### Goals 47 | * Provide shell command parser which is correct and efficient, and agnostic to 48 | the final AST representation 49 | * Parsing should never require any form of runtime, thus no part of the source 50 | should have to be executed or evaluated when parsing 51 | 52 | ### Non-goals 53 | * 100% POSIX.1-2008 compliance: the standard is used as a baseline for 54 | implementation and features may be further added (or dropped) based on what 55 | makes sense or is most useful 56 | * Feature parity with all major shells: unless a specific feature is 57 | widely used (and considered common) or another compelling reason exists 58 | for inclusion. However, this is not to say that the library will never 59 | support extensions for adding additional syntax features. 60 | 61 | ## Supported grammar 62 | - [x] Conditional lists (`foo && bar || baz`) 63 | - [x] Pipelines (`! foo | bar`) 64 | - [x] Compound commands 65 | - [x] Brace blocks (`{ foo; }`) 66 | - [x] Subshells (`$(foo)`) 67 | - [x] `for` / `case` / `if` / `while` / `until` 68 | - [x] Function declarations 69 | - [x] Redirections 70 | - [x] Heredocs 71 | - [x] Comments 72 | - [x] Parameters (`$foo`, `$@`, etc.) 73 | - [x] Parameter substitutions (`${foo:-bar}`) 74 | - [x] Quoting (single, double, backticks, escaping) 75 | - [ ] Arithmetic substitutions 76 | - [x] Common arithmetic operations required by the [standard][POSIX-arith] 77 | - [x] Variable expansion 78 | - [ ] Other inner abitrary parameter/substitution expansion 79 | 80 | [POSIX-arith]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_04 81 | 82 | ## License 83 | Licensed under either of 84 | 85 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 86 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 87 | 88 | at your option. 89 | 90 | ### Contribution 91 | Unless you explicitly state otherwise, any contribution intentionally 92 | submitted for inclusion in the work by you, as defined in the Apache-2.0 93 | license, shall be dual licensed as above, without any additional terms or 94 | conditions. 95 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - TARGET: x86_64-pc-windows-msvc 4 | RUST_VERSION: stable 5 | 6 | branches: 7 | only: 8 | - master 9 | - /v?\d(\.\d)*/ 10 | 11 | install: 12 | - set PATH=C:\Program Files\Git\mingw64\bin;%PATH% 13 | - curl -sSf -o rustup-init.exe https://win.rustup.rs/ 14 | - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% 15 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 16 | - rustc -vV 17 | - cargo -vV 18 | 19 | build: false 20 | 21 | test_script: 22 | - cargo check 23 | - cargo test 24 | -------------------------------------------------------------------------------- /ci/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit with an error if any command fails 4 | set -e 5 | 6 | # NB: cargo adds a unique identifier to all built tests/deps to disambiguate 7 | # between different deps (e.g. libs, executables, integ tests, etc.), but this means 8 | # that our deps directory could have other/older test files esp. if they were cached 9 | # from a previous build. 10 | # 11 | # As of today there is no way to get cargo to clean up the test deps so we've got 12 | # one of two options: 13 | # 1) clean the entire directory and rebuild it - at the cost of extra build times 14 | # 2) run cargo test, capture its output and infer which tests are actually relevant to run 15 | # 16 | # travis-cargo does both #1 and #2 above (but currently doesn't work with kcov so we're 17 | # reimplementing its functionality here. #1 is largely due to recompiling with the 18 | # link-dead-code flag, which we're already setting in the .travis.yml config, so it 19 | # should be safe to skip doing #1 20 | # 21 | # Run cargo test here, due to previous compilations this should immediately run the 22 | # tests (but no output will be shown, so if the tests take longer than 10mins to run 23 | # it may be worth calling cargo through travis_wait). 24 | # 25 | # Then we extract any test files that get executed 26 | for file in $(cargo test 2>&1 1>/dev/null | grep -oP '(?<=Running target\/debug\/).*'); do 27 | cov_dir="target/cov/$(basename "$file")"; 28 | mkdir -p "$cov_dir" 29 | ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "$cov_dir" "target/debug/$file"; 30 | done 31 | 32 | # Report the coverage to codecov 33 | bash <(curl -s https://codecov.io/bash) 34 | echo "Uploaded code coverage"; 35 | -------------------------------------------------------------------------------- /ci/install-kcov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit with an error if any command fails 4 | set -e 5 | 6 | wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz 7 | tar xzf master.tar.gz 8 | 9 | cd kcov-master 10 | mkdir build 11 | 12 | cd build 13 | cmake .. 14 | make 15 | make install DESTDIR=../../kcov-build 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | behavior: new 3 | -------------------------------------------------------------------------------- /examples/analysis.rs: -------------------------------------------------------------------------------- 1 | //! An example of how one may analyze a shell program. In this case, 2 | //! we traverse the parsed AST and count up how many times an "echo" 3 | //! command appears in the source. 4 | 5 | use conch_parser::ast; 6 | use conch_parser::lexer::Lexer; 7 | use conch_parser::parse::DefaultParser; 8 | use owned_chars::OwnedCharsExt; 9 | 10 | use std::io::{stdin, BufRead, BufReader}; 11 | 12 | fn main() -> Result<(), Box> { 13 | let stdin = BufReader::new(stdin()) 14 | .lines() 15 | .map(|result| result.expect("stdin error")) 16 | .flat_map(|mut line| { 17 | line.push('\n'); // BufRead::lines unfortunately strips \n and \r\n 18 | line.into_chars() 19 | }); 20 | 21 | // Initialize our token lexer and shell parser with the program's input 22 | let lex = Lexer::new(stdin); 23 | let parser = DefaultParser::new(lex); 24 | 25 | let mut num_echo = 0usize; 26 | for cmd in parser { 27 | num_echo += count_echo_top_level(&cmd?); 28 | } 29 | 30 | println!("total number of echo commands: {}", num_echo); 31 | Ok(()) 32 | } 33 | 34 | fn count_echo_top_level_array(cmds: &[ast::TopLevelCommand]) -> usize { 35 | cmds.iter().map(count_echo_top_level).sum() 36 | } 37 | 38 | fn count_echo_top_level(cmd: &ast::TopLevelCommand) -> usize { 39 | match &cmd.0 { 40 | ast::Command::Job(list) | ast::Command::List(list) => std::iter::once(&list.first) 41 | .chain(list.rest.iter().map(|and_or| match and_or { 42 | ast::AndOr::And(cmd) | ast::AndOr::Or(cmd) => cmd, 43 | })) 44 | .map(|cmd| count_echo_listable(&cmd)) 45 | .sum(), 46 | } 47 | } 48 | 49 | fn count_echo_listable(cmd: &ast::DefaultListableCommand) -> usize { 50 | match cmd { 51 | ast::ListableCommand::Single(cmd) => count_echo_pipeable(cmd), 52 | ast::ListableCommand::Pipe(_, cmds) => cmds.into_iter().map(count_echo_pipeable).sum(), 53 | } 54 | } 55 | 56 | fn count_echo_pipeable(cmd: &ast::DefaultPipeableCommand) -> usize { 57 | match cmd { 58 | ast::PipeableCommand::Simple(cmd) => count_echo_simple(cmd), 59 | ast::PipeableCommand::Compound(cmd) => count_echo_compound(cmd), 60 | ast::PipeableCommand::FunctionDef(_, cmd) => count_echo_compound(cmd), 61 | } 62 | } 63 | 64 | fn count_echo_compound(cmd: &ast::DefaultCompoundCommand) -> usize { 65 | match &cmd.kind { 66 | ast::CompoundCommandKind::Brace(cmds) | ast::CompoundCommandKind::Subshell(cmds) => { 67 | count_echo_top_level_array(&cmds) 68 | } 69 | 70 | ast::CompoundCommandKind::While(lp) | ast::CompoundCommandKind::Until(lp) => { 71 | count_echo_top_level_array(&lp.guard) + count_echo_top_level_array(&lp.body) 72 | } 73 | 74 | ast::CompoundCommandKind::If { 75 | conditionals, 76 | else_branch, 77 | } => { 78 | let num_echo_in_conditionals = conditionals 79 | .iter() 80 | .map(|gbp| { 81 | count_echo_top_level_array(&gbp.guard) + count_echo_top_level_array(&gbp.body) 82 | }) 83 | .sum::(); 84 | 85 | let num_echo_in_else = else_branch 86 | .as_ref() 87 | .map(|cmds| count_echo_top_level_array(&cmds)) 88 | .unwrap_or(0); 89 | 90 | num_echo_in_conditionals + num_echo_in_else 91 | } 92 | 93 | ast::CompoundCommandKind::For { body, .. } => count_echo_top_level_array(&body), 94 | 95 | ast::CompoundCommandKind::Case { arms, .. } => arms 96 | .iter() 97 | .map(|pat| count_echo_top_level_array(&pat.body)) 98 | .sum(), 99 | } 100 | } 101 | 102 | fn count_echo_simple(cmd: &ast::DefaultSimpleCommand) -> usize { 103 | cmd.redirects_or_cmd_words 104 | .iter() 105 | .filter_map(|redirect_or_word| match redirect_or_word { 106 | ast::RedirectOrCmdWord::CmdWord(w) => Some(&w.0), 107 | ast::RedirectOrCmdWord::Redirect(_) => None, 108 | }) 109 | .filter_map(|word| match word { 110 | ast::ComplexWord::Single(w) => Some(w), 111 | // We're going to ignore concatenated words for simplicity here 112 | ast::ComplexWord::Concat(_) => None, 113 | }) 114 | .filter_map(|word| match word { 115 | ast::Word::SingleQuoted(w) => Some(w), 116 | ast::Word::Simple(w) => get_simple_word_as_string(w), 117 | 118 | ast::Word::DoubleQuoted(words) if words.len() == 1 => { 119 | get_simple_word_as_string(&words[0]) 120 | } 121 | ast::Word::DoubleQuoted(_) => None, // Ignore all multi-word double quoted strings 122 | }) 123 | .filter(|w| *w == "echo") 124 | .count() 125 | } 126 | 127 | fn get_simple_word_as_string(word: &ast::DefaultSimpleWord) -> Option<&String> { 128 | match word { 129 | ast::SimpleWord::Literal(w) => Some(w), 130 | _ => None, // Ignoring substitutions and others for simplicity here 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /examples/parsing_input.rs: -------------------------------------------------------------------------------- 1 | use conch_parser::lexer::Lexer; 2 | use conch_parser::parse::DefaultParser; 3 | use owned_chars::OwnedCharsExt; 4 | 5 | use std::io::{stdin, BufRead, BufReader}; 6 | 7 | fn main() { 8 | let stdin = BufReader::new(stdin()) 9 | .lines() 10 | .map(Result::unwrap) 11 | .flat_map(|mut line| { 12 | line.push_str("\n"); // BufRead::lines unfortunately strips \n and \r\n 13 | line.into_chars() 14 | }); 15 | 16 | // Initialize our token lexer and shell parser with the program's input 17 | let lex = Lexer::new(stdin); 18 | let parser = DefaultParser::new(lex); 19 | 20 | // Parse our input! 21 | for t in parser { 22 | println!("{:?}", t); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_try_shorthand = true 2 | -------------------------------------------------------------------------------- /src/ast/builder/empty_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::builder::*; 2 | use crate::ast::{AndOr, RedirectOrCmdWord, RedirectOrEnvVar}; 3 | use void::Void; 4 | 5 | /// A no-op `Builder` which ignores all inputs and always returns `()`. 6 | /// 7 | /// Useful for validation of correct programs (i.e. parsing input without 8 | /// caring about the actual AST representations). 9 | #[derive(Debug, Copy, Clone)] 10 | pub struct EmptyBuilder; 11 | 12 | impl Default for EmptyBuilder { 13 | fn default() -> Self { 14 | EmptyBuilder::new() 15 | } 16 | } 17 | 18 | impl EmptyBuilder { 19 | /// Constructs a builder. 20 | pub fn new() -> Self { 21 | EmptyBuilder 22 | } 23 | } 24 | 25 | impl Builder for EmptyBuilder { 26 | type Command = (); 27 | type CommandList = (); 28 | type ListableCommand = (); 29 | type PipeableCommand = (); 30 | type CompoundCommand = (); 31 | type Word = (); 32 | type Redirect = (); 33 | type Error = Void; 34 | 35 | fn complete_command( 36 | &mut self, 37 | _pre_cmd_comments: Vec, 38 | _cmd: Self::Command, 39 | _separator: SeparatorKind, 40 | _cmd_comment: Option, 41 | ) -> Result { 42 | Ok(()) 43 | } 44 | 45 | fn and_or_list( 46 | &mut self, 47 | _first: Self::ListableCommand, 48 | _rest: Vec<(Vec, AndOr)>, 49 | ) -> Result { 50 | Ok(()) 51 | } 52 | 53 | fn pipeline( 54 | &mut self, 55 | _bang: bool, 56 | _cmds: Vec<(Vec, Self::Command)>, 57 | ) -> Result { 58 | Ok(()) 59 | } 60 | 61 | fn simple_command( 62 | &mut self, 63 | _redirects_or_env_vars: Vec>, 64 | _redirects_or_cmd_words: Vec>, 65 | ) -> Result { 66 | Ok(()) 67 | } 68 | 69 | fn brace_group( 70 | &mut self, 71 | _cmds: CommandGroup, 72 | _redirects: Vec, 73 | ) -> Result { 74 | Ok(()) 75 | } 76 | 77 | fn subshell( 78 | &mut self, 79 | _cmds: CommandGroup, 80 | _redirects: Vec, 81 | ) -> Result { 82 | Ok(()) 83 | } 84 | 85 | fn loop_command( 86 | &mut self, 87 | __kind: LoopKind, 88 | __guard_body_pair: GuardBodyPairGroup, 89 | __redirects: Vec, 90 | ) -> Result { 91 | Ok(()) 92 | } 93 | 94 | fn if_command( 95 | &mut self, 96 | _fragments: IfFragments, 97 | _redirects: Vec, 98 | ) -> Result { 99 | Ok(()) 100 | } 101 | 102 | fn for_command( 103 | &mut self, 104 | _fragments: ForFragments, 105 | _redirects: Vec, 106 | ) -> Result { 107 | Ok(()) 108 | } 109 | 110 | fn case_command( 111 | &mut self, 112 | _fragments: CaseFragments, 113 | _redirects: Vec, 114 | ) -> Result { 115 | Ok(()) 116 | } 117 | 118 | fn function_declaration( 119 | &mut self, 120 | _name: String, 121 | _post_name_comments: Vec, 122 | _body: Self::CompoundCommand, 123 | ) -> Result { 124 | Ok(()) 125 | } 126 | 127 | fn comments(&mut self, _comments: Vec) -> Result<(), Self::Error> { 128 | Ok(()) 129 | } 130 | 131 | fn word(&mut self, _kind: ComplexWordKind) -> Result { 132 | Ok(()) 133 | } 134 | 135 | fn redirect(&mut self, _kind: RedirectKind) -> Result { 136 | Ok(()) 137 | } 138 | 139 | fn compound_command_into_pipeable( 140 | &mut self, 141 | _cmd: Self::CompoundCommand, 142 | ) -> Result { 143 | Ok(()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/lexer.rs: -------------------------------------------------------------------------------- 1 | //! This module defines a lexer to recognize tokens of the shell language. 2 | 3 | use self::TokenOrLiteral::*; 4 | use super::token::Token::*; 5 | use super::token::{Positional, Token}; 6 | use std::iter::{Fuse, Peekable}; 7 | 8 | #[derive(PartialEq, Eq, Debug, Clone)] 9 | enum TokenOrLiteral { 10 | Tok(Token), 11 | Escaped(Option), 12 | Lit(char), 13 | } 14 | 15 | /// Converts raw characters into shell tokens. 16 | #[must_use = "`Lexer` is lazy and does nothing unless consumed"] 17 | #[derive(Clone, Debug)] 18 | pub struct Lexer> { 19 | inner: Peekable>, 20 | peeked: Option, 21 | } 22 | 23 | impl> Lexer { 24 | /// Creates a new Lexer from any char iterator. 25 | pub fn new(iter: I) -> Lexer { 26 | Lexer { 27 | inner: iter.fuse().peekable(), 28 | peeked: None, 29 | } 30 | } 31 | 32 | #[inline] 33 | fn next_is(&mut self, c: char) -> bool { 34 | let is = self.inner.peek() == Some(&c); 35 | if is { 36 | self.inner.next(); 37 | } 38 | is 39 | } 40 | 41 | fn next_internal(&mut self) -> Option { 42 | if self.peeked.is_some() { 43 | return self.peeked.take(); 44 | } 45 | 46 | let cur = match self.inner.next() { 47 | Some(c) => c, 48 | None => return None, 49 | }; 50 | 51 | let tok = match cur { 52 | '\n' => Newline, 53 | '!' => Bang, 54 | '~' => Tilde, 55 | '#' => Pound, 56 | '*' => Star, 57 | '?' => Question, 58 | '%' => Percent, 59 | '-' => Dash, 60 | '=' => Equals, 61 | '+' => Plus, 62 | ':' => Colon, 63 | '@' => At, 64 | '^' => Caret, 65 | '/' => Slash, 66 | ',' => Comma, 67 | 68 | // Make sure that we treat the next token as a single character, 69 | // preventing multi-char tokens from being recognized. This is 70 | // important because something like `\&&` would mean that the 71 | // first & is a literal while the second retains its properties. 72 | // We will let the parser deal with what actually becomes a literal. 73 | '\\' => { 74 | return Some(Escaped( 75 | self.inner 76 | .next() 77 | .and_then(|c| Lexer::new(std::iter::once(c)).next()), 78 | )) 79 | } 80 | 81 | '\'' => SingleQuote, 82 | '"' => DoubleQuote, 83 | '`' => Backtick, 84 | 85 | ';' => { 86 | if self.next_is(';') { 87 | DSemi 88 | } else { 89 | Semi 90 | } 91 | } 92 | '&' => { 93 | if self.next_is('&') { 94 | AndIf 95 | } else { 96 | Amp 97 | } 98 | } 99 | '|' => { 100 | if self.next_is('|') { 101 | OrIf 102 | } else { 103 | Pipe 104 | } 105 | } 106 | 107 | '(' => ParenOpen, 108 | ')' => ParenClose, 109 | '{' => CurlyOpen, 110 | '}' => CurlyClose, 111 | '[' => SquareOpen, 112 | ']' => SquareClose, 113 | 114 | '$' => { 115 | // Positional parameters are 0-9, so we only 116 | // need to check a single digit ahead. 117 | let positional = match self.inner.peek() { 118 | Some(&'0') => Some(Positional::Zero), 119 | Some(&'1') => Some(Positional::One), 120 | Some(&'2') => Some(Positional::Two), 121 | Some(&'3') => Some(Positional::Three), 122 | Some(&'4') => Some(Positional::Four), 123 | Some(&'5') => Some(Positional::Five), 124 | Some(&'6') => Some(Positional::Six), 125 | Some(&'7') => Some(Positional::Seven), 126 | Some(&'8') => Some(Positional::Eight), 127 | Some(&'9') => Some(Positional::Nine), 128 | _ => None, 129 | }; 130 | 131 | match positional { 132 | Some(p) => { 133 | self.inner.next(); // Consume the character we just peeked 134 | ParamPositional(p) 135 | } 136 | None => Dollar, 137 | } 138 | } 139 | 140 | '<' => { 141 | if self.next_is('<') { 142 | if self.next_is('-') { 143 | DLessDash 144 | } else { 145 | DLess 146 | } 147 | } else if self.next_is('&') { 148 | LessAnd 149 | } else if self.next_is('>') { 150 | LessGreat 151 | } else { 152 | Less 153 | } 154 | } 155 | 156 | '>' => { 157 | if self.next_is('&') { 158 | GreatAnd 159 | } else if self.next_is('>') { 160 | DGreat 161 | } else if self.next_is('|') { 162 | Clobber 163 | } else { 164 | Great 165 | } 166 | } 167 | 168 | // Newlines are valid whitespace, however, we want to tokenize them separately! 169 | c if c.is_whitespace() => { 170 | let mut buf = String::new(); 171 | buf.push(c); 172 | 173 | // NB: Can't use filter here because it will advance the iterator too far. 174 | while let Some(&c) = self.inner.peek() { 175 | if c.is_whitespace() && c != '\n' { 176 | self.inner.next(); 177 | buf.push(c); 178 | } else { 179 | break; 180 | } 181 | } 182 | 183 | Whitespace(buf) 184 | } 185 | 186 | c => return Some(Lit(c)), 187 | }; 188 | 189 | Some(Tok(tok)) 190 | } 191 | } 192 | 193 | impl> Iterator for Lexer { 194 | type Item = Token; 195 | 196 | fn next(&mut self) -> Option { 197 | fn name_start_char(c: char) -> bool { 198 | c == '_' || c.is_alphabetic() 199 | } 200 | 201 | fn is_digit(c: char) -> bool { 202 | c.is_digit(10) 203 | } 204 | 205 | fn name_char(c: char) -> bool { 206 | is_digit(c) || name_start_char(c) 207 | } 208 | 209 | match self.next_internal() { 210 | None => None, 211 | Some(Tok(t)) => Some(t), 212 | Some(Escaped(t)) => { 213 | debug_assert_eq!(self.peeked, None); 214 | self.peeked = t.map(Tok); 215 | Some(Backslash) 216 | } 217 | 218 | Some(Lit(c)) => { 219 | let is_name = name_start_char(c); 220 | let mut word = String::new(); 221 | word.push(c); 222 | 223 | loop { 224 | match self.next_internal() { 225 | // If we hit a token, delimit the current word w/o losing the token 226 | Some(tok @ Tok(_)) | Some(tok @ Escaped(_)) => { 227 | debug_assert_eq!(self.peeked, None); 228 | self.peeked = Some(tok); 229 | break; 230 | } 231 | 232 | // Make sure we delimit valid names whenever a non-name char comes along 233 | Some(Lit(c)) if is_name && !name_char(c) => { 234 | debug_assert_eq!(self.peeked, None); 235 | self.peeked = Some(Lit(c)); 236 | return Some(Name(word)); 237 | } 238 | 239 | // Otherwise, keep consuming characters for the literal 240 | Some(Lit(c)) => word.push(c), 241 | 242 | None => break, 243 | } 244 | } 245 | 246 | if is_name { 247 | Some(Name(word)) 248 | } else { 249 | Some(Literal(word)) 250 | } 251 | } 252 | } 253 | } 254 | 255 | fn size_hint(&self) -> (usize, Option) { 256 | // The number of actual tokens we yield will never exceed 257 | // the amount of characters we are processing. In practice 258 | // the caller will probably see a lot fewer tokens than 259 | // number of characters processed, however, they can prepare 260 | // themselves for the worst possible case. A high estimate 261 | // is better than no estimate. 262 | let (_, hi) = self.inner.size_hint(); 263 | let low = if self.peeked.is_some() { 1 } else { 0 }; 264 | (low, hi) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for parsing programs written in the shell programming language. 2 | //! 3 | //! The `Parser` implementation will pass all of its intermediate parse results 4 | //! to a `Builder` implementation, allowing the `Builder` to transform the 5 | //! results to a desired format. This allows for customizing what AST is 6 | //! produced without having to walk and transform an entire AST produced by 7 | //! the parser. 8 | //! 9 | //! See the `Parser` documentation for more information on getting started. 10 | //! 11 | //! # Supported Grammar 12 | //! 13 | //! * Conditional lists (`foo && bar || baz`) 14 | //! * Pipelines (`! foo | bar`) 15 | //! * Compound commands 16 | //! * Brace blocks (`{ foo; }`) 17 | //! * Subshells (`$(foo)`) 18 | //! * `for` / `case` / `if` / `while` / `until` 19 | //! * Function declarations 20 | //! * Redirections 21 | //! * Heredocs 22 | //! * Comments 23 | //! * Parameters (`$foo`, `$@`, etc.) 24 | //! * Parameter substitutions (`${foo:-bar}`) 25 | //! * Quoting (single, double, backticks, escaping) 26 | //! * Arithmetic substitutions 27 | //! * Common arithmetic operations required by the POSIX standard 28 | //! * Variable expansion 29 | //! * **Not yet implemented**: Other inner abitrary parameter/substitution expansion 30 | 31 | #![doc(html_root_url = "https://docs.rs/conch-parser/0.1")] 32 | #![cfg_attr(not(test), deny(clippy::print_stdout))] 33 | #![deny(clippy::wrong_self_convention)] 34 | #![deny(missing_copy_implementations)] 35 | #![deny(missing_debug_implementations)] 36 | #![deny(missing_docs)] 37 | #![deny(rust_2018_idioms)] 38 | #![deny(trivial_casts)] 39 | #![deny(trivial_numeric_casts)] 40 | #![deny(unused_import_braces)] 41 | #![deny(unused_qualifications)] 42 | #![forbid(unsafe_code)] 43 | 44 | pub mod ast; 45 | pub mod lexer; 46 | pub mod parse; 47 | pub mod token; 48 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the tokens of the shell language. 2 | 3 | use self::Token::*; 4 | use std::fmt; 5 | 6 | /// The inner representation of a positional parameter. 7 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 8 | pub enum Positional { 9 | /// $0 10 | Zero, 11 | /// $1 12 | One, 13 | /// $2 14 | Two, 15 | /// $3 16 | Three, 17 | /// $4 18 | Four, 19 | /// $5 20 | Five, 21 | /// $6 22 | Six, 23 | /// $7 24 | Seven, 25 | /// $8 26 | Eight, 27 | /// $9 28 | Nine, 29 | } 30 | 31 | impl Positional { 32 | /// Converts a `Positional` as a numeric representation 33 | pub fn as_num(&self) -> u8 { 34 | match *self { 35 | Positional::Zero => 0, 36 | Positional::One => 1, 37 | Positional::Two => 2, 38 | Positional::Three => 3, 39 | Positional::Four => 4, 40 | Positional::Five => 5, 41 | Positional::Six => 6, 42 | Positional::Seven => 7, 43 | Positional::Eight => 8, 44 | Positional::Nine => 9, 45 | } 46 | } 47 | 48 | /// Attempts to convert a number to a `Positional` representation 49 | pub fn from_num(num: u8) -> Option { 50 | match num { 51 | 0 => Some(Positional::Zero), 52 | 1 => Some(Positional::One), 53 | 2 => Some(Positional::Two), 54 | 3 => Some(Positional::Three), 55 | 4 => Some(Positional::Four), 56 | 5 => Some(Positional::Five), 57 | 6 => Some(Positional::Six), 58 | 7 => Some(Positional::Seven), 59 | 8 => Some(Positional::Eight), 60 | 9 => Some(Positional::Nine), 61 | _ => None, 62 | } 63 | } 64 | } 65 | 66 | impl Into for Positional { 67 | fn into(self) -> u8 { 68 | self.as_num() 69 | } 70 | } 71 | 72 | /// The representation of (context free) shell tokens. 73 | #[derive(PartialEq, Eq, Debug, Clone)] 74 | pub enum Token { 75 | /// \n 76 | Newline, 77 | 78 | /// ( 79 | ParenOpen, 80 | /// ) 81 | ParenClose, 82 | /// { 83 | CurlyOpen, 84 | /// } 85 | CurlyClose, 86 | /// [ 87 | SquareOpen, 88 | /// ] 89 | SquareClose, 90 | 91 | /// ! 92 | Bang, 93 | /// ~ 94 | Tilde, 95 | /// \# 96 | Pound, 97 | /// * 98 | Star, 99 | /// ? 100 | Question, 101 | /// \\ 102 | Backslash, 103 | /// % 104 | Percent, 105 | /// \- 106 | Dash, 107 | /// \= 108 | Equals, 109 | /// + 110 | Plus, 111 | /// : 112 | Colon, 113 | /// @ 114 | At, 115 | /// ^ 116 | Caret, 117 | /// / 118 | Slash, 119 | /// , 120 | Comma, 121 | 122 | /// ' 123 | SingleQuote, 124 | /// " 125 | DoubleQuote, 126 | /// ` 127 | Backtick, 128 | 129 | /// ; 130 | Semi, 131 | /// & 132 | Amp, 133 | /// | 134 | Pipe, 135 | /// && 136 | AndIf, 137 | /// || 138 | OrIf, 139 | /// ;; 140 | DSemi, 141 | 142 | /// < 143 | Less, 144 | /// \> 145 | Great, 146 | /// << 147 | DLess, 148 | /// \>> 149 | DGreat, 150 | /// \>& 151 | GreatAnd, 152 | /// <& 153 | LessAnd, 154 | /// <<- 155 | DLessDash, 156 | /// \>| 157 | Clobber, 158 | /// <> 159 | LessGreat, 160 | 161 | /// $ 162 | Dollar, 163 | /// $0, $1, ..., $9 164 | /// 165 | /// Must be its own token to avoid lumping the positional parameter 166 | /// as a `Literal` if the parameter is concatenated to something. 167 | ParamPositional(Positional), 168 | 169 | /// Any string of whitespace characters NOT including a newline. 170 | Whitespace(String), 171 | 172 | /// Any literal delimited by whitespace. 173 | Literal(String), 174 | /// A `Literal` capable of being used as a variable or function name. According to the POSIX 175 | /// standard it should only contain alphanumerics or underscores, and does not start with a digit. 176 | Name(String), 177 | } 178 | 179 | impl fmt::Display for Token { 180 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 181 | write!(fmt, "{}", self.as_str()) 182 | } 183 | } 184 | 185 | impl Token { 186 | /// Returns if the token's length is zero. 187 | pub fn is_empty(&self) -> bool { 188 | self.len() == 0 189 | } 190 | 191 | /// Returns the number of characters it took to recognize a token. 192 | pub fn len(&self) -> usize { 193 | self.as_str().len() 194 | } 195 | 196 | /// Indicates whether a word can be delimited by this token 197 | /// when the token is **not** quoted or escaped. 198 | pub fn is_word_delimiter(&self) -> bool { 199 | match *self { 200 | Newline | ParenOpen | ParenClose | Semi | Amp | Less | Great | Pipe | AndIf | OrIf 201 | | DSemi | DLess | DGreat | GreatAnd | LessAnd | DLessDash | Clobber | LessGreat 202 | | Whitespace(_) => true, 203 | 204 | Bang | Star | Question | Backslash | SingleQuote | DoubleQuote | Backtick | Percent 205 | | Dash | Equals | Plus | Colon | At | Caret | Slash | Comma | CurlyOpen 206 | | CurlyClose | SquareOpen | SquareClose | Dollar | Tilde | Pound | Name(_) 207 | | Literal(_) | ParamPositional(_) => false, 208 | } 209 | } 210 | 211 | /// Gets a representation of the token as a string slice. 212 | pub fn as_str(&self) -> &str { 213 | match *self { 214 | Newline => "\n", 215 | ParenOpen => "(", 216 | ParenClose => ")", 217 | CurlyOpen => "{", 218 | CurlyClose => "}", 219 | SquareOpen => "[", 220 | SquareClose => "]", 221 | Dollar => "$", 222 | Bang => "!", 223 | Semi => ";", 224 | Amp => "&", 225 | Less => "<", 226 | Great => ">", 227 | Pipe => "|", 228 | Tilde => "~", 229 | Pound => "#", 230 | Star => "*", 231 | Question => "?", 232 | Backslash => "\\", 233 | Percent => "%", 234 | Dash => "-", 235 | Equals => "=", 236 | Plus => "+", 237 | Colon => ":", 238 | At => "@", 239 | Caret => "^", 240 | Slash => "/", 241 | Comma => ",", 242 | SingleQuote => "\'", 243 | DoubleQuote => "\"", 244 | Backtick => "`", 245 | AndIf => "&&", 246 | OrIf => "||", 247 | DSemi => ";;", 248 | DLess => "<<", 249 | DGreat => ">>", 250 | GreatAnd => ">&", 251 | LessAnd => "<&", 252 | DLessDash => "<<-", 253 | Clobber => ">|", 254 | LessGreat => "<>", 255 | 256 | ParamPositional(Positional::Zero) => "$0", 257 | ParamPositional(Positional::One) => "$1", 258 | ParamPositional(Positional::Two) => "$2", 259 | ParamPositional(Positional::Three) => "$3", 260 | ParamPositional(Positional::Four) => "$4", 261 | ParamPositional(Positional::Five) => "$5", 262 | ParamPositional(Positional::Six) => "$6", 263 | ParamPositional(Positional::Seven) => "$7", 264 | ParamPositional(Positional::Eight) => "$8", 265 | ParamPositional(Positional::Nine) => "$9", 266 | 267 | Whitespace(ref s) | Name(ref s) | Literal(ref s) => s, 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tests/and_or.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::PipeableCommand::*; 3 | use conch_parser::ast::*; 4 | use conch_parser::parse::ParseError::*; 5 | use conch_parser::token::Token; 6 | 7 | mod parse_support; 8 | use crate::parse_support::*; 9 | 10 | #[test] 11 | fn test_and_or_correct_associativity() { 12 | let mut p = make_parser("foo || bar && baz"); 13 | let correct = CommandList { 14 | first: ListableCommand::Single(Simple(cmd_simple("foo"))), 15 | rest: vec![ 16 | AndOr::Or(ListableCommand::Single(Simple(cmd_simple("bar")))), 17 | AndOr::And(ListableCommand::Single(Simple(cmd_simple("baz")))), 18 | ], 19 | }; 20 | assert_eq!(correct, p.and_or_list().unwrap()); 21 | } 22 | 23 | #[test] 24 | fn test_and_or_valid_with_newlines_after_operator() { 25 | let mut p = make_parser("foo ||\n\n\n\nbar && baz"); 26 | let correct = CommandList { 27 | first: ListableCommand::Single(Simple(cmd_simple("foo"))), 28 | rest: vec![ 29 | AndOr::Or(ListableCommand::Single(Simple(cmd_simple("bar")))), 30 | AndOr::And(ListableCommand::Single(Simple(cmd_simple("baz")))), 31 | ], 32 | }; 33 | assert_eq!(correct, p.and_or_list().unwrap()); 34 | } 35 | 36 | #[test] 37 | fn test_and_or_invalid_with_newlines_before_operator() { 38 | let mut p = make_parser("foo || bar\n\n&& baz"); 39 | p.and_or_list().unwrap(); // Successful parse Or(foo, bar) 40 | // Fail to parse "&& baz" which is an error 41 | assert_eq!( 42 | Err(Unexpected(Token::AndIf, src(12, 3, 1))), 43 | p.complete_command() 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /tests/backticked.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::ComplexWord::*; 3 | use conch_parser::ast::SimpleWord::*; 4 | use conch_parser::ast::*; 5 | use conch_parser::parse::ParseError::*; 6 | use conch_parser::token::Token; 7 | 8 | mod parse_support; 9 | use crate::parse_support::*; 10 | 11 | #[test] 12 | fn test_backticked_valid() { 13 | let correct = word_subst(ParameterSubstitution::Command(vec![cmd("foo")])); 14 | assert_eq!( 15 | correct, 16 | make_parser("`foo`") 17 | .backticked_command_substitution() 18 | .unwrap() 19 | ); 20 | } 21 | 22 | #[test] 23 | fn test_backticked_valid_backslashes_removed_if_before_dollar_backslash_and_backtick() { 24 | let correct = word_subst(ParameterSubstitution::Command(vec![cmd_from_simple( 25 | SimpleCommand { 26 | redirects_or_env_vars: vec![], 27 | redirects_or_cmd_words: vec![ 28 | RedirectOrCmdWord::CmdWord(word("foo")), 29 | RedirectOrCmdWord::CmdWord(TopLevelWord(Concat(vec![ 30 | Word::Simple(Param(Parameter::Dollar)), 31 | escaped("`"), 32 | escaped("o"), 33 | ]))), 34 | ], 35 | }, 36 | )])); 37 | assert_eq!( 38 | correct, 39 | make_parser("`foo \\$\\$\\\\\\`\\o`") 40 | .backticked_command_substitution() 41 | .unwrap() 42 | ); 43 | } 44 | 45 | #[test] 46 | fn test_backticked_nested_backticks() { 47 | let correct = word_subst(ParameterSubstitution::Command(vec![cmd_from_simple( 48 | SimpleCommand { 49 | redirects_or_env_vars: vec![], 50 | redirects_or_cmd_words: vec![ 51 | RedirectOrCmdWord::CmdWord(word("foo")), 52 | RedirectOrCmdWord::CmdWord(word_subst(ParameterSubstitution::Command(vec![ 53 | cmd_from_simple(SimpleCommand { 54 | redirects_or_env_vars: vec![], 55 | redirects_or_cmd_words: vec![ 56 | RedirectOrCmdWord::CmdWord(word("bar")), 57 | RedirectOrCmdWord::CmdWord(TopLevelWord(Concat(vec![ 58 | escaped("$"), 59 | escaped("$"), 60 | ]))), 61 | ], 62 | }), 63 | ]))), 64 | ], 65 | }, 66 | )])); 67 | assert_eq!( 68 | correct, 69 | make_parser(r#"`foo \`bar \\\\$\\\\$\``"#) 70 | .backticked_command_substitution() 71 | .unwrap() 72 | ); 73 | } 74 | 75 | #[test] 76 | fn test_backticked_nested_backticks_x2() { 77 | let correct = word_subst(ParameterSubstitution::Command(vec![cmd_from_simple( 78 | SimpleCommand { 79 | redirects_or_env_vars: vec![], 80 | redirects_or_cmd_words: vec![ 81 | RedirectOrCmdWord::CmdWord(word("foo")), 82 | RedirectOrCmdWord::CmdWord(word_subst(ParameterSubstitution::Command(vec![ 83 | cmd_from_simple(SimpleCommand { 84 | redirects_or_env_vars: vec![], 85 | redirects_or_cmd_words: vec![ 86 | RedirectOrCmdWord::CmdWord(word("bar")), 87 | RedirectOrCmdWord::CmdWord(word_subst(ParameterSubstitution::Command( 88 | vec![cmd_from_simple(SimpleCommand { 89 | redirects_or_env_vars: vec![], 90 | redirects_or_cmd_words: vec![ 91 | RedirectOrCmdWord::CmdWord(word("baz")), 92 | RedirectOrCmdWord::CmdWord(TopLevelWord(Concat(vec![ 93 | escaped("$"), 94 | escaped("$"), 95 | ]))), 96 | ], 97 | })], 98 | ))), 99 | ], 100 | }), 101 | ]))), 102 | ], 103 | }, 104 | )])); 105 | assert_eq!( 106 | correct, 107 | make_parser(r#"`foo \`bar \\\`baz \\\\\\\\$\\\\\\\\$ \\\`\``"#) 108 | .backticked_command_substitution() 109 | .unwrap() 110 | ); 111 | } 112 | 113 | #[test] 114 | fn test_backticked_nested_backticks_x3() { 115 | let correct = word_subst(ParameterSubstitution::Command(vec![cmd_from_simple( 116 | SimpleCommand { 117 | redirects_or_env_vars: vec![], 118 | redirects_or_cmd_words: vec![ 119 | RedirectOrCmdWord::CmdWord(word("foo")), 120 | RedirectOrCmdWord::CmdWord(word_subst(ParameterSubstitution::Command(vec![ 121 | cmd_from_simple(SimpleCommand { 122 | redirects_or_env_vars: vec![], 123 | redirects_or_cmd_words: vec![ 124 | RedirectOrCmdWord::CmdWord(word("bar")), 125 | RedirectOrCmdWord::CmdWord(word_subst(ParameterSubstitution::Command( 126 | vec![cmd_from_simple(SimpleCommand { 127 | redirects_or_env_vars: vec![], 128 | redirects_or_cmd_words: vec![ 129 | RedirectOrCmdWord::CmdWord(word("baz")), 130 | RedirectOrCmdWord::CmdWord(word_subst( 131 | ParameterSubstitution::Command(vec![cmd_from_simple( 132 | SimpleCommand { 133 | redirects_or_env_vars: vec![], 134 | redirects_or_cmd_words: vec![ 135 | RedirectOrCmdWord::CmdWord(word("qux")), 136 | RedirectOrCmdWord::CmdWord(TopLevelWord( 137 | Concat(vec![ 138 | escaped("$"), 139 | escaped("$"), 140 | ]), 141 | )), 142 | ], 143 | }, 144 | )]), 145 | )), 146 | ], 147 | })], 148 | ))), 149 | ], 150 | }), 151 | ]))), 152 | ], 153 | }, 154 | )])); 155 | assert_eq!( 156 | correct, 157 | make_parser( 158 | r#"`foo \`bar \\\`baz \\\\\\\`qux \\\\\\\\\\\\\\\\$\\\\\\\\\\\\\\\\$ \\\\\\\` \\\`\``"# 159 | ) 160 | .backticked_command_substitution() 161 | .unwrap() 162 | ); 163 | } 164 | 165 | #[test] 166 | fn test_backticked_invalid_missing_closing_backtick() { 167 | let src = [ 168 | // Missing outermost backtick 169 | (r#"`foo"#, src(0, 1, 1)), 170 | (r#"`foo \`bar \\\\$\\\\$\`"#, src(0, 1, 1)), 171 | ( 172 | r#"`foo \`bar \\\`baz \\\\\\\\$\\\\\\\\$ \\\`\`"#, 173 | src(0, 1, 1), 174 | ), 175 | ( 176 | r#"`foo \`bar \\\`baz \\\\\\\`qux \\\\\\\\\\\\\\\\$ \\\\\\\\\\\\\\\\$ \\\\\\\` \\\`\`"#, 177 | src(0, 1, 1), 178 | ), 179 | // Missing second to last backtick 180 | (r#"`foo \`bar \\\\$\\\\$`"#, src(6, 1, 7)), 181 | ( 182 | r#"`foo \`bar \\\`baz \\\\\\\\$\\\\\\\\$ \\\``"#, 183 | src(6, 1, 7), 184 | ), 185 | ( 186 | r#"`foo \`bar \\\`baz \\\\\\\`qux \\\\\\\\\\\\\\\\$ \\\\\\\\\\\\\\\\$ \\\\\\\` \\\``"#, 187 | src(6, 1, 7), 188 | ), 189 | // Missing third to last backtick 190 | ( 191 | r#"`foo \`bar \\\`baz \\\\\\\\$\\\\\\\\$ \``"#, 192 | src(14, 1, 15), 193 | ), 194 | ( 195 | r#"`foo \`bar \\\`baz \\\\\\\`qux \\\\\\\\\\\\\\\\$ \\\\\\\\\\\\\\\\$ \\\\\\\` \``"#, 196 | src(14, 1, 15), 197 | ), 198 | // Missing fourth to last backtick 199 | ( 200 | r#"`foo \`bar \\\`baz \\\\\\\`qux \\\\\\\\\\\\\\\\$ \\\\\\\\\\\\\\\\$ \\\`\``"#, 201 | src(26, 1, 27), 202 | ), 203 | ]; 204 | 205 | for &(s, p) in &src { 206 | let correct = Unmatched(Token::Backtick, p); 207 | match make_parser(s).backticked_command_substitution() { 208 | Ok(w) => panic!("Unexpectedly parsed the source \"{}\" as\n{:?}", s, w), 209 | Err(ref err) => { 210 | if err != &correct { 211 | panic!( 212 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 213 | s, correct, err 214 | ); 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | #[test] 222 | fn test_backticked_invalid_maintains_accurate_source_positions() { 223 | let src = [ 224 | (r#"`foo ${invalid param}`"#, src(14, 1, 15)), 225 | (r#"`foo \`bar ${invalid param}\``"#, src(20, 1, 21)), 226 | ( 227 | r#"`foo \`bar \\\`baz ${invalid param} \\\`\``"#, 228 | src(28, 1, 29), 229 | ), 230 | ( 231 | r#"`foo \`bar \\\`baz \\\\\\\`qux ${invalid param} \\\\\\\` \\\`\``"#, 232 | src(40, 1, 41), 233 | ), 234 | ]; 235 | 236 | for &(s, p) in &src { 237 | let correct = BadSubst(Token::Whitespace(String::from(" ")), p); 238 | match make_parser(s).backticked_command_substitution() { 239 | Ok(w) => panic!("Unexpectedly parsed the source \"{}\" as\n{:?}", s, w), 240 | Err(ref err) => { 241 | if err != &correct { 242 | panic!( 243 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 244 | s, correct, err 245 | ); 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | #[test] 253 | fn test_backticked_invalid_missing_opening_backtick() { 254 | let mut p = make_parser("foo`"); 255 | assert_eq!( 256 | Err(Unexpected(Token::Name(String::from("foo")), src(0, 1, 1))), 257 | p.backticked_command_substitution() 258 | ); 259 | } 260 | -------------------------------------------------------------------------------- /tests/brace.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::builder::*; 3 | use conch_parser::parse::ParseError::*; 4 | use conch_parser::token::Token; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_brace_group_valid() { 11 | let mut p = make_parser("{ foo\nbar; baz\n#comment1\n#comment2\n }"); 12 | let correct = CommandGroup { 13 | commands: vec![cmd("foo"), cmd("bar"), cmd("baz")], 14 | trailing_comments: vec![ 15 | Newline(Some("#comment1".into())), 16 | Newline(Some("#comment2".into())), 17 | ], 18 | }; 19 | assert_eq!(correct, p.brace_group().unwrap()); 20 | } 21 | 22 | #[test] 23 | fn test_brace_group_invalid_missing_separator() { 24 | assert_eq!( 25 | Err(Unmatched(Token::CurlyOpen, src(0, 1, 1))), 26 | make_parser("{ foo\nbar; baz }").brace_group() 27 | ); 28 | } 29 | 30 | #[test] 31 | fn test_brace_group_invalid_start_must_be_whitespace_delimited() { 32 | let mut p = make_parser("{foo\nbar; baz; }"); 33 | assert_eq!( 34 | Err(Unexpected(Token::Name(String::from("foo")), src(1, 1, 2))), 35 | p.brace_group() 36 | ); 37 | } 38 | 39 | #[test] 40 | fn test_brace_group_valid_end_must_be_whitespace_and_separator_delimited() { 41 | let mut p = make_parser("{ foo\nbar}; baz; }"); 42 | p.brace_group().unwrap(); 43 | assert_eq!(p.complete_command().unwrap(), None); // Ensure stream is empty 44 | let mut p = make_parser("{ foo\nbar; }baz; }"); 45 | p.brace_group().unwrap(); 46 | assert_eq!(p.complete_command().unwrap(), None); // Ensure stream is empty 47 | } 48 | 49 | #[test] 50 | fn test_brace_group_valid_keyword_delimited_by_separator() { 51 | let mut p = make_parser("{ foo }; }"); 52 | let correct = CommandGroup { 53 | commands: vec![cmd_args("foo", &["}"])], 54 | trailing_comments: vec![], 55 | }; 56 | assert_eq!(correct, p.brace_group().unwrap()); 57 | } 58 | 59 | #[test] 60 | fn test_brace_group_invalid_missing_keyword() { 61 | let mut p = make_parser("{ foo\nbar; baz"); 62 | assert_eq!( 63 | Err(Unmatched(Token::CurlyOpen, src(0, 1, 1))), 64 | p.brace_group() 65 | ); 66 | let mut p = make_parser("foo\nbar; baz; }"); 67 | assert_eq!( 68 | Err(Unexpected(Token::Name(String::from("foo")), src(0, 1, 1))), 69 | p.brace_group() 70 | ); 71 | } 72 | 73 | #[test] 74 | fn test_brace_group_invalid_quoted() { 75 | let cmds = [ 76 | ( 77 | "'{' foo\nbar; baz; }", 78 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 79 | ), 80 | ( 81 | "{ foo\nbar; baz; '}'", 82 | Unmatched(Token::CurlyOpen, src(0, 1, 1)), 83 | ), 84 | ( 85 | "\"{\" foo\nbar; baz; }", 86 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 87 | ), 88 | ( 89 | "{ foo\nbar; baz; \"}\"", 90 | Unmatched(Token::CurlyOpen, src(0, 1, 1)), 91 | ), 92 | ]; 93 | 94 | for (c, e) in &cmds { 95 | match make_parser(c).brace_group() { 96 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 97 | Err(ref err) => { 98 | if err != e { 99 | panic!( 100 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 101 | c, e, err 102 | ); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | #[test] 110 | fn test_brace_group_invalid_missing_body() { 111 | assert_eq!( 112 | Err(Unexpected(Token::CurlyClose, src(2, 2, 1))), 113 | make_parser("{\n}").brace_group() 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /tests/case.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::builder::*; 3 | use conch_parser::parse::ParseError::*; 4 | use conch_parser::token::Token; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_case_command_valid() { 11 | let correct = CaseFragments { 12 | word: word("foo"), 13 | post_word_comments: vec![], 14 | in_comment: None, 15 | arms: vec![ 16 | CaseArm { 17 | patterns: CasePatternFragments { 18 | pre_pattern_comments: vec![], 19 | pattern_alternatives: vec![word("hello"), word("goodbye")], 20 | pattern_comment: None, 21 | }, 22 | body: CommandGroup { 23 | commands: vec![cmd_args("echo", &["greeting"])], 24 | trailing_comments: vec![], 25 | }, 26 | arm_comment: None, 27 | }, 28 | CaseArm { 29 | patterns: CasePatternFragments { 30 | pre_pattern_comments: vec![], 31 | pattern_alternatives: vec![word("world")], 32 | pattern_comment: None, 33 | }, 34 | body: CommandGroup { 35 | commands: vec![cmd_args("echo", &["noun"])], 36 | trailing_comments: vec![], 37 | }, 38 | arm_comment: None, 39 | }, 40 | ], 41 | post_arms_comments: vec![], 42 | }; 43 | 44 | let cases = vec![ 45 | // `(` before pattern is optional 46 | "case foo in hello | goodbye) echo greeting;; world) echo noun;; esac", 47 | "case foo in (hello | goodbye) echo greeting;; world) echo noun;; esac", 48 | "case foo in (hello | goodbye) echo greeting;; (world) echo noun;; esac", 49 | // Final `;;` is optional as long as last command is complete 50 | "case foo in hello | goodbye) echo greeting;; world) echo noun\nesac", 51 | "case foo in hello | goodbye) echo greeting;; world) echo noun; esac", 52 | ]; 53 | 54 | for src in cases { 55 | assert_eq!(correct, make_parser(src).case_command().unwrap()); 56 | } 57 | } 58 | 59 | #[test] 60 | fn test_case_command_valid_with_comments() { 61 | let correct = CaseFragments { 62 | word: word("foo"), 63 | post_word_comments: vec![ 64 | Newline(Some(String::from("#word_comment"))), 65 | Newline(Some(String::from("#post_word_a"))), 66 | Newline(None), 67 | Newline(Some(String::from("#post_word_b"))), 68 | ], 69 | in_comment: Some(Newline(Some(String::from("#in_comment")))), 70 | arms: vec![ 71 | CaseArm { 72 | patterns: CasePatternFragments { 73 | pre_pattern_comments: vec![ 74 | Newline(None), 75 | Newline(Some(String::from("#pre_pat_a"))), 76 | ], 77 | pattern_alternatives: vec![word("hello"), word("goodbye")], 78 | pattern_comment: Some(Newline(Some(String::from("#pat_a")))), 79 | }, 80 | body: CommandGroup { 81 | commands: vec![cmd_args("echo", &["greeting"])], 82 | trailing_comments: vec![ 83 | Newline(None), 84 | Newline(Some(String::from("#post_body_a"))), 85 | ], 86 | }, 87 | arm_comment: Some(Newline(Some(String::from("#arm_a")))), 88 | }, 89 | CaseArm { 90 | patterns: CasePatternFragments { 91 | pre_pattern_comments: vec![ 92 | Newline(None), 93 | Newline(Some(String::from("#pre_pat_b"))), 94 | ], 95 | pattern_alternatives: vec![word("world")], 96 | pattern_comment: Some(Newline(Some(String::from("#pat_b")))), 97 | }, 98 | body: CommandGroup { 99 | commands: vec![cmd_args("echo", &["noun"])], 100 | trailing_comments: vec![], 101 | }, 102 | arm_comment: Some(Newline(Some(String::from("#arm_b")))), 103 | }, 104 | ], 105 | post_arms_comments: vec![Newline(None), Newline(Some(String::from("#post_arms")))], 106 | }; 107 | 108 | // Various newlines and comments allowed within the command 109 | let cmd = "case foo #word_comment 110 | #post_word_a 111 | 112 | #post_word_b 113 | in #in_comment 114 | 115 | #pre_pat_a 116 | (hello | goodbye) #pat_a 117 | 118 | #cmd_leading 119 | echo greeting #within_body 120 | 121 | #post_body_a 122 | ;; #arm_a 123 | 124 | #pre_pat_b 125 | world) #pat_b 126 | 127 | #cmd_leading 128 | echo noun 129 | ;; #arm_b 130 | 131 | #post_arms 132 | esac"; 133 | 134 | assert_eq!(Ok(correct), make_parser(cmd).case_command()); 135 | } 136 | 137 | #[test] 138 | fn test_case_command_valid_with_comments_no_body() { 139 | let correct = CaseFragments { 140 | word: word("foo"), 141 | post_word_comments: vec![ 142 | Newline(Some(String::from("#word_comment"))), 143 | Newline(Some(String::from("#post_word_a"))), 144 | Newline(None), 145 | Newline(Some(String::from("#post_word_b"))), 146 | ], 147 | in_comment: Some(Newline(Some(String::from("#in_comment")))), 148 | arms: vec![], 149 | post_arms_comments: vec![Newline(None), Newline(Some(String::from("#post_arms")))], 150 | }; 151 | 152 | // Various newlines and comments allowed within the command 153 | let cmd = "case foo #word_comment 154 | #post_word_a 155 | 156 | #post_word_b 157 | in #in_comment 158 | 159 | #post_arms 160 | esac #case_comment"; 161 | 162 | assert_eq!(correct, make_parser(cmd).case_command().unwrap()); 163 | } 164 | 165 | #[test] 166 | fn test_case_command_word_need_not_be_simple_literal() { 167 | let mut p = make_parser("case 'foo'bar$$ in foo) echo foo;; esac"); 168 | p.case_command().unwrap(); 169 | } 170 | 171 | #[test] 172 | fn test_case_command_valid_with_no_arms() { 173 | let mut p = make_parser("case foo in esac"); 174 | p.case_command().unwrap(); 175 | } 176 | 177 | #[test] 178 | fn test_case_command_valid_branch_with_no_command() { 179 | let mut p = make_parser("case foo in pat)\nesac"); 180 | p.case_command().unwrap(); 181 | let mut p = make_parser("case foo in pat);;esac"); 182 | p.case_command().unwrap(); 183 | } 184 | 185 | #[test] 186 | fn test_case_command_invalid_missing_keyword() { 187 | let mut p = make_parser("foo in foo) echo foo;; bar) echo bar;; esac"); 188 | assert_eq!( 189 | Err(Unexpected(Token::Name(String::from("foo")), src(0, 1, 1))), 190 | p.case_command() 191 | ); 192 | let mut p = make_parser("case foo foo) echo foo;; bar) echo bar;; esac"); 193 | assert_eq!( 194 | Err(IncompleteCmd("case", src(0, 1, 1), "in", src(9, 1, 10))), 195 | p.case_command() 196 | ); 197 | let mut p = make_parser("case foo in foo) echo foo;; bar) echo bar;;"); 198 | assert_eq!( 199 | Err(IncompleteCmd("case", src(0, 1, 1), "esac", src(43, 1, 44))), 200 | p.case_command() 201 | ); 202 | } 203 | 204 | #[test] 205 | fn test_case_command_invalid_missing_word() { 206 | let mut p = make_parser("case in foo) echo foo;; bar) echo bar;; esac"); 207 | assert_eq!( 208 | Err(IncompleteCmd("case", src(0, 1, 1), "in", src(8, 1, 9))), 209 | p.case_command() 210 | ); 211 | } 212 | 213 | #[test] 214 | fn test_case_command_invalid_quoted() { 215 | let cmds = [ 216 | ( 217 | "'case' foo in foo) echo foo;; bar) echo bar;; esac", 218 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 219 | ), 220 | ( 221 | "case foo 'in' foo) echo foo;; bar) echo bar;; esac", 222 | IncompleteCmd("case", src(0, 1, 1), "in", src(9, 1, 10)), 223 | ), 224 | ( 225 | "case foo in foo) echo foo;; bar')' echo bar;; esac", 226 | Unexpected(Token::Name(String::from("echo")), src(35, 1, 36)), 227 | ), 228 | ( 229 | "case foo in foo) echo foo;; bar) echo bar;; 'esac'", 230 | IncompleteCmd("case", src(0, 1, 1), "esac", src(50, 1, 51)), 231 | ), 232 | ( 233 | "\"case\" foo in foo) echo foo;; bar) echo bar;; esac", 234 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 235 | ), 236 | ( 237 | "case foo \"in\" foo) echo foo;; bar) echo bar;; esac", 238 | IncompleteCmd("case", src(0, 1, 1), "in", src(9, 1, 10)), 239 | ), 240 | ( 241 | "case foo in foo) echo foo;; bar\")\" echo bar;; esac", 242 | Unexpected(Token::Name(String::from("echo")), src(35, 1, 36)), 243 | ), 244 | ( 245 | "case foo in foo) echo foo;; bar) echo bar;; \"esac\"", 246 | IncompleteCmd("case", src(0, 1, 1), "esac", src(50, 1, 51)), 247 | ), 248 | ]; 249 | 250 | for (c, e) in &cmds { 251 | match make_parser(c).case_command() { 252 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 253 | Err(ref err) => { 254 | if err != e { 255 | panic!( 256 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 257 | c, e, err 258 | ); 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | #[test] 266 | fn test_case_command_invalid_newline_after_case() { 267 | let mut p = make_parser("case\nfoo in foo) echo foo;; bar) echo bar;; esac"); 268 | assert_eq!( 269 | Err(Unexpected(Token::Newline, src(4, 1, 5))), 270 | p.case_command() 271 | ); 272 | } 273 | 274 | #[test] 275 | fn test_case_command_invalid_concat() { 276 | let mut p = make_parser_from_tokens(vec![ 277 | Token::Literal(String::from("ca")), 278 | Token::Literal(String::from("se")), 279 | Token::Whitespace(String::from(" ")), 280 | Token::Literal(String::from("foo")), 281 | Token::Literal(String::from("bar")), 282 | Token::Whitespace(String::from(" ")), 283 | Token::Literal(String::from("in")), 284 | Token::Literal(String::from("foo")), 285 | Token::ParenClose, 286 | Token::Newline, 287 | Token::Literal(String::from("echo")), 288 | Token::Whitespace(String::from(" ")), 289 | Token::Literal(String::from("foo")), 290 | Token::Newline, 291 | Token::Newline, 292 | Token::DSemi, 293 | Token::Literal(String::from("esac")), 294 | ]); 295 | assert_eq!( 296 | Err(Unexpected(Token::Literal(String::from("ca")), src(0, 1, 1))), 297 | p.case_command() 298 | ); 299 | 300 | let mut p = make_parser_from_tokens(vec![ 301 | Token::Literal(String::from("case")), 302 | Token::Whitespace(String::from(" ")), 303 | Token::Literal(String::from("foo")), 304 | Token::Literal(String::from("bar")), 305 | Token::Whitespace(String::from(" ")), 306 | Token::Literal(String::from("i")), 307 | Token::Literal(String::from("n")), 308 | Token::Literal(String::from("foo")), 309 | Token::ParenClose, 310 | Token::Newline, 311 | Token::Literal(String::from("echo")), 312 | Token::Whitespace(String::from(" ")), 313 | Token::Literal(String::from("foo")), 314 | Token::Newline, 315 | Token::Newline, 316 | Token::DSemi, 317 | Token::Literal(String::from("esac")), 318 | ]); 319 | assert_eq!( 320 | Err(IncompleteCmd("case", src(0, 1, 1), "in", src(12, 1, 13))), 321 | p.case_command() 322 | ); 323 | 324 | let mut p = make_parser_from_tokens(vec![ 325 | Token::Literal(String::from("case")), 326 | Token::Whitespace(String::from(" ")), 327 | Token::Literal(String::from("foo")), 328 | Token::Literal(String::from("bar")), 329 | Token::Whitespace(String::from(" ")), 330 | Token::Literal(String::from("in")), 331 | Token::Whitespace(String::from(" ")), 332 | Token::Literal(String::from("foo")), 333 | Token::ParenClose, 334 | Token::Newline, 335 | Token::Literal(String::from("echo")), 336 | Token::Whitespace(String::from(" ")), 337 | Token::Literal(String::from("foo")), 338 | Token::Newline, 339 | Token::Newline, 340 | Token::DSemi, 341 | Token::Literal(String::from("es")), 342 | Token::Literal(String::from("ac")), 343 | ]); 344 | assert_eq!( 345 | Err(IncompleteCmd("case", src(0, 1, 1), "esac", src(36, 4, 7))), 346 | p.case_command() 347 | ); 348 | } 349 | 350 | #[test] 351 | fn test_case_command_should_recognize_literals_and_names() { 352 | let case_str = String::from("case"); 353 | let in_str = String::from("in"); 354 | let esac_str = String::from("esac"); 355 | for case_tok in vec![Token::Literal(case_str.clone()), Token::Name(case_str)] { 356 | for in_tok in vec![Token::Literal(in_str.clone()), Token::Name(in_str.clone())] { 357 | for esac_tok in vec![ 358 | Token::Literal(esac_str.clone()), 359 | Token::Name(esac_str.clone()), 360 | ] { 361 | let mut p = make_parser_from_tokens(vec![ 362 | case_tok.clone(), 363 | Token::Whitespace(String::from(" ")), 364 | Token::Literal(String::from("foo")), 365 | Token::Literal(String::from("bar")), 366 | Token::Whitespace(String::from(" ")), 367 | in_tok.clone(), 368 | Token::Whitespace(String::from(" ")), 369 | Token::Literal(String::from("foo")), 370 | Token::ParenClose, 371 | Token::Newline, 372 | Token::Literal(String::from("echo")), 373 | Token::Whitespace(String::from(" ")), 374 | Token::Literal(String::from("foo")), 375 | Token::Newline, 376 | Token::Newline, 377 | Token::DSemi, 378 | esac_tok, 379 | ]); 380 | p.case_command().unwrap(); 381 | } 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /tests/command.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use std::rc::Rc; 3 | 4 | use conch_parser::ast::Command::*; 5 | use conch_parser::ast::CompoundCommandKind::*; 6 | use conch_parser::ast::PipeableCommand::*; 7 | use conch_parser::ast::*; 8 | use conch_parser::token::Token; 9 | 10 | mod parse_support; 11 | use crate::parse_support::*; 12 | 13 | #[test] 14 | fn test_complete_command_job() { 15 | let mut p = make_parser("foo && bar & baz"); 16 | let cmd1 = p 17 | .complete_command() 18 | .unwrap() 19 | .expect("failed to parse first command"); 20 | let cmd2 = p 21 | .complete_command() 22 | .unwrap() 23 | .expect("failed to parse second command"); 24 | 25 | let correct1 = TopLevelCommand(Job(CommandList { 26 | first: ListableCommand::Single(Simple(cmd_simple("foo"))), 27 | rest: vec![AndOr::And(ListableCommand::Single(Simple(cmd_simple( 28 | "bar", 29 | ))))], 30 | })); 31 | let correct2 = cmd("baz"); 32 | 33 | assert_eq!(correct1, cmd1); 34 | assert_eq!(correct2, cmd2); 35 | } 36 | 37 | #[test] 38 | fn test_complete_command_non_eager_parse() { 39 | let mut p = make_parser("foo && bar; baz\n\nqux"); 40 | let cmd1 = p 41 | .complete_command() 42 | .unwrap() 43 | .expect("failed to parse first command"); 44 | let cmd2 = p 45 | .complete_command() 46 | .unwrap() 47 | .expect("failed to parse second command"); 48 | let cmd3 = p 49 | .complete_command() 50 | .unwrap() 51 | .expect("failed to parse third command"); 52 | 53 | let correct1 = TopLevelCommand(List(CommandList { 54 | first: ListableCommand::Single(Simple(cmd_simple("foo"))), 55 | rest: vec![AndOr::And(ListableCommand::Single(Simple(cmd_simple( 56 | "bar", 57 | ))))], 58 | })); 59 | let correct2 = cmd("baz"); 60 | let correct3 = cmd("qux"); 61 | 62 | assert_eq!(correct1, cmd1); 63 | assert_eq!(correct2, cmd2); 64 | assert_eq!(correct3, cmd3); 65 | } 66 | 67 | #[test] 68 | fn test_complete_command_valid_no_input() { 69 | let mut p = make_parser(""); 70 | p.complete_command().expect("no input caused an error"); 71 | } 72 | 73 | #[test] 74 | fn test_command_delegates_valid_commands_brace() { 75 | let correct = Compound(Box::new(CompoundCommand { 76 | kind: Brace(vec![cmd("foo")]), 77 | io: vec![], 78 | })); 79 | assert_eq!(correct, make_parser("{ foo; }").command().unwrap()); 80 | } 81 | 82 | #[test] 83 | fn test_command_delegates_valid_commands_subshell() { 84 | let commands = ["(foo)", "( foo)"]; 85 | 86 | let correct = Compound(Box::new(CompoundCommand { 87 | kind: Subshell(vec![cmd("foo")]), 88 | io: vec![], 89 | })); 90 | 91 | for cmd in &commands { 92 | match make_parser(cmd).command() { 93 | Ok(ref result) if result == &correct => {} 94 | Ok(result) => panic!( 95 | "Parsed \"{}\" as an unexpected command type:\n{:#?}", 96 | cmd, result 97 | ), 98 | Err(err) => panic!("Failed to parse \"{}\": {}", cmd, err), 99 | } 100 | } 101 | } 102 | 103 | #[test] 104 | fn test_command_delegates_valid_commands_while() { 105 | let correct = Compound(Box::new(CompoundCommand { 106 | kind: While(GuardBodyPair { 107 | guard: vec![cmd("guard")], 108 | body: vec![cmd("foo")], 109 | }), 110 | io: vec![], 111 | })); 112 | assert_eq!( 113 | correct, 114 | make_parser("while guard; do foo; done").command().unwrap() 115 | ); 116 | } 117 | 118 | #[test] 119 | fn test_command_delegates_valid_commands_until() { 120 | let correct = Compound(Box::new(CompoundCommand { 121 | kind: Until(GuardBodyPair { 122 | guard: vec![cmd("guard")], 123 | body: vec![cmd("foo")], 124 | }), 125 | io: vec![], 126 | })); 127 | assert_eq!( 128 | correct, 129 | make_parser("until guard; do foo; done").command().unwrap() 130 | ); 131 | } 132 | 133 | #[test] 134 | fn test_command_delegates_valid_commands_for() { 135 | let correct = Compound(Box::new(CompoundCommand { 136 | kind: For { 137 | var: String::from("var"), 138 | words: Some(vec![]), 139 | body: vec![cmd("foo")], 140 | }, 141 | io: vec![], 142 | })); 143 | assert_eq!( 144 | correct, 145 | make_parser("for var in; do foo; done").command().unwrap() 146 | ); 147 | } 148 | 149 | #[test] 150 | fn test_command_delegates_valid_commands_if() { 151 | let correct = Compound(Box::new(CompoundCommand { 152 | kind: If { 153 | conditionals: vec![GuardBodyPair { 154 | guard: vec![cmd("guard")], 155 | body: vec![cmd("body")], 156 | }], 157 | else_branch: None, 158 | }, 159 | io: vec![], 160 | })); 161 | assert_eq!( 162 | correct, 163 | make_parser("if guard; then body; fi").command().unwrap() 164 | ); 165 | } 166 | 167 | #[test] 168 | fn test_command_delegates_valid_commands_case() { 169 | let correct = Compound(Box::new(CompoundCommand { 170 | kind: Case { 171 | word: word("foo"), 172 | arms: vec![], 173 | }, 174 | io: vec![], 175 | })); 176 | assert_eq!(correct, make_parser("case foo in esac").command().unwrap()); 177 | } 178 | 179 | #[test] 180 | fn test_command_delegates_valid_simple_commands() { 181 | let correct = Simple(cmd_args_simple("echo", &["foo", "bar"])); 182 | assert_eq!(correct, make_parser("echo foo bar").command().unwrap()); 183 | } 184 | 185 | #[test] 186 | fn test_command_delegates_valid_commands_function() { 187 | let commands = [ 188 | "function foo() { echo body; }", 189 | "function foo () { echo body; }", 190 | "function foo ( ) { echo body; }", 191 | "function foo( ) { echo body; }", 192 | "function foo { echo body; }", 193 | "foo() { echo body; }", 194 | "foo () { echo body; }", 195 | "foo ( ) { echo body; }", 196 | "foo( ) { echo body; }", 197 | ]; 198 | 199 | let correct = FunctionDef( 200 | String::from("foo"), 201 | Rc::new(CompoundCommand { 202 | kind: Brace(vec![cmd_args("echo", &["body"])]), 203 | io: vec![], 204 | }), 205 | ); 206 | 207 | for cmd in &commands { 208 | match make_parser(cmd).command() { 209 | Ok(ref result) if result == &correct => {} 210 | Ok(result) => panic!( 211 | "Parsed \"{}\" as an unexpected command type:\n{:#?}", 212 | cmd, result 213 | ), 214 | Err(err) => panic!("Failed to parse \"{}\": {}", cmd, err), 215 | } 216 | } 217 | } 218 | 219 | #[test] 220 | fn test_command_parses_quoted_compound_commands_as_simple_commands() { 221 | let cases = [ 222 | "{foo; }", // { is a reserved word, thus concatenating it essentially "quotes" it 223 | "'{' foo; }", 224 | "'(' foo; )", 225 | "'while' guard do foo; done", 226 | "'until' guard do foo; done", 227 | "'if' guard; then body; fi", 228 | "'for' var in; do echo $var; done", 229 | "'function' name { echo body; }", 230 | "name'()' { echo body; }", 231 | "123fn() { echo body; }", 232 | "'case' foo in esac", 233 | "\"{\" foo; }", 234 | "\"(\" foo; )", 235 | "\"while\" guard do foo; done", 236 | "\"until\" guard do foo; done", 237 | "\"if\" guard; then body; fi", 238 | "\"for\" var in; do echo $var; done", 239 | "\"function\" name { echo body; }", 240 | "name\"()\" { echo body; }", 241 | "\"case\" foo in esac", 242 | ]; 243 | 244 | for cmd in &cases { 245 | match make_parser(cmd).command() { 246 | Ok(Simple(_)) => {} 247 | Ok(result) => panic!( 248 | "Parse::command unexpectedly parsed \"{}\" as a non-simple command:\n{:#?}", 249 | cmd, result 250 | ), 251 | Err(err) => panic!("Parse::command failed to parse \"{}\": {}", cmd, err), 252 | } 253 | } 254 | } 255 | 256 | #[test] 257 | fn test_command_should_delegate_literals_and_names_loop_while() { 258 | for kw in vec![ 259 | Token::Literal(String::from("while")), 260 | Token::Name(String::from("while")), 261 | ] { 262 | let mut p = make_parser_from_tokens(vec![ 263 | kw, 264 | Token::Newline, 265 | Token::Literal(String::from("guard")), 266 | Token::Newline, 267 | Token::Literal(String::from("do")), 268 | Token::Newline, 269 | Token::Literal(String::from("foo")), 270 | Token::Newline, 271 | Token::Literal(String::from("done")), 272 | ]); 273 | 274 | let cmd = p.command().unwrap(); 275 | if let Compound(ref compound_cmd) = cmd { 276 | if let While(..) = compound_cmd.kind { 277 | continue; 278 | } 279 | } 280 | 281 | panic!("Parsed an unexpected command:\n{:#?}", cmd) 282 | } 283 | } 284 | 285 | #[test] 286 | fn test_command_should_delegate_literals_and_names_loop_until() { 287 | for kw in vec![ 288 | Token::Literal(String::from("until")), 289 | Token::Name(String::from("until")), 290 | ] { 291 | let mut p = make_parser_from_tokens(vec![ 292 | kw, 293 | Token::Newline, 294 | Token::Literal(String::from("guard")), 295 | Token::Newline, 296 | Token::Literal(String::from("do")), 297 | Token::Newline, 298 | Token::Literal(String::from("foo")), 299 | Token::Newline, 300 | Token::Literal(String::from("done")), 301 | ]); 302 | 303 | let cmd = p.command().unwrap(); 304 | if let Compound(ref compound_cmd) = cmd { 305 | if let Until(..) = compound_cmd.kind { 306 | continue; 307 | } 308 | } 309 | 310 | panic!("Parsed an unexpected command:\n{:#?}", cmd) 311 | } 312 | } 313 | 314 | #[test] 315 | fn test_command_should_delegate_literals_and_names_if() { 316 | for if_tok in vec![ 317 | Token::Literal(String::from("if")), 318 | Token::Name(String::from("if")), 319 | ] { 320 | for then_tok in vec![ 321 | Token::Literal(String::from("then")), 322 | Token::Name(String::from("then")), 323 | ] { 324 | for elif_tok in vec![ 325 | Token::Literal(String::from("elif")), 326 | Token::Name(String::from("elif")), 327 | ] { 328 | for else_tok in vec![ 329 | Token::Literal(String::from("else")), 330 | Token::Name(String::from("else")), 331 | ] { 332 | for fi_tok in vec![ 333 | Token::Literal(String::from("fi")), 334 | Token::Name(String::from("fi")), 335 | ] { 336 | let mut p = make_parser_from_tokens(vec![ 337 | if_tok.clone(), 338 | Token::Whitespace(String::from(" ")), 339 | Token::Literal(String::from("guard1")), 340 | Token::Newline, 341 | then_tok.clone(), 342 | Token::Newline, 343 | Token::Literal(String::from("body1")), 344 | elif_tok.clone(), 345 | Token::Whitespace(String::from(" ")), 346 | Token::Literal(String::from("guard2")), 347 | Token::Newline, 348 | then_tok.clone(), 349 | Token::Whitespace(String::from(" ")), 350 | Token::Literal(String::from("body2")), 351 | else_tok.clone(), 352 | Token::Whitespace(String::from(" ")), 353 | Token::Whitespace(String::from(" ")), 354 | Token::Literal(String::from("else part")), 355 | Token::Newline, 356 | fi_tok, 357 | ]); 358 | 359 | let cmd = p.command().unwrap(); 360 | if let Compound(ref compound_cmd) = cmd { 361 | if let If { .. } = compound_cmd.kind { 362 | continue; 363 | } 364 | } 365 | 366 | panic!("Parsed an unexpected command:\n{:#?}", cmd) 367 | } 368 | } 369 | } 370 | } 371 | } 372 | } 373 | 374 | #[test] 375 | fn test_command_should_delegate_literals_and_names_for() { 376 | for for_tok in vec![ 377 | Token::Literal(String::from("for")), 378 | Token::Name(String::from("for")), 379 | ] { 380 | for in_tok in vec![ 381 | Token::Literal(String::from("in")), 382 | Token::Name(String::from("in")), 383 | ] { 384 | let mut p = make_parser_from_tokens(vec![ 385 | for_tok.clone(), 386 | Token::Whitespace(String::from(" ")), 387 | Token::Name(String::from("var")), 388 | Token::Whitespace(String::from(" ")), 389 | in_tok.clone(), 390 | Token::Whitespace(String::from(" ")), 391 | Token::Literal(String::from("one")), 392 | Token::Whitespace(String::from(" ")), 393 | Token::Literal(String::from("two")), 394 | Token::Whitespace(String::from(" ")), 395 | Token::Literal(String::from("three")), 396 | Token::Whitespace(String::from(" ")), 397 | Token::Newline, 398 | Token::Literal(String::from("do")), 399 | Token::Whitespace(String::from(" ")), 400 | Token::Literal(String::from("echo")), 401 | Token::Whitespace(String::from(" ")), 402 | Token::Dollar, 403 | Token::Name(String::from("var")), 404 | Token::Newline, 405 | Token::Literal(String::from("done")), 406 | ]); 407 | 408 | let cmd = p.command().unwrap(); 409 | if let Compound(ref compound_cmd) = cmd { 410 | if let For { .. } = compound_cmd.kind { 411 | continue; 412 | } 413 | } 414 | 415 | panic!("Parsed an unexpected command:\n{:#?}", cmd) 416 | } 417 | } 418 | } 419 | 420 | #[test] 421 | fn test_command_should_delegate_literals_and_names_case() { 422 | let case_str = String::from("case"); 423 | let in_str = String::from("in"); 424 | let esac_str = String::from("esac"); 425 | for case_tok in vec![Token::Literal(case_str.clone()), Token::Name(case_str)] { 426 | for in_tok in vec![Token::Literal(in_str.clone()), Token::Name(in_str.clone())] { 427 | for esac_tok in vec![ 428 | Token::Literal(esac_str.clone()), 429 | Token::Name(esac_str.clone()), 430 | ] { 431 | let mut p = make_parser_from_tokens(vec![ 432 | case_tok.clone(), 433 | Token::Whitespace(String::from(" ")), 434 | Token::Literal(String::from("foo")), 435 | Token::Literal(String::from("bar")), 436 | Token::Whitespace(String::from(" ")), 437 | in_tok.clone(), 438 | Token::Whitespace(String::from(" ")), 439 | Token::Literal(String::from("foo")), 440 | Token::ParenClose, 441 | Token::Newline, 442 | Token::Literal(String::from("echo")), 443 | Token::Whitespace(String::from(" ")), 444 | Token::Literal(String::from("foo")), 445 | Token::Newline, 446 | Token::Newline, 447 | Token::DSemi, 448 | esac_tok, 449 | ]); 450 | 451 | let cmd = p.command().unwrap(); 452 | if let Compound(ref compound_cmd) = cmd { 453 | if let Case { .. } = compound_cmd.kind { 454 | continue; 455 | } 456 | } 457 | 458 | panic!("Parsed an unexpected command:\n{:#?}", cmd) 459 | } 460 | } 461 | } 462 | } 463 | 464 | #[test] 465 | fn test_command_should_delegate_literals_and_names_for_function_declaration() { 466 | for fn_tok in vec![ 467 | Token::Literal(String::from("function")), 468 | Token::Name(String::from("function")), 469 | ] { 470 | let mut p = make_parser_from_tokens(vec![ 471 | fn_tok, 472 | Token::Whitespace(String::from(" ")), 473 | Token::Name(String::from("fn_name")), 474 | Token::Whitespace(String::from(" ")), 475 | Token::ParenOpen, 476 | Token::ParenClose, 477 | Token::Whitespace(String::from(" ")), 478 | Token::CurlyOpen, 479 | Token::Whitespace(String::from(" ")), 480 | Token::Literal(String::from("echo")), 481 | Token::Whitespace(String::from(" ")), 482 | Token::Literal(String::from("fn body")), 483 | Token::Semi, 484 | Token::CurlyClose, 485 | ]); 486 | match p.command() { 487 | Ok(FunctionDef(..)) => {} 488 | Ok(result) => panic!("Parsed an unexpected command type:\n{:#?}", result), 489 | Err(err) => panic!("Failed to parse command: {}", err), 490 | } 491 | } 492 | } 493 | 494 | #[test] 495 | fn test_command_do_not_delegate_functions_only_if_fn_name_is_a_literal_token() { 496 | let mut p = make_parser_from_tokens(vec![ 497 | Token::Literal(String::from("fn_name")), 498 | Token::Whitespace(String::from(" ")), 499 | Token::ParenOpen, 500 | Token::ParenClose, 501 | Token::Whitespace(String::from(" ")), 502 | Token::CurlyOpen, 503 | Token::Literal(String::from("echo")), 504 | Token::Whitespace(String::from(" ")), 505 | Token::Literal(String::from("fn body")), 506 | Token::Semi, 507 | Token::CurlyClose, 508 | ]); 509 | match p.command() { 510 | Ok(Simple(..)) => {} 511 | Ok(result) => panic!("Parsed an unexpected command type:\n{:#?}", result), 512 | Err(err) => panic!("Failed to parse command: {}", err), 513 | } 514 | } 515 | 516 | #[test] 517 | fn test_command_delegate_functions_only_if_fn_name_is_a_name_token() { 518 | let mut p = make_parser_from_tokens(vec![ 519 | Token::Name(String::from("fn_name")), 520 | Token::Whitespace(String::from(" ")), 521 | Token::ParenOpen, 522 | Token::ParenClose, 523 | Token::Whitespace(String::from(" ")), 524 | Token::CurlyOpen, 525 | Token::Whitespace(String::from(" ")), 526 | Token::Literal(String::from("echo")), 527 | Token::Whitespace(String::from(" ")), 528 | Token::Literal(String::from("fn body")), 529 | Token::Semi, 530 | Token::CurlyClose, 531 | ]); 532 | match p.command() { 533 | Ok(FunctionDef(..)) => {} 534 | Ok(result) => panic!("Parsed an unexpected command type:\n{:#?}", result), 535 | Err(err) => panic!("Failed to parse command: {}", err), 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /tests/compound_command.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::builder::*; 3 | use conch_parser::ast::CompoundCommandKind::*; 4 | use conch_parser::ast::*; 5 | use conch_parser::parse::ParseError::*; 6 | use conch_parser::token::Token; 7 | 8 | mod parse_support; 9 | use crate::parse_support::*; 10 | 11 | #[test] 12 | fn test_do_group_valid() { 13 | let mut p = make_parser("do foo\nbar; baz\n#comment\n done"); 14 | let correct = CommandGroup { 15 | commands: vec![cmd("foo"), cmd("bar"), cmd("baz")], 16 | trailing_comments: vec![Newline(Some("#comment".into()))], 17 | }; 18 | assert_eq!(correct, p.do_group().unwrap()); 19 | } 20 | 21 | #[test] 22 | fn test_do_group_invalid_missing_separator() { 23 | let mut p = make_parser("do foo\nbar; baz done"); 24 | assert_eq!( 25 | Err(IncompleteCmd("do", src(0, 1, 1), "done", src(20, 2, 14))), 26 | p.do_group() 27 | ); 28 | } 29 | 30 | #[test] 31 | fn test_do_group_valid_keyword_delimited_by_separator() { 32 | let mut p = make_parser("do foo done; done"); 33 | let correct = CommandGroup { 34 | commands: vec![cmd_args("foo", &["done"])], 35 | trailing_comments: vec![], 36 | }; 37 | assert_eq!(correct, p.do_group().unwrap()); 38 | } 39 | 40 | #[test] 41 | fn test_do_group_invalid_missing_keyword() { 42 | let mut p = make_parser("foo\nbar; baz; done"); 43 | assert_eq!( 44 | Err(Unexpected(Token::Name(String::from("foo")), src(0, 1, 1))), 45 | p.do_group() 46 | ); 47 | let mut p = make_parser("do foo\nbar; baz"); 48 | assert_eq!( 49 | Err(IncompleteCmd("do", src(0, 1, 1), "done", src(15, 2, 9))), 50 | p.do_group() 51 | ); 52 | } 53 | 54 | #[test] 55 | fn test_do_group_invalid_quoted() { 56 | let cmds = [ 57 | ( 58 | "'do' foo\nbar; baz; done", 59 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 60 | ), 61 | ( 62 | "do foo\nbar; baz; 'done'", 63 | IncompleteCmd("do", src(0, 1, 1), "done", src(23, 2, 17)), 64 | ), 65 | ( 66 | "\"do\" foo\nbar; baz; done", 67 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 68 | ), 69 | ( 70 | "do foo\nbar; baz; \"done\"", 71 | IncompleteCmd("do", src(0, 1, 1), "done", src(23, 2, 17)), 72 | ), 73 | ]; 74 | 75 | for (c, e) in &cmds { 76 | match make_parser(c).do_group() { 77 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 78 | Err(ref err) => { 79 | if err != e { 80 | panic!( 81 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 82 | c, e, err 83 | ); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | #[test] 91 | fn test_do_group_invalid_concat() { 92 | let mut p = make_parser_from_tokens(vec![ 93 | Token::Literal(String::from("d")), 94 | Token::Literal(String::from("o")), 95 | Token::Newline, 96 | Token::Literal(String::from("foo")), 97 | Token::Newline, 98 | Token::Literal(String::from("done")), 99 | ]); 100 | assert_eq!( 101 | Err(Unexpected(Token::Literal(String::from("d")), src(0, 1, 1))), 102 | p.do_group() 103 | ); 104 | let mut p = make_parser_from_tokens(vec![ 105 | Token::Literal(String::from("do")), 106 | Token::Newline, 107 | Token::Literal(String::from("foo")), 108 | Token::Newline, 109 | Token::Literal(String::from("do")), 110 | Token::Literal(String::from("ne")), 111 | ]); 112 | assert_eq!( 113 | Err(IncompleteCmd("do", src(0, 1, 1), "done", src(11, 3, 5))), 114 | p.do_group() 115 | ); 116 | } 117 | 118 | #[test] 119 | fn test_do_group_should_recognize_literals_and_names() { 120 | for do_tok in vec![ 121 | Token::Literal(String::from("do")), 122 | Token::Name(String::from("do")), 123 | ] { 124 | for done_tok in vec![ 125 | Token::Literal(String::from("done")), 126 | Token::Name(String::from("done")), 127 | ] { 128 | let mut p = make_parser_from_tokens(vec![ 129 | do_tok.clone(), 130 | Token::Newline, 131 | Token::Literal(String::from("foo")), 132 | Token::Newline, 133 | done_tok, 134 | ]); 135 | p.do_group().unwrap(); 136 | } 137 | } 138 | } 139 | 140 | #[test] 141 | fn test_do_group_invalid_missing_body() { 142 | let mut p = make_parser("do\ndone"); 143 | assert_eq!( 144 | Err(Unexpected(Token::Name("done".into()), src(3, 2, 1))), 145 | p.do_group() 146 | ); 147 | } 148 | 149 | #[test] 150 | fn test_compound_command_delegates_valid_commands_brace() { 151 | let correct = CompoundCommand { 152 | kind: Brace(vec![cmd("foo")]), 153 | io: vec![], 154 | }; 155 | assert_eq!(correct, make_parser("{ foo; }").compound_command().unwrap()); 156 | } 157 | 158 | #[test] 159 | fn test_compound_command_delegates_valid_commands_subshell() { 160 | let commands = ["(foo)", "( foo)", " (foo)", "\t(foo)", "\\\n(foo)"]; 161 | 162 | let correct = CompoundCommand { 163 | kind: Subshell(vec![cmd("foo")]), 164 | io: vec![], 165 | }; 166 | 167 | for cmd in &commands { 168 | match make_parser(cmd).compound_command() { 169 | Ok(ref result) if result == &correct => {} 170 | Ok(result) => panic!( 171 | "Parsed \"{}\" as an unexpected command type:\n{:#?}", 172 | cmd, result 173 | ), 174 | Err(err) => panic!("Failed to parse \"{}\": {}", cmd, err), 175 | } 176 | } 177 | } 178 | 179 | #[test] 180 | fn test_compound_command_delegates_valid_commands_while() { 181 | let correct = CompoundCommand { 182 | kind: While(GuardBodyPair { 183 | guard: vec![cmd("guard")], 184 | body: vec![cmd("foo")], 185 | }), 186 | io: vec![], 187 | }; 188 | assert_eq!( 189 | correct, 190 | make_parser("while guard; do foo; done") 191 | .compound_command() 192 | .unwrap() 193 | ); 194 | } 195 | 196 | #[test] 197 | fn test_compound_command_delegates_valid_commands_until() { 198 | let correct = CompoundCommand { 199 | kind: Until(GuardBodyPair { 200 | guard: vec![cmd("guard")], 201 | body: vec![cmd("foo")], 202 | }), 203 | io: vec![], 204 | }; 205 | assert_eq!( 206 | correct, 207 | make_parser("until guard; do foo; done") 208 | .compound_command() 209 | .unwrap() 210 | ); 211 | } 212 | 213 | #[test] 214 | fn test_compound_command_delegates_valid_commands_for() { 215 | let correct = CompoundCommand { 216 | kind: For { 217 | var: String::from("var"), 218 | words: Some(vec![]), 219 | body: vec![cmd("foo")], 220 | }, 221 | io: vec![], 222 | }; 223 | assert_eq!( 224 | correct, 225 | make_parser("for var in; do foo; done") 226 | .compound_command() 227 | .unwrap() 228 | ); 229 | } 230 | 231 | #[test] 232 | fn test_compound_command_delegates_valid_commands_if() { 233 | let correct = CompoundCommand { 234 | kind: If { 235 | conditionals: vec![GuardBodyPair { 236 | guard: vec![cmd("guard")], 237 | body: vec![cmd("body")], 238 | }], 239 | else_branch: None, 240 | }, 241 | io: vec![], 242 | }; 243 | assert_eq!( 244 | correct, 245 | make_parser("if guard; then body; fi") 246 | .compound_command() 247 | .unwrap() 248 | ); 249 | } 250 | 251 | #[test] 252 | fn test_compound_command_delegates_valid_commands_case() { 253 | let correct = CompoundCommand { 254 | kind: Case { 255 | word: word("foo"), 256 | arms: vec![], 257 | }, 258 | io: vec![], 259 | }; 260 | assert_eq!( 261 | correct, 262 | make_parser("case foo in esac").compound_command().unwrap() 263 | ); 264 | } 265 | 266 | #[test] 267 | fn test_compound_command_errors_on_quoted_commands() { 268 | let cases = [ 269 | // { is a reserved word, thus concatenating it essentially "quotes" it 270 | // `compound_command` doesn't know or care enough to specify that "foo" is 271 | // the problematic token instead of {, however, callers who are smart enough 272 | // to expect a brace command would be aware themselves that no such valid 273 | // command actually exists. TL;DR: it's okay for `compound_command` to blame { 274 | ("{foo; }", Unexpected(Token::CurlyOpen, src(0, 1, 1))), 275 | ("'{' foo; }", Unexpected(Token::SingleQuote, src(0, 1, 1))), 276 | ("'(' foo; )", Unexpected(Token::SingleQuote, src(0, 1, 1))), 277 | ( 278 | "'while' guard do foo; done", 279 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 280 | ), 281 | ( 282 | "'until' guard do foo; done", 283 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 284 | ), 285 | ( 286 | "'if' guard; then body; fi", 287 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 288 | ), 289 | ( 290 | "'for' var in; do foo; done", 291 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 292 | ), 293 | ( 294 | "'case' foo in esac", 295 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 296 | ), 297 | ("\"{\" foo; }", Unexpected(Token::DoubleQuote, src(0, 1, 1))), 298 | ("\"(\" foo; )", Unexpected(Token::DoubleQuote, src(0, 1, 1))), 299 | ( 300 | "\"while\" guard do foo; done", 301 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 302 | ), 303 | ( 304 | "\"until\" guard do foo; done", 305 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 306 | ), 307 | ( 308 | "\"if\" guard; then body; fi", 309 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 310 | ), 311 | ( 312 | "\"for\" var in; do foo; done", 313 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 314 | ), 315 | ( 316 | "\"case\" foo in esac", 317 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 318 | ), 319 | ]; 320 | 321 | for &(src, ref e) in &cases { 322 | match make_parser(src).compound_command() { 323 | Ok(result) => panic!( 324 | "Parse::compound_command unexpectedly succeeded parsing \"{}\" with result:\n{:#?}", 325 | src, result 326 | ), 327 | Err(ref err) => { 328 | if err != e { 329 | panic!( 330 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 331 | src, e, err 332 | ); 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | #[test] 340 | fn test_compound_command_captures_redirections_after_command() { 341 | let cases = [ 342 | "{ foo; } 1>>out <& 2 2>&-", 343 | "( foo; ) 1>>out <& 2 2>&-", 344 | "while guard; do foo; done 1>>out <& 2 2>&-", 345 | "until guard; do foo; done 1>>out <& 2 2>&-", 346 | "if guard; then body; fi 1>>out <& 2 2>&-", 347 | "for var in; do foo; done 1>>out <& 2 2>&-", 348 | "case foo in esac 1>>out <& 2 2>&-", 349 | ]; 350 | 351 | for cmd in &cases { 352 | match make_parser(cmd).compound_command() { 353 | Ok(CompoundCommand { io, .. }) => assert_eq!( 354 | io, 355 | vec!( 356 | Redirect::Append(Some(1), word("out")), 357 | Redirect::DupRead(None, word("2")), 358 | Redirect::DupWrite(Some(2), word("-")), 359 | ) 360 | ), 361 | 362 | Err(err) => panic!("Failed to parse \"{}\": {}", cmd, err), 363 | } 364 | } 365 | } 366 | 367 | #[test] 368 | fn test_compound_command_should_delegate_literals_and_names_loop() { 369 | for kw in vec![ 370 | Token::Literal(String::from("while")), 371 | Token::Name(String::from("while")), 372 | Token::Literal(String::from("until")), 373 | Token::Name(String::from("until")), 374 | ] { 375 | let mut p = make_parser_from_tokens(vec![ 376 | kw, 377 | Token::Newline, 378 | Token::Literal(String::from("guard")), 379 | Token::Newline, 380 | Token::Literal(String::from("do")), 381 | Token::Newline, 382 | Token::Literal(String::from("foo")), 383 | Token::Newline, 384 | Token::Literal(String::from("done")), 385 | ]); 386 | p.compound_command().unwrap(); 387 | } 388 | } 389 | 390 | #[test] 391 | fn test_compound_command_should_delegate_literals_and_names_if() { 392 | for if_tok in vec![ 393 | Token::Literal(String::from("if")), 394 | Token::Name(String::from("if")), 395 | ] { 396 | for then_tok in vec![ 397 | Token::Literal(String::from("then")), 398 | Token::Name(String::from("then")), 399 | ] { 400 | for elif_tok in vec![ 401 | Token::Literal(String::from("elif")), 402 | Token::Name(String::from("elif")), 403 | ] { 404 | for else_tok in vec![ 405 | Token::Literal(String::from("else")), 406 | Token::Name(String::from("else")), 407 | ] { 408 | for fi_tok in vec![ 409 | Token::Literal(String::from("fi")), 410 | Token::Name(String::from("fi")), 411 | ] { 412 | let mut p = make_parser_from_tokens(vec![ 413 | if_tok.clone(), 414 | Token::Whitespace(String::from(" ")), 415 | Token::Literal(String::from("guard1")), 416 | Token::Newline, 417 | then_tok.clone(), 418 | Token::Newline, 419 | Token::Literal(String::from("body1")), 420 | elif_tok.clone(), 421 | Token::Whitespace(String::from(" ")), 422 | Token::Literal(String::from("guard2")), 423 | Token::Newline, 424 | then_tok.clone(), 425 | Token::Whitespace(String::from(" ")), 426 | Token::Literal(String::from("body2")), 427 | else_tok.clone(), 428 | Token::Whitespace(String::from(" ")), 429 | Token::Whitespace(String::from(" ")), 430 | Token::Literal(String::from("else part")), 431 | Token::Newline, 432 | fi_tok, 433 | ]); 434 | p.compound_command().unwrap(); 435 | } 436 | } 437 | } 438 | } 439 | } 440 | } 441 | 442 | #[test] 443 | fn test_compound_command_should_delegate_literals_and_names_for() { 444 | for for_tok in vec![ 445 | Token::Literal(String::from("for")), 446 | Token::Name(String::from("for")), 447 | ] { 448 | for in_tok in vec![ 449 | Token::Literal(String::from("in")), 450 | Token::Name(String::from("in")), 451 | ] { 452 | let mut p = make_parser_from_tokens(vec![ 453 | for_tok.clone(), 454 | Token::Whitespace(String::from(" ")), 455 | Token::Name(String::from("var")), 456 | Token::Whitespace(String::from(" ")), 457 | in_tok.clone(), 458 | Token::Whitespace(String::from(" ")), 459 | Token::Literal(String::from("one")), 460 | Token::Whitespace(String::from(" ")), 461 | Token::Literal(String::from("two")), 462 | Token::Whitespace(String::from(" ")), 463 | Token::Literal(String::from("three")), 464 | Token::Whitespace(String::from(" ")), 465 | Token::Newline, 466 | Token::Literal(String::from("do")), 467 | Token::Whitespace(String::from(" ")), 468 | Token::Literal(String::from("echo")), 469 | Token::Whitespace(String::from(" ")), 470 | Token::Dollar, 471 | Token::Name(String::from("var")), 472 | Token::Newline, 473 | Token::Literal(String::from("done")), 474 | ]); 475 | p.compound_command().unwrap(); 476 | } 477 | } 478 | } 479 | 480 | #[test] 481 | fn test_compound_command_should_delegate_literals_and_names_case() { 482 | let case_str = String::from("case"); 483 | let in_str = String::from("in"); 484 | let esac_str = String::from("esac"); 485 | for case_tok in vec![Token::Literal(case_str.clone()), Token::Name(case_str)] { 486 | for in_tok in vec![Token::Literal(in_str.clone()), Token::Name(in_str.clone())] { 487 | for esac_tok in vec![ 488 | Token::Literal(esac_str.clone()), 489 | Token::Name(esac_str.clone()), 490 | ] { 491 | let mut p = make_parser_from_tokens(vec![ 492 | case_tok.clone(), 493 | Token::Whitespace(String::from(" ")), 494 | Token::Literal(String::from("foo")), 495 | Token::Literal(String::from("bar")), 496 | Token::Whitespace(String::from(" ")), 497 | in_tok.clone(), 498 | Token::Whitespace(String::from(" ")), 499 | Token::Literal(String::from("foo")), 500 | Token::ParenClose, 501 | Token::Newline, 502 | Token::Literal(String::from("echo")), 503 | Token::Whitespace(String::from(" ")), 504 | Token::Literal(String::from("foo")), 505 | Token::Newline, 506 | Token::Newline, 507 | Token::DSemi, 508 | esac_tok, 509 | ]); 510 | p.compound_command().unwrap(); 511 | } 512 | } 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /tests/for.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::builder::*; 3 | use conch_parser::parse::ParseError::*; 4 | use conch_parser::token::Token; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_for_command_valid_with_words() { 11 | let mut p = make_parser( 12 | "\ 13 | for var #var comment 14 | #prew1 15 | #prew2 16 | in one two three #word comment 17 | #precmd1 18 | #precmd2 19 | do echo; 20 | #body_comment 21 | done 22 | ", 23 | ); 24 | assert_eq!( 25 | p.for_command(), 26 | Ok(ForFragments { 27 | var: "var".into(), 28 | var_comment: Some(Newline(Some("#var comment".into()))), 29 | words: Some(( 30 | vec!( 31 | Newline(Some("#prew1".into())), 32 | Newline(Some("#prew2".into())), 33 | ), 34 | vec!(word("one"), word("two"), word("three"),), 35 | Some(Newline(Some("#word comment".into()))) 36 | )), 37 | pre_body_comments: vec!( 38 | Newline(Some("#precmd1".into())), 39 | Newline(Some("#precmd2".into())), 40 | ), 41 | body: CommandGroup { 42 | commands: vec!(cmd("echo")), 43 | trailing_comments: vec!(Newline(Some("#body_comment".into()))), 44 | }, 45 | }) 46 | ); 47 | } 48 | 49 | #[test] 50 | fn test_for_command_valid_without_words() { 51 | let mut p = make_parser( 52 | "\ 53 | for var #var comment 54 | #w1 55 | #w2 56 | do echo; 57 | #body_comment 58 | done 59 | ", 60 | ); 61 | assert_eq!( 62 | p.for_command(), 63 | Ok(ForFragments { 64 | var: "var".into(), 65 | var_comment: Some(Newline(Some("#var comment".into()))), 66 | words: None, 67 | pre_body_comments: vec!(Newline(Some("#w1".into())), Newline(Some("#w2".into())),), 68 | body: CommandGroup { 69 | commands: vec!(cmd("echo")), 70 | trailing_comments: vec!(Newline(Some("#body_comment".into()))), 71 | }, 72 | }) 73 | ); 74 | } 75 | 76 | #[test] 77 | fn test_for_command_valid_separators() { 78 | let cases = vec![ 79 | "for var do body; done", 80 | "for var ; do body; done", 81 | "for var ;\n do body; done", 82 | "for var\n do body; done", 83 | "for var\n in ; do body; done", 84 | "for var\n in ;\n do body; done", 85 | "for var\n in \n do body; done", 86 | "for var in ; do body; done", 87 | "for var in ;\n do body; done", 88 | "for var in \n do body; done", 89 | "for var\n in one two; do body; done", 90 | "for var\n in one two;\n do body; done", 91 | "for var\n in one two \n do body; done", 92 | "for var in one two; do body; done", 93 | "for var in one two;\n do body; done", 94 | "for var in one two \n do body; done", 95 | ]; 96 | 97 | for src in cases { 98 | match make_parser(src).for_command() { 99 | Ok(_) => {} 100 | e @ Err(_) => panic!("expected `{}` to parse successfully, but got: {:?}", src, e), 101 | } 102 | } 103 | } 104 | 105 | #[test] 106 | fn test_for_command_valid_with_separator() { 107 | let mut p = make_parser("for var in one two three\ndo echo $var; done"); 108 | p.for_command().unwrap(); 109 | let mut p = make_parser("for var in one two three;do echo $var; done"); 110 | p.for_command().unwrap(); 111 | } 112 | 113 | #[test] 114 | fn test_for_command_invalid_with_in_no_words_no_with_separator() { 115 | let mut p = make_parser("for var in do echo $var; done"); 116 | assert_eq!( 117 | Err(IncompleteCmd("for", src(0, 1, 1), "do", src(25, 1, 26))), 118 | p.for_command() 119 | ); 120 | } 121 | 122 | #[test] 123 | fn test_for_command_invalid_missing_separator() { 124 | let mut p = make_parser("for var in one two three do echo $var; done"); 125 | assert_eq!( 126 | Err(IncompleteCmd("for", src(0, 1, 1), "do", src(39, 1, 40))), 127 | p.for_command() 128 | ); 129 | } 130 | 131 | #[test] 132 | fn test_for_command_invalid_amp_not_valid_separator() { 133 | let mut p = make_parser("for var in one two three& do echo $var; done"); 134 | assert_eq!(Err(Unexpected(Token::Amp, src(24, 1, 25))), p.for_command()); 135 | } 136 | 137 | #[test] 138 | fn test_for_command_invalid_missing_keyword() { 139 | let mut p = make_parser("var in one two three\ndo echo $var; done"); 140 | assert_eq!( 141 | Err(Unexpected(Token::Name(String::from("var")), src(0, 1, 1))), 142 | p.for_command() 143 | ); 144 | } 145 | 146 | #[test] 147 | fn test_for_command_invalid_missing_var() { 148 | let mut p = make_parser("for in one two three\ndo echo $var; done"); 149 | assert_eq!( 150 | Err(IncompleteCmd("for", src(0, 1, 1), "in", src(7, 1, 8))), 151 | p.for_command() 152 | ); 153 | } 154 | 155 | #[test] 156 | fn test_for_command_invalid_missing_body() { 157 | let mut p = make_parser("for var in one two three\n"); 158 | assert_eq!( 159 | Err(IncompleteCmd("for", src(0, 1, 1), "do", src(25, 2, 1))), 160 | p.for_command() 161 | ); 162 | } 163 | 164 | #[test] 165 | fn test_for_command_invalid_quoted() { 166 | let cmds = [ 167 | ( 168 | "'for' var in one two three\ndo echo $var; done", 169 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 170 | ), 171 | ( 172 | "for var 'in' one two three\ndo echo $var; done", 173 | IncompleteCmd("for", src(0, 1, 1), "in", src(8, 1, 9)), 174 | ), 175 | ( 176 | "\"for\" var in one two three\ndo echo $var; done", 177 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 178 | ), 179 | ( 180 | "for var \"in\" one two three\ndo echo $var; done", 181 | IncompleteCmd("for", src(0, 1, 1), "in", src(8, 1, 9)), 182 | ), 183 | ]; 184 | 185 | for (c, e) in &cmds { 186 | match make_parser(c).for_command() { 187 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 188 | Err(ref err) => { 189 | if err != e { 190 | panic!( 191 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 192 | c, e, err 193 | ); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | 200 | #[test] 201 | fn test_for_command_invalid_var_must_be_name() { 202 | let mut p = make_parser("for 123var in one two three\ndo echo $var; done"); 203 | assert_eq!( 204 | Err(BadIdent(String::from("123var"), src(4, 1, 5))), 205 | p.for_command() 206 | ); 207 | let mut p = make_parser("for 'var' in one two three\ndo echo $var; done"); 208 | assert_eq!( 209 | Err(Unexpected(Token::SingleQuote, src(4, 1, 5))), 210 | p.for_command() 211 | ); 212 | let mut p = make_parser("for \"var\" in one two three\ndo echo $var; done"); 213 | assert_eq!( 214 | Err(Unexpected(Token::DoubleQuote, src(4, 1, 5))), 215 | p.for_command() 216 | ); 217 | let mut p = make_parser("for var*% in one two three\ndo echo $var; done"); 218 | assert_eq!( 219 | Err(IncompleteCmd("for", src(0, 1, 1), "in", src(7, 1, 8))), 220 | p.for_command() 221 | ); 222 | } 223 | 224 | #[test] 225 | fn test_for_command_invalid_concat() { 226 | let mut p = make_parser_from_tokens(vec![ 227 | Token::Literal(String::from("fo")), 228 | Token::Literal(String::from("r")), 229 | Token::Whitespace(String::from(" ")), 230 | Token::Name(String::from("var")), 231 | Token::Whitespace(String::from(" ")), 232 | Token::Literal(String::from("in")), 233 | Token::Literal(String::from("one")), 234 | Token::Whitespace(String::from(" ")), 235 | Token::Literal(String::from("two")), 236 | Token::Whitespace(String::from(" ")), 237 | Token::Literal(String::from("three")), 238 | Token::Whitespace(String::from(" ")), 239 | Token::Newline, 240 | Token::Literal(String::from("do")), 241 | Token::Whitespace(String::from(" ")), 242 | Token::Literal(String::from("echo")), 243 | Token::Whitespace(String::from(" ")), 244 | Token::Dollar, 245 | Token::Literal(String::from("var")), 246 | Token::Newline, 247 | Token::Literal(String::from("done")), 248 | ]); 249 | assert_eq!( 250 | Err(Unexpected(Token::Literal(String::from("fo")), src(0, 1, 1))), 251 | p.for_command() 252 | ); 253 | 254 | let mut p = make_parser_from_tokens(vec![ 255 | Token::Literal(String::from("for")), 256 | Token::Whitespace(String::from(" ")), 257 | Token::Name(String::from("var")), 258 | Token::Whitespace(String::from(" ")), 259 | Token::Literal(String::from("i")), 260 | Token::Literal(String::from("n")), 261 | Token::Literal(String::from("one")), 262 | Token::Whitespace(String::from(" ")), 263 | Token::Literal(String::from("two")), 264 | Token::Whitespace(String::from(" ")), 265 | Token::Literal(String::from("three")), 266 | Token::Whitespace(String::from(" ")), 267 | Token::Newline, 268 | Token::Literal(String::from("do")), 269 | Token::Whitespace(String::from(" ")), 270 | Token::Literal(String::from("echo")), 271 | Token::Whitespace(String::from(" ")), 272 | Token::Dollar, 273 | Token::Literal(String::from("var")), 274 | Token::Newline, 275 | Token::Literal(String::from("done")), 276 | ]); 277 | assert_eq!( 278 | Err(IncompleteCmd("for", src(0, 1, 1), "in", src(8, 1, 9))), 279 | p.for_command() 280 | ); 281 | } 282 | 283 | #[test] 284 | fn test_for_command_should_recognize_literals_and_names() { 285 | for for_tok in vec![ 286 | Token::Literal(String::from("for")), 287 | Token::Name(String::from("for")), 288 | ] { 289 | for in_tok in vec![ 290 | Token::Literal(String::from("in")), 291 | Token::Name(String::from("in")), 292 | ] { 293 | let mut p = make_parser_from_tokens(vec![ 294 | for_tok.clone(), 295 | Token::Whitespace(String::from(" ")), 296 | Token::Name(String::from("var")), 297 | Token::Whitespace(String::from(" ")), 298 | in_tok.clone(), 299 | Token::Whitespace(String::from(" ")), 300 | Token::Literal(String::from("one")), 301 | Token::Whitespace(String::from(" ")), 302 | Token::Literal(String::from("two")), 303 | Token::Whitespace(String::from(" ")), 304 | Token::Literal(String::from("three")), 305 | Token::Whitespace(String::from(" ")), 306 | Token::Newline, 307 | Token::Literal(String::from("do")), 308 | Token::Whitespace(String::from(" ")), 309 | Token::Literal(String::from("echo")), 310 | Token::Whitespace(String::from(" ")), 311 | Token::Dollar, 312 | Token::Name(String::from("var")), 313 | Token::Newline, 314 | Token::Literal(String::from("done")), 315 | ]); 316 | p.for_command().unwrap(); 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /tests/function.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::CompoundCommandKind::*; 3 | use conch_parser::ast::PipeableCommand::*; 4 | use conch_parser::ast::*; 5 | use conch_parser::parse::ParseError::*; 6 | use conch_parser::token::Token; 7 | 8 | use std::rc::Rc; 9 | 10 | mod parse_support; 11 | use crate::parse_support::*; 12 | 13 | #[test] 14 | fn test_function_declaration_valid() { 15 | let correct = FunctionDef( 16 | String::from("foo"), 17 | Rc::new(CompoundCommand { 18 | kind: Brace(vec![cmd_args("echo", &["body"])]), 19 | io: vec![], 20 | }), 21 | ); 22 | 23 | assert_eq!( 24 | correct, 25 | make_parser("function foo() { echo body; }") 26 | .function_declaration() 27 | .unwrap() 28 | ); 29 | assert_eq!( 30 | correct, 31 | make_parser("function foo () { echo body; }") 32 | .function_declaration() 33 | .unwrap() 34 | ); 35 | assert_eq!( 36 | correct, 37 | make_parser("function foo ( ) { echo body; }") 38 | .function_declaration() 39 | .unwrap() 40 | ); 41 | assert_eq!( 42 | correct, 43 | make_parser("function foo( ) { echo body; }") 44 | .function_declaration() 45 | .unwrap() 46 | ); 47 | assert_eq!( 48 | correct, 49 | make_parser("function foo { echo body; }") 50 | .function_declaration() 51 | .unwrap() 52 | ); 53 | assert_eq!( 54 | correct, 55 | make_parser("foo() { echo body; }") 56 | .function_declaration() 57 | .unwrap() 58 | ); 59 | assert_eq!( 60 | correct, 61 | make_parser("foo () { echo body; }") 62 | .function_declaration() 63 | .unwrap() 64 | ); 65 | assert_eq!( 66 | correct, 67 | make_parser("foo ( ) { echo body; }") 68 | .function_declaration() 69 | .unwrap() 70 | ); 71 | assert_eq!( 72 | correct, 73 | make_parser("foo( ) { echo body; }") 74 | .function_declaration() 75 | .unwrap() 76 | ); 77 | 78 | assert_eq!( 79 | correct, 80 | make_parser("function foo() \n{ echo body; }") 81 | .function_declaration() 82 | .unwrap() 83 | ); 84 | assert_eq!( 85 | correct, 86 | make_parser("function foo () \n{ echo body; }") 87 | .function_declaration() 88 | .unwrap() 89 | ); 90 | assert_eq!( 91 | correct, 92 | make_parser("function foo ( )\n{ echo body; }") 93 | .function_declaration() 94 | .unwrap() 95 | ); 96 | assert_eq!( 97 | correct, 98 | make_parser("function foo( ) \n{ echo body; }") 99 | .function_declaration() 100 | .unwrap() 101 | ); 102 | assert_eq!( 103 | correct, 104 | make_parser("function foo \n{ echo body; }") 105 | .function_declaration() 106 | .unwrap() 107 | ); 108 | assert_eq!( 109 | correct, 110 | make_parser("foo() \n{ echo body; }") 111 | .function_declaration() 112 | .unwrap() 113 | ); 114 | assert_eq!( 115 | correct, 116 | make_parser("foo () \n{ echo body; }") 117 | .function_declaration() 118 | .unwrap() 119 | ); 120 | assert_eq!( 121 | correct, 122 | make_parser("foo ( ) \n{ echo body; }") 123 | .function_declaration() 124 | .unwrap() 125 | ); 126 | assert_eq!( 127 | correct, 128 | make_parser("foo( ) \n{ echo body; }") 129 | .function_declaration() 130 | .unwrap() 131 | ); 132 | } 133 | 134 | #[test] 135 | fn test_function_declaration_valid_body_need_not_be_a_compound_command() { 136 | let src = vec![ 137 | ("function foo() echo body;", src(20, 1, 21)), 138 | ("function foo () echo body;", src(20, 1, 21)), 139 | ("function foo ( ) echo body;", src(20, 1, 21)), 140 | ("function foo( ) echo body;", src(20, 1, 21)), 141 | ("function foo echo body;", src(20, 1, 21)), 142 | ("foo() echo body;", src(20, 1, 21)), 143 | ("foo () echo body;", src(20, 1, 21)), 144 | ("foo ( ) echo body;", src(20, 1, 21)), 145 | ("foo( ) echo body;", src(20, 1, 21)), 146 | ("function foo() \necho body;", src(20, 2, 1)), 147 | ("function foo () \necho body;", src(20, 2, 1)), 148 | ("function foo ( )\necho body;", src(20, 2, 1)), 149 | ("function foo( ) \necho body;", src(20, 2, 1)), 150 | ("function foo \necho body;", src(20, 2, 1)), 151 | ("foo() \necho body;", src(20, 2, 1)), 152 | ("foo () \necho body;", src(20, 2, 1)), 153 | ("foo ( ) \necho body;", src(20, 2, 1)), 154 | ("foo( ) \necho body;", src(20, 2, 1)), 155 | ]; 156 | 157 | for (s, p) in src { 158 | let correct = Unexpected(Token::Name(String::from("echo")), p); 159 | match make_parser(s).function_declaration() { 160 | Ok(w) => panic!("Unexpectedly parsed the source \"{}\" as\n{:?}", s, w), 161 | Err(ref err) => { 162 | if err != &correct { 163 | panic!( 164 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 165 | s, correct, err 166 | ); 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | #[test] 174 | fn test_function_declaration_parens_can_be_subshell_if_function_keyword_present() { 175 | let correct = FunctionDef( 176 | String::from("foo"), 177 | Rc::new(CompoundCommand { 178 | kind: Subshell(vec![cmd_args("echo", &["subshell"])]), 179 | io: vec![], 180 | }), 181 | ); 182 | 183 | assert_eq!( 184 | correct, 185 | make_parser("function foo (echo subshell)") 186 | .function_declaration() 187 | .unwrap() 188 | ); 189 | assert_eq!( 190 | correct, 191 | make_parser("function foo() (echo subshell)") 192 | .function_declaration() 193 | .unwrap() 194 | ); 195 | assert_eq!( 196 | correct, 197 | make_parser("function foo () (echo subshell)") 198 | .function_declaration() 199 | .unwrap() 200 | ); 201 | assert_eq!( 202 | correct, 203 | make_parser("function foo\n(echo subshell)") 204 | .function_declaration() 205 | .unwrap() 206 | ); 207 | } 208 | 209 | #[test] 210 | fn test_function_declaration_invalid_newline_in_declaration() { 211 | let mut p = make_parser("function\nname() { echo body; }"); 212 | assert_eq!( 213 | Err(Unexpected(Token::Newline, src(8, 1, 9))), 214 | p.function_declaration() 215 | ); 216 | // If the function keyword is present the () are optional, and at this particular point 217 | // they become an empty subshell (which is invalid) 218 | let mut p = make_parser("function name\n() { echo body; }"); 219 | assert_eq!( 220 | Err(Unexpected(Token::ParenClose, src(15, 2, 2))), 221 | p.function_declaration() 222 | ); 223 | } 224 | 225 | #[test] 226 | fn test_function_declaration_invalid_missing_space_after_fn_keyword_and_no_parens() { 227 | let mut p = make_parser("functionname { echo body; }"); 228 | assert_eq!( 229 | Err(Unexpected(Token::CurlyOpen, src(13, 1, 14))), 230 | p.function_declaration() 231 | ); 232 | } 233 | 234 | #[test] 235 | fn test_function_declaration_invalid_missing_fn_keyword_and_parens() { 236 | let mut p = make_parser("name { echo body; }"); 237 | assert_eq!( 238 | Err(Unexpected(Token::CurlyOpen, src(5, 1, 6))), 239 | p.function_declaration() 240 | ); 241 | } 242 | 243 | #[test] 244 | fn test_function_declaration_invalid_missing_space_after_name_no_parens() { 245 | let mut p = make_parser("function name{ echo body; }"); 246 | assert_eq!( 247 | Err(Unexpected(Token::CurlyOpen, src(13, 1, 14))), 248 | p.function_declaration() 249 | ); 250 | let mut p = make_parser("function name( echo body; )"); 251 | assert_eq!( 252 | Err(Unexpected( 253 | Token::Name(String::from("echo")), 254 | src(15, 1, 16) 255 | )), 256 | p.function_declaration() 257 | ); 258 | } 259 | 260 | #[test] 261 | fn test_function_declaration_invalid_missing_name() { 262 | let mut p = make_parser("function { echo body; }"); 263 | assert_eq!( 264 | Err(Unexpected(Token::CurlyOpen, src(9, 1, 10))), 265 | p.function_declaration() 266 | ); 267 | let mut p = make_parser("function () { echo body; }"); 268 | assert_eq!( 269 | Err(Unexpected(Token::ParenOpen, src(9, 1, 10))), 270 | p.function_declaration() 271 | ); 272 | let mut p = make_parser("() { echo body; }"); 273 | assert_eq!( 274 | Err(Unexpected(Token::ParenOpen, src(0, 1, 1))), 275 | p.function_declaration() 276 | ); 277 | } 278 | 279 | #[test] 280 | fn test_function_declaration_invalid_missing_body() { 281 | let mut p = make_parser("function name"); 282 | assert_eq!(Err(UnexpectedEOF), p.function_declaration()); 283 | let mut p = make_parser("function name()"); 284 | assert_eq!(Err(UnexpectedEOF), p.function_declaration()); 285 | let mut p = make_parser("name()"); 286 | assert_eq!(Err(UnexpectedEOF), p.function_declaration()); 287 | } 288 | 289 | #[test] 290 | fn test_function_declaration_invalid_quoted() { 291 | let cmds = [ 292 | ( 293 | "'function' name { echo body; }", 294 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 295 | ), 296 | ( 297 | "function 'name'() { echo body; }", 298 | Unexpected(Token::SingleQuote, src(9, 1, 10)), 299 | ), 300 | ( 301 | "name'()' { echo body; }", 302 | Unexpected(Token::SingleQuote, src(4, 1, 5)), 303 | ), 304 | ( 305 | "\"function\" name { echo body; }", 306 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 307 | ), 308 | ( 309 | "function \"name\"() { echo body; }", 310 | Unexpected(Token::DoubleQuote, src(9, 1, 10)), 311 | ), 312 | ( 313 | "name\"()\" { echo body; }", 314 | Unexpected(Token::DoubleQuote, src(4, 1, 5)), 315 | ), 316 | ]; 317 | 318 | for (c, e) in &cmds { 319 | match make_parser(c).function_declaration() { 320 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 321 | Err(ref err) => { 322 | if err != e { 323 | panic!( 324 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 325 | c, e, err 326 | ); 327 | } 328 | } 329 | } 330 | } 331 | } 332 | 333 | #[test] 334 | fn test_function_declaration_invalid_fn_must_be_name() { 335 | let mut p = make_parser("function 123fn { echo body; }"); 336 | assert_eq!( 337 | Err(BadIdent(String::from("123fn"), src(9, 1, 10))), 338 | p.function_declaration() 339 | ); 340 | let mut p = make_parser("function 123fn() { echo body; }"); 341 | assert_eq!( 342 | Err(BadIdent(String::from("123fn"), src(9, 1, 10))), 343 | p.function_declaration() 344 | ); 345 | let mut p = make_parser("123fn() { echo body; }"); 346 | assert_eq!( 347 | Err(BadIdent(String::from("123fn"), src(0, 1, 1))), 348 | p.function_declaration() 349 | ); 350 | } 351 | 352 | #[test] 353 | fn test_function_declaration_invalid_fn_name_must_be_name_token() { 354 | let mut p = make_parser_from_tokens(vec![ 355 | Token::Literal(String::from("function")), 356 | Token::Whitespace(String::from(" ")), 357 | Token::Literal(String::from("fn_name")), 358 | Token::Whitespace(String::from(" ")), 359 | Token::ParenOpen, 360 | Token::ParenClose, 361 | Token::Whitespace(String::from(" ")), 362 | Token::CurlyOpen, 363 | Token::Whitespace(String::from(" ")), 364 | Token::Literal(String::from("echo")), 365 | Token::Whitespace(String::from(" ")), 366 | Token::Literal(String::from("fn body")), 367 | Token::Semi, 368 | Token::CurlyClose, 369 | ]); 370 | assert_eq!( 371 | Err(BadIdent(String::from("fn_name"), src(9, 1, 10))), 372 | p.function_declaration() 373 | ); 374 | 375 | let mut p = make_parser_from_tokens(vec![ 376 | Token::Literal(String::from("function")), 377 | Token::Whitespace(String::from(" ")), 378 | Token::Name(String::from("fn_name")), 379 | Token::Whitespace(String::from(" ")), 380 | Token::ParenOpen, 381 | Token::ParenClose, 382 | Token::Whitespace(String::from(" ")), 383 | Token::CurlyOpen, 384 | Token::Whitespace(String::from(" ")), 385 | Token::Literal(String::from("echo")), 386 | Token::Whitespace(String::from(" ")), 387 | Token::Literal(String::from("fn body")), 388 | Token::Semi, 389 | Token::CurlyClose, 390 | ]); 391 | p.function_declaration().unwrap(); 392 | } 393 | 394 | #[test] 395 | fn test_function_declaration_invalid_concat() { 396 | let mut p = make_parser_from_tokens(vec![ 397 | Token::Literal(String::from("func")), 398 | Token::Literal(String::from("tion")), 399 | Token::Whitespace(String::from(" ")), 400 | Token::Name(String::from("fn_name")), 401 | Token::Whitespace(String::from(" ")), 402 | Token::ParenOpen, 403 | Token::ParenClose, 404 | Token::Whitespace(String::from(" ")), 405 | Token::CurlyOpen, 406 | Token::Literal(String::from("echo")), 407 | Token::Whitespace(String::from(" ")), 408 | Token::Literal(String::from("fn body")), 409 | Token::Semi, 410 | Token::CurlyClose, 411 | ]); 412 | assert_eq!( 413 | Err(BadIdent(String::from("func"), src(0, 1, 1))), 414 | p.function_declaration() 415 | ); 416 | } 417 | 418 | #[test] 419 | fn test_function_declaration_should_recognize_literals_and_names_for_fn_keyword() { 420 | for fn_tok in vec![ 421 | Token::Literal(String::from("function")), 422 | Token::Name(String::from("function")), 423 | ] { 424 | let mut p = make_parser_from_tokens(vec![ 425 | fn_tok, 426 | Token::Whitespace(String::from(" ")), 427 | Token::Name(String::from("fn_name")), 428 | Token::Whitespace(String::from(" ")), 429 | Token::ParenOpen, 430 | Token::ParenClose, 431 | Token::Whitespace(String::from(" ")), 432 | Token::CurlyOpen, 433 | Token::Whitespace(String::from(" ")), 434 | Token::Literal(String::from("echo")), 435 | Token::Whitespace(String::from(" ")), 436 | Token::Literal(String::from("fn body")), 437 | Token::Semi, 438 | Token::CurlyClose, 439 | ]); 440 | p.function_declaration().unwrap(); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /tests/heredoc.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::ComplexWord::*; 3 | use conch_parser::ast::Redirect::Heredoc; 4 | use conch_parser::ast::SimpleWord::*; 5 | use conch_parser::ast::*; 6 | use conch_parser::parse::ParseError::*; 7 | use conch_parser::token::Token; 8 | 9 | mod parse_support; 10 | use crate::parse_support::*; 11 | 12 | fn cat_heredoc(fd: Option, body: &'static str) -> TopLevelCommand { 13 | cmd_from_simple(SimpleCommand { 14 | redirects_or_env_vars: vec![], 15 | redirects_or_cmd_words: vec![ 16 | RedirectOrCmdWord::CmdWord(word("cat")), 17 | RedirectOrCmdWord::Redirect(Heredoc(fd, word(body))), 18 | ], 19 | }) 20 | } 21 | 22 | #[test] 23 | fn test_heredoc_valid() { 24 | let correct = Some(cat_heredoc(None, "hello\n")); 25 | assert_eq!( 26 | correct, 27 | make_parser("cat < panic!("Unexpectedly parsed \"{}\" as\n{:#?}", s, result), 182 | Err(ref err) => { 183 | if err != e { 184 | panic!( 185 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 186 | s, e, err 187 | ); 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | #[test] 195 | fn test_if_command_invalid_concat() { 196 | let mut p = make_parser_from_tokens(vec![ 197 | Token::Literal(String::from("i")), 198 | Token::Literal(String::from("f")), 199 | Token::Newline, 200 | Token::Literal(String::from("guard1")), 201 | Token::Newline, 202 | Token::Literal(String::from("then")), 203 | Token::Newline, 204 | Token::Literal(String::from("body1")), 205 | Token::Newline, 206 | Token::Literal(String::from("elif")), 207 | Token::Newline, 208 | Token::Literal(String::from("guard2")), 209 | Token::Newline, 210 | Token::Literal(String::from("then")), 211 | Token::Newline, 212 | Token::Literal(String::from("body2")), 213 | Token::Newline, 214 | Token::Literal(String::from("else")), 215 | Token::Newline, 216 | Token::Literal(String::from("else part")), 217 | Token::Newline, 218 | Token::Literal(String::from("fi")), 219 | ]); 220 | assert_eq!( 221 | Err(Unexpected(Token::Literal(String::from("i")), src(0, 1, 1))), 222 | p.if_command() 223 | ); 224 | 225 | // Splitting up `then`, `elif`, and `else` tokens makes them 226 | // get interpreted as arguments, so an explicit error may not be raised 227 | // although the command will be malformed 228 | 229 | let mut p = make_parser_from_tokens(vec![ 230 | Token::Literal(String::from("if")), 231 | Token::Newline, 232 | Token::Literal(String::from("guard1")), 233 | Token::Newline, 234 | Token::Literal(String::from("then")), 235 | Token::Newline, 236 | Token::Literal(String::from("body1")), 237 | Token::Newline, 238 | Token::Literal(String::from("elif")), 239 | Token::Newline, 240 | Token::Literal(String::from("guard2")), 241 | Token::Newline, 242 | Token::Literal(String::from("then")), 243 | Token::Newline, 244 | Token::Literal(String::from("body2")), 245 | Token::Newline, 246 | Token::Literal(String::from("else")), 247 | Token::Newline, 248 | Token::Literal(String::from("else part")), 249 | Token::Newline, 250 | Token::Literal(String::from("f")), 251 | Token::Literal(String::from("i")), 252 | ]); 253 | assert_eq!( 254 | Err(IncompleteCmd("if", src(0, 1, 1), "fi", src(61, 11, 3))), 255 | p.if_command() 256 | ); 257 | } 258 | 259 | #[test] 260 | fn test_if_command_should_recognize_literals_and_names() { 261 | for if_tok in vec![ 262 | Token::Literal(String::from("if")), 263 | Token::Name(String::from("if")), 264 | ] { 265 | for then_tok in vec![ 266 | Token::Literal(String::from("then")), 267 | Token::Name(String::from("then")), 268 | ] { 269 | for elif_tok in vec![ 270 | Token::Literal(String::from("elif")), 271 | Token::Name(String::from("elif")), 272 | ] { 273 | for else_tok in vec![ 274 | Token::Literal(String::from("else")), 275 | Token::Name(String::from("else")), 276 | ] { 277 | for fi_tok in vec![ 278 | Token::Literal(String::from("fi")), 279 | Token::Name(String::from("fi")), 280 | ] { 281 | let mut p = make_parser_from_tokens(vec![ 282 | if_tok.clone(), 283 | Token::Whitespace(String::from(" ")), 284 | Token::Literal(String::from("guard1")), 285 | Token::Newline, 286 | then_tok.clone(), 287 | Token::Newline, 288 | Token::Literal(String::from("body1")), 289 | elif_tok.clone(), 290 | Token::Whitespace(String::from(" ")), 291 | Token::Literal(String::from("guard2")), 292 | Token::Newline, 293 | then_tok.clone(), 294 | Token::Whitespace(String::from(" ")), 295 | Token::Literal(String::from("body2")), 296 | else_tok.clone(), 297 | Token::Whitespace(String::from(" ")), 298 | Token::Whitespace(String::from(" ")), 299 | Token::Literal(String::from("else part")), 300 | Token::Newline, 301 | fi_tok, 302 | ]); 303 | p.if_command().unwrap(); 304 | } 305 | } 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /tests/lexer.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::lexer::Lexer; 3 | use conch_parser::token::Token::*; 4 | use conch_parser::token::{Positional, Token}; 5 | 6 | macro_rules! check_tok { 7 | ($fn_name:ident, $tok:expr) => { 8 | #[test] 9 | #[allow(non_snake_case)] 10 | fn $fn_name() { 11 | let s = format!("{}", $tok); 12 | let mut lex = Lexer::new(s.chars()); 13 | assert_eq!($tok, lex.next().unwrap()); 14 | } 15 | }; 16 | } 17 | 18 | macro_rules! lex_str { 19 | ($fn_name:ident, $s:expr, $($tok:expr),+ ) => { 20 | #[test] 21 | #[allow(non_snake_case)] 22 | fn $fn_name() { 23 | let lex = Lexer::new($s.chars()); 24 | let tokens: Vec = lex.collect(); 25 | assert_eq!(tokens, vec!( $($tok),+ )); 26 | } 27 | } 28 | } 29 | 30 | check_tok!(check_Newline, Newline); 31 | check_tok!(check_ParenOpen, ParenOpen); 32 | check_tok!(check_ParenClose, ParenClose); 33 | check_tok!(check_CurlyOpen, CurlyOpen); 34 | check_tok!(check_CurlyClose, CurlyClose); 35 | check_tok!(check_SquareOpen, SquareOpen); 36 | check_tok!(check_SquareClose, SquareClose); 37 | check_tok!(check_Dollar, Dollar); 38 | check_tok!(check_Bang, Bang); 39 | check_tok!(check_Semi, Semi); 40 | check_tok!(check_Amp, Amp); 41 | check_tok!(check_Less, Less); 42 | check_tok!(check_Great, Great); 43 | check_tok!(check_Pipe, Pipe); 44 | check_tok!(check_Tilde, Tilde); 45 | check_tok!(check_Star, Star); 46 | check_tok!(check_Question, Question); 47 | check_tok!(check_Percent, Percent); 48 | check_tok!(check_Dash, Dash); 49 | check_tok!(check_Equals, Equals); 50 | check_tok!(check_Plus, Plus); 51 | check_tok!(check_Colon, Colon); 52 | check_tok!(check_At, At); 53 | check_tok!(check_Caret, Caret); 54 | check_tok!(check_Slash, Slash); 55 | check_tok!(check_Comma, Comma); 56 | check_tok!(check_Pound, Pound); 57 | check_tok!(check_DoubleQuote, DoubleQuote); 58 | check_tok!(check_Backtick, Backtick); 59 | check_tok!(check_AndIf, AndIf); 60 | check_tok!(check_OrIf, OrIf); 61 | check_tok!(check_DSemi, DSemi); 62 | check_tok!(check_DLess, DLess); 63 | check_tok!(check_DGreat, DGreat); 64 | check_tok!(check_GreatAnd, GreatAnd); 65 | check_tok!(check_LessAnd, LessAnd); 66 | check_tok!(check_DLessDash, DLessDash); 67 | check_tok!(check_Clobber, Clobber); 68 | check_tok!(check_LessGreat, LessGreat); 69 | check_tok!(check_Whitespace, Whitespace(String::from(" \t\r"))); 70 | check_tok!(check_Name, Name(String::from("abc_23_defg"))); 71 | check_tok!(check_Literal, Literal(String::from("5abcdefg80hijklmnop"))); 72 | check_tok!(check_ParamPositional, ParamPositional(Positional::Nine)); 73 | 74 | lex_str!(check_greedy_Amp, "&&&", AndIf, Amp); 75 | lex_str!(check_greedy_Pipe, "|||", OrIf, Pipe); 76 | lex_str!(check_greedy_Semi, ";;;", DSemi, Semi); 77 | lex_str!(check_greedy_Less, "<<<", DLess, Less); 78 | lex_str!(check_greedy_Great, ">>>", DGreat, Great); 79 | lex_str!(check_greedy_Less2, "<<<-", DLess, Less, Dash); 80 | 81 | lex_str!( 82 | check_bad_Assigmnent_and_value, 83 | "5foobar=test", 84 | Literal(String::from("5foobar")), 85 | Equals, 86 | Name(String::from("test")) 87 | ); 88 | 89 | lex_str!( 90 | check_Literal_and_Name_combo, 91 | "hello 5asdf5_ 6world __name ^.abc _test2", 92 | Name(String::from("hello")), 93 | Whitespace(String::from(" ")), 94 | Literal(String::from("5asdf5_")), 95 | Whitespace(String::from(" ")), 96 | Literal(String::from("6world")), 97 | Whitespace(String::from(" ")), 98 | Name(String::from("__name")), 99 | Whitespace(String::from(" ")), 100 | Caret, 101 | Literal(String::from(".abc")), 102 | Whitespace(String::from(" ")), 103 | Name(String::from("_test2")) 104 | ); 105 | 106 | lex_str!(check_escape_Backslash, "\\\\", Backslash, Backslash); 107 | lex_str!(check_escape_AndIf, "\\&&", Backslash, Amp, Amp); 108 | lex_str!(check_escape_DSemi, "\\;;", Backslash, Semi, Semi); 109 | lex_str!(check_escape_DLess, "\\<<", Backslash, Less, Less); 110 | lex_str!(check_escape_DLessDash, "\\<<-", Backslash, Less, Less, Dash); 111 | lex_str!( 112 | check_escape_ParamPositional, 113 | "\\$0", 114 | Backslash, 115 | Dollar, 116 | Literal(String::from("0")) 117 | ); 118 | lex_str!( 119 | check_escape_Whitespace, 120 | "\\ ", 121 | Backslash, 122 | Whitespace(String::from(" ")), 123 | Whitespace(String::from(" ")) 124 | ); 125 | lex_str!( 126 | check_escape_Name, 127 | "\\ab", 128 | Backslash, 129 | Name(String::from("a")), 130 | Name(String::from("b")) 131 | ); 132 | lex_str!( 133 | check_escape_Literal, 134 | "\\13", 135 | Backslash, 136 | Literal(String::from("1")), 137 | Literal(String::from("3")) 138 | ); 139 | 140 | lex_str!( 141 | check_no_tokens_lost, 142 | "word\\'", 143 | Name(String::from("word")), 144 | Backslash, 145 | SingleQuote 146 | ); 147 | -------------------------------------------------------------------------------- /tests/loop.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::builder::*; 3 | use conch_parser::parse::ParseError::*; 4 | use conch_parser::token::Token; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_loop_command_while_valid() { 11 | let mut p = make_parser( 12 | "while guard1; guard2;\n#guard_comment\n do foo\nbar; baz\n#body_comment\n done", 13 | ); 14 | let (until, GuardBodyPairGroup { guard, body }) = p.loop_command().unwrap(); 15 | 16 | let correct_guard = CommandGroup { 17 | commands: vec![cmd("guard1"), cmd("guard2")], 18 | trailing_comments: vec![Newline(Some("#guard_comment".into()))], 19 | }; 20 | let correct_body = CommandGroup { 21 | commands: vec![cmd("foo"), cmd("bar"), cmd("baz")], 22 | trailing_comments: vec![Newline(Some("#body_comment".into()))], 23 | }; 24 | 25 | assert_eq!(until, LoopKind::While); 26 | assert_eq!(correct_guard, guard); 27 | assert_eq!(correct_body, body); 28 | } 29 | 30 | #[test] 31 | fn test_loop_command_until_valid() { 32 | let mut p = make_parser( 33 | "until guard1; guard2;\n#guard_comment\n do foo\nbar; baz\n#body_comment\n done", 34 | ); 35 | let (until, GuardBodyPairGroup { guard, body }) = p.loop_command().unwrap(); 36 | 37 | let correct_guard = CommandGroup { 38 | commands: vec![cmd("guard1"), cmd("guard2")], 39 | trailing_comments: vec![Newline(Some("#guard_comment".into()))], 40 | }; 41 | let correct_body = CommandGroup { 42 | commands: vec![cmd("foo"), cmd("bar"), cmd("baz")], 43 | trailing_comments: vec![Newline(Some("#body_comment".into()))], 44 | }; 45 | 46 | assert_eq!(until, LoopKind::Until); 47 | assert_eq!(correct_guard, guard); 48 | assert_eq!(correct_body, body); 49 | } 50 | 51 | #[test] 52 | fn test_loop_command_invalid_missing_separator() { 53 | let mut p = make_parser("while guard do foo\nbar; baz; done"); 54 | assert_eq!( 55 | Err(IncompleteCmd("while", src(0, 1, 1), "do", src(33, 2, 15))), 56 | p.loop_command() 57 | ); 58 | let mut p = make_parser("while guard; do foo\nbar; baz done"); 59 | assert_eq!( 60 | Err(IncompleteCmd("do", src(13, 1, 14), "done", src(33, 2, 14))), 61 | p.loop_command() 62 | ); 63 | } 64 | 65 | #[test] 66 | fn test_loop_command_invalid_missing_keyword() { 67 | let mut p = make_parser("guard; do foo\nbar; baz; done"); 68 | assert_eq!( 69 | Err(Unexpected(Token::Name(String::from("guard")), src(0, 1, 1))), 70 | p.loop_command() 71 | ); 72 | } 73 | 74 | #[test] 75 | fn test_loop_command_invalid_missing_guard() { 76 | // With command separator between loop and do keywords 77 | let mut p = make_parser("while; do foo\nbar; baz; done"); 78 | assert_eq!(Err(Unexpected(Token::Semi, src(5, 1, 6))), p.loop_command()); 79 | let mut p = make_parser("until; do foo\nbar; baz; done"); 80 | assert_eq!(Err(Unexpected(Token::Semi, src(5, 1, 6))), p.loop_command()); 81 | 82 | // Without command separator between loop and do keywords 83 | let mut p = make_parser("while do foo\nbar; baz; done"); 84 | assert_eq!( 85 | Err(Unexpected(Token::Name(String::from("do")), src(6, 1, 7))), 86 | p.loop_command() 87 | ); 88 | let mut p = make_parser("until do foo\nbar; baz; done"); 89 | assert_eq!( 90 | Err(Unexpected(Token::Name(String::from("do")), src(6, 1, 7))), 91 | p.loop_command() 92 | ); 93 | } 94 | 95 | #[test] 96 | fn test_loop_command_invalid_quoted() { 97 | let cmds = [ 98 | ( 99 | "'while' guard do foo\nbar; baz; done", 100 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 101 | ), 102 | ( 103 | "'until' guard do foo\nbar; baz; done", 104 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 105 | ), 106 | ( 107 | "\"while\" guard do foo\nbar; baz; done", 108 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 109 | ), 110 | ( 111 | "\"until\" guard do foo\nbar; baz; done", 112 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 113 | ), 114 | ]; 115 | 116 | for (c, e) in &cmds { 117 | match make_parser(c).loop_command() { 118 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 119 | Err(ref err) => { 120 | if err != e { 121 | panic!( 122 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 123 | c, e, err 124 | ); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | #[test] 132 | fn test_loop_command_invalid_concat() { 133 | let mut p = make_parser_from_tokens(vec![ 134 | Token::Literal(String::from("whi")), 135 | Token::Literal(String::from("le")), 136 | Token::Newline, 137 | Token::Literal(String::from("guard")), 138 | Token::Newline, 139 | Token::Literal(String::from("do")), 140 | Token::Literal(String::from("foo")), 141 | Token::Newline, 142 | Token::Literal(String::from("done")), 143 | ]); 144 | assert_eq!( 145 | Err(Unexpected( 146 | Token::Literal(String::from("whi")), 147 | src(0, 1, 1) 148 | )), 149 | p.loop_command() 150 | ); 151 | let mut p = make_parser_from_tokens(vec![ 152 | Token::Literal(String::from("un")), 153 | Token::Literal(String::from("til")), 154 | Token::Newline, 155 | Token::Literal(String::from("guard")), 156 | Token::Newline, 157 | Token::Literal(String::from("do")), 158 | Token::Literal(String::from("foo")), 159 | Token::Newline, 160 | Token::Literal(String::from("done")), 161 | ]); 162 | assert_eq!( 163 | Err(Unexpected(Token::Literal(String::from("un")), src(0, 1, 1))), 164 | p.loop_command() 165 | ); 166 | } 167 | 168 | #[test] 169 | fn test_loop_command_should_recognize_literals_and_names() { 170 | for kw in vec![ 171 | Token::Literal(String::from("while")), 172 | Token::Name(String::from("while")), 173 | Token::Literal(String::from("until")), 174 | Token::Name(String::from("until")), 175 | ] { 176 | let mut p = make_parser_from_tokens(vec![ 177 | kw, 178 | Token::Newline, 179 | Token::Literal(String::from("guard")), 180 | Token::Newline, 181 | Token::Literal(String::from("do")), 182 | Token::Newline, 183 | Token::Literal(String::from("foo")), 184 | Token::Newline, 185 | Token::Literal(String::from("done")), 186 | ]); 187 | p.loop_command().unwrap(); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tests/parameter.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::Parameter::*; 3 | use conch_parser::ast::ParameterSubstitution::*; 4 | use conch_parser::parse::ParseError::*; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_parameter_short() { 11 | let words = vec![At, Star, Pound, Question, Dash, Dollar, Bang, Positional(3)]; 12 | 13 | let mut p = make_parser("$@$*$#$?$-$$$!$3$"); 14 | for param in words { 15 | assert_eq!(p.parameter().unwrap(), word_param(param)); 16 | } 17 | 18 | assert_eq!(word("$"), p.parameter().unwrap()); 19 | assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted 20 | } 21 | 22 | #[test] 23 | fn test_parameter_short_in_curlies() { 24 | let words = vec![ 25 | At, 26 | Star, 27 | Pound, 28 | Question, 29 | Dash, 30 | Dollar, 31 | Bang, 32 | Var(String::from("foo")), 33 | Positional(3), 34 | Positional(1000), 35 | ]; 36 | 37 | let mut p = make_parser("${@}${*}${#}${?}${-}${$}${!}${foo}${3}${1000}"); 38 | for param in words { 39 | assert_eq!(p.parameter().unwrap(), word_param(param)); 40 | } 41 | 42 | assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted 43 | } 44 | 45 | #[test] 46 | fn test_parameter_command_substitution() { 47 | let correct = word_subst(Command(vec![ 48 | cmd_args("echo", &["hello"]), 49 | cmd_args("echo", &["world"]), 50 | ])); 51 | 52 | assert_eq!( 53 | correct, 54 | make_parser("$(echo hello; echo world)") 55 | .parameter() 56 | .unwrap() 57 | ); 58 | } 59 | 60 | #[test] 61 | fn test_parameter_command_substitution_valid_empty_substitution() { 62 | let correct = word_subst(Command(vec![])); 63 | assert_eq!(correct, make_parser("$()").parameter().unwrap()); 64 | assert_eq!(correct, make_parser("$( )").parameter().unwrap()); 65 | assert_eq!(correct, make_parser("$(\n\n)").parameter().unwrap()); 66 | } 67 | 68 | #[test] 69 | fn test_parameter_literal_dollar_if_no_param() { 70 | let mut p = make_parser("$%asdf"); 71 | assert_eq!(word("$"), p.parameter().unwrap()); 72 | assert_eq!(p.word().unwrap().unwrap(), word("%asdf")); 73 | } 74 | -------------------------------------------------------------------------------- /tests/parse.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | #![recursion_limit = "128"] 3 | 4 | use conch_parser::ast::builder::*; 5 | use conch_parser::parse::*; 6 | 7 | mod parse_support; 8 | use crate::parse_support::*; 9 | 10 | #[test] 11 | fn test_parser_should_yield_none_after_error() { 12 | let mut iter = make_parser("foo && ||").into_iter(); 13 | let _ = iter.next().expect("failed to get error").unwrap_err(); 14 | assert_eq!(iter.next(), None); 15 | } 16 | 17 | #[test] 18 | fn test_linebreak_valid_with_comments_and_whitespace() { 19 | let mut p = make_parser("\n\t\t\t\n # comment1\n#comment2\n \n"); 20 | assert_eq!( 21 | p.linebreak(), 22 | vec!( 23 | Newline(None), 24 | Newline(None), 25 | Newline(Some(String::from("# comment1"))), 26 | Newline(Some(String::from("#comment2"))), 27 | Newline(None) 28 | ) 29 | ); 30 | } 31 | 32 | #[test] 33 | fn test_linebreak_valid_empty() { 34 | let mut p = make_parser(""); 35 | assert_eq!(p.linebreak(), vec!()); 36 | } 37 | 38 | #[test] 39 | fn test_linebreak_valid_nonnewline() { 40 | let mut p = make_parser("hello world"); 41 | assert_eq!(p.linebreak(), vec!()); 42 | } 43 | 44 | #[test] 45 | fn test_linebreak_valid_eof_instead_of_newline() { 46 | let mut p = make_parser("#comment"); 47 | assert_eq!(p.linebreak(), vec!(Newline(Some(String::from("#comment"))))); 48 | } 49 | 50 | #[test] 51 | fn test_linebreak_single_quote_insiginificant() { 52 | let mut p = make_parser("#unclosed quote ' comment"); 53 | assert_eq!( 54 | p.linebreak(), 55 | vec!(Newline(Some(String::from("#unclosed quote ' comment")))) 56 | ); 57 | } 58 | 59 | #[test] 60 | fn test_linebreak_double_quote_insiginificant() { 61 | let mut p = make_parser("#unclosed quote \" comment"); 62 | assert_eq!( 63 | p.linebreak(), 64 | vec!(Newline(Some(String::from("#unclosed quote \" comment")))) 65 | ); 66 | } 67 | 68 | #[test] 69 | fn test_linebreak_escaping_newline_insignificant() { 70 | let mut p = make_parser("#comment escapes newline\\\n"); 71 | assert_eq!( 72 | p.linebreak(), 73 | vec!(Newline(Some(String::from("#comment escapes newline\\")))) 74 | ); 75 | } 76 | 77 | #[test] 78 | fn test_skip_whitespace_preserve_newline() { 79 | let mut p = make_parser(" \t\t \t \t\n "); 80 | p.skip_whitespace(); 81 | assert_eq!(p.linebreak().len(), 1); 82 | } 83 | 84 | #[test] 85 | fn test_skip_whitespace_preserve_comments() { 86 | let mut p = make_parser(" \t\t \t \t#comment\n "); 87 | p.skip_whitespace(); 88 | assert_eq!( 89 | p.linebreak().pop().unwrap(), 90 | Newline(Some(String::from("#comment"))) 91 | ); 92 | } 93 | 94 | #[test] 95 | fn test_skip_whitespace_comments_capture_all_up_to_newline() { 96 | let mut p = make_parser("#comment&&||;;()<<-\n"); 97 | assert_eq!( 98 | p.linebreak().pop().unwrap(), 99 | Newline(Some(String::from("#comment&&||;;()<<-"))) 100 | ); 101 | } 102 | 103 | #[test] 104 | fn test_skip_whitespace_comments_may_end_with_eof() { 105 | let mut p = make_parser("#comment"); 106 | assert_eq!( 107 | p.linebreak().pop().unwrap(), 108 | Newline(Some(String::from("#comment"))) 109 | ); 110 | } 111 | 112 | #[test] 113 | fn test_skip_whitespace_skip_escapes_dont_affect_newlines() { 114 | let mut p = make_parser(" \t \\\n \\\n#comment\n"); 115 | p.skip_whitespace(); 116 | assert_eq!( 117 | p.linebreak().pop().unwrap(), 118 | Newline(Some(String::from("#comment"))) 119 | ); 120 | } 121 | 122 | #[test] 123 | fn test_skip_whitespace_skips_escaped_newlines() { 124 | let mut p = make_parser("\\\n\\\n \\\n"); 125 | p.skip_whitespace(); 126 | assert_eq!(p.linebreak(), vec!()); 127 | } 128 | 129 | #[test] 130 | fn test_comment_cannot_start_mid_whitespace_delimited_word() { 131 | let mut p = make_parser("hello#world"); 132 | let w = p.word().unwrap().expect("no valid word was discovered"); 133 | assert_eq!(w, word("hello#world")); 134 | } 135 | 136 | #[test] 137 | fn test_comment_can_start_if_whitespace_before_pound() { 138 | let mut p = make_parser("hello #world"); 139 | p.word().unwrap().expect("no valid word was discovered"); 140 | let comment = p.linebreak(); 141 | assert_eq!(comment, vec!(Newline(Some(String::from("#world"))))); 142 | } 143 | 144 | #[test] 145 | fn test_braces_literal_unless_brace_group_expected() { 146 | let source = "echo {} } {"; 147 | let mut p = make_parser(source); 148 | assert_eq!(p.word().unwrap().unwrap(), word("echo")); 149 | assert_eq!(p.word().unwrap().unwrap(), word("{}")); 150 | assert_eq!(p.word().unwrap().unwrap(), word("}")); 151 | assert_eq!(p.word().unwrap().unwrap(), word("{")); 152 | 153 | let correct = Some(cmd_args("echo", &["{}", "}", "{"])); 154 | assert_eq!(correct, make_parser(source).complete_command().unwrap()); 155 | } 156 | 157 | #[test] 158 | fn ensure_parse_errors_are_send_and_sync() { 159 | fn send_and_sync() {} 160 | send_and_sync::>(); 161 | } 162 | 163 | #[test] 164 | fn ensure_parser_could_be_send_and_sync() { 165 | use conch_parser::token::Token; 166 | 167 | fn send_and_sync() {} 168 | send_and_sync::, ArcBuilder>>(); 169 | } 170 | -------------------------------------------------------------------------------- /tests/parse_support.rs: -------------------------------------------------------------------------------- 1 | // Certain helpers may only be used by specific tests, 2 | // suppress dead_code warnings since the compiler can't 3 | // see our intent 4 | #![allow(dead_code)] 5 | 6 | use conch_parser::ast::Command::*; 7 | use conch_parser::ast::ComplexWord::*; 8 | use conch_parser::ast::PipeableCommand::*; 9 | use conch_parser::ast::SimpleWord::*; 10 | use conch_parser::ast::*; 11 | use conch_parser::lexer::Lexer; 12 | use conch_parser::parse::*; 13 | use conch_parser::token::Token; 14 | 15 | pub fn lit(s: &str) -> DefaultWord { 16 | Word::Simple(Literal(String::from(s))) 17 | } 18 | 19 | pub fn escaped(s: &str) -> DefaultWord { 20 | Word::Simple(Escaped(String::from(s))) 21 | } 22 | 23 | pub fn subst(s: DefaultParameterSubstitution) -> DefaultWord { 24 | Word::Simple(Subst(Box::new(s))) 25 | } 26 | 27 | pub fn single_quoted(s: &str) -> TopLevelWord { 28 | TopLevelWord(Single(Word::SingleQuoted(String::from(s)))) 29 | } 30 | 31 | pub fn double_quoted(s: &str) -> TopLevelWord { 32 | TopLevelWord(Single(Word::DoubleQuoted(vec![Literal(String::from(s))]))) 33 | } 34 | 35 | pub fn word(s: &str) -> TopLevelWord { 36 | TopLevelWord(Single(lit(s))) 37 | } 38 | 39 | pub fn word_escaped(s: &str) -> TopLevelWord { 40 | TopLevelWord(Single(escaped(s))) 41 | } 42 | 43 | pub fn word_subst(s: DefaultParameterSubstitution) -> TopLevelWord { 44 | TopLevelWord(Single(subst(s))) 45 | } 46 | 47 | pub fn word_param(p: DefaultParameter) -> TopLevelWord { 48 | TopLevelWord(Single(Word::Simple(Param(p)))) 49 | } 50 | 51 | pub fn make_parser(src: &str) -> DefaultParser>> { 52 | DefaultParser::new(Lexer::new(src.chars())) 53 | } 54 | 55 | pub fn make_parser_from_tokens(src: Vec) -> DefaultParser> { 56 | DefaultParser::new(src.into_iter()) 57 | } 58 | 59 | pub fn cmd_args_simple(cmd: &str, args: &[&str]) -> Box { 60 | let mut cmd_args = Vec::with_capacity(args.len() + 1); 61 | cmd_args.push(RedirectOrCmdWord::CmdWord(word(cmd))); 62 | cmd_args.extend(args.iter().map(|&a| RedirectOrCmdWord::CmdWord(word(a)))); 63 | 64 | Box::new(SimpleCommand { 65 | redirects_or_env_vars: vec![], 66 | redirects_or_cmd_words: cmd_args, 67 | }) 68 | } 69 | 70 | pub fn cmd_simple(cmd: &str) -> Box { 71 | cmd_args_simple(cmd, &[]) 72 | } 73 | 74 | pub fn cmd_args(cmd: &str, args: &[&str]) -> TopLevelCommand { 75 | TopLevelCommand(List(CommandList { 76 | first: ListableCommand::Single(Simple(cmd_args_simple(cmd, args))), 77 | rest: vec![], 78 | })) 79 | } 80 | 81 | pub fn cmd(cmd: &str) -> TopLevelCommand { 82 | cmd_args(cmd, &[]) 83 | } 84 | 85 | pub fn cmd_from_simple(cmd: DefaultSimpleCommand) -> TopLevelCommand { 86 | TopLevelCommand(List(CommandList { 87 | first: ListableCommand::Single(Simple(Box::new(cmd))), 88 | rest: vec![], 89 | })) 90 | } 91 | 92 | pub fn src(byte: usize, line: usize, col: usize) -> SourcePos { 93 | SourcePos { byte, line, col } 94 | } 95 | -------------------------------------------------------------------------------- /tests/pipeline.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::PipeableCommand::*; 3 | use conch_parser::ast::*; 4 | use conch_parser::parse::ParseError::*; 5 | use conch_parser::token::Token; 6 | 7 | mod parse_support; 8 | use crate::parse_support::*; 9 | 10 | #[test] 11 | fn test_pipeline_valid_bang() { 12 | let mut p = make_parser("! foo | bar | baz"); 13 | let correct = CommandList { 14 | first: ListableCommand::Pipe( 15 | true, 16 | vec![ 17 | Simple(cmd_simple("foo")), 18 | Simple(cmd_simple("bar")), 19 | Simple(cmd_simple("baz")), 20 | ], 21 | ), 22 | rest: vec![], 23 | }; 24 | assert_eq!(correct, p.and_or_list().unwrap()); 25 | } 26 | 27 | #[test] 28 | fn test_pipeline_valid_bangs_in_and_or() { 29 | let mut p = make_parser("! foo | bar || ! baz && ! foobar"); 30 | let correct = CommandList { 31 | first: ListableCommand::Pipe( 32 | true, 33 | vec![Simple(cmd_simple("foo")), Simple(cmd_simple("bar"))], 34 | ), 35 | rest: vec![ 36 | AndOr::Or(ListableCommand::Pipe(true, vec![Simple(cmd_simple("baz"))])), 37 | AndOr::And(ListableCommand::Pipe( 38 | true, 39 | vec![Simple(cmd_simple("foobar"))], 40 | )), 41 | ], 42 | }; 43 | assert_eq!(correct, p.and_or_list().unwrap()); 44 | } 45 | 46 | #[test] 47 | fn test_pipeline_no_bang_single_cmd_optimize_wrapper_out() { 48 | let mut p = make_parser("foo"); 49 | let parse = p.pipeline().unwrap(); 50 | if let ListableCommand::Pipe(..) = parse { 51 | panic!("Parser::pipeline should not create a wrapper if no ! present and only a single command"); 52 | } 53 | } 54 | 55 | #[test] 56 | fn test_pipeline_invalid_multiple_bangs_in_same_pipeline() { 57 | let mut p = make_parser("! foo | bar | ! baz"); 58 | assert_eq!(Err(Unexpected(Token::Bang, src(14, 1, 15))), p.pipeline()); 59 | } 60 | -------------------------------------------------------------------------------- /tests/positional.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::token::Positional; 3 | 4 | #[test] 5 | fn test_positional_conversions() { 6 | for i in 0..10u8 { 7 | let positional = 8 | Positional::from_num(i).unwrap_or_else(|| panic!("failed to convert {}", i)); 9 | assert_eq!(positional.as_num(), i); 10 | } 11 | 12 | assert_eq!(Positional::from_num(10), None); 13 | } 14 | -------------------------------------------------------------------------------- /tests/redirect.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::ComplexWord::*; 3 | use conch_parser::ast::PipeableCommand::*; 4 | use conch_parser::ast::SimpleWord::*; 5 | use conch_parser::ast::*; 6 | use conch_parser::parse::ParseError::*; 7 | use conch_parser::token::Token; 8 | 9 | mod parse_support; 10 | use crate::parse_support::*; 11 | 12 | fn simple_command_with_redirect(cmd: &str, redirect: DefaultRedirect) -> DefaultPipeableCommand { 13 | Simple(Box::new(SimpleCommand { 14 | redirects_or_env_vars: vec![], 15 | redirects_or_cmd_words: vec![ 16 | RedirectOrCmdWord::CmdWord(word(cmd)), 17 | RedirectOrCmdWord::Redirect(redirect), 18 | ], 19 | })) 20 | } 21 | 22 | #[test] 23 | fn test_redirect_valid_close_without_whitespace() { 24 | let mut p = make_parser(">&-"); 25 | assert_eq!( 26 | Some(Ok(Redirect::DupWrite(None, word("-")))), 27 | p.redirect().unwrap() 28 | ); 29 | } 30 | 31 | #[test] 32 | fn test_redirect_valid_close_with_whitespace() { 33 | let mut p = make_parser("<& -"); 34 | assert_eq!( 35 | Some(Ok(Redirect::DupRead(None, word("-")))), 36 | p.redirect().unwrap() 37 | ); 38 | } 39 | 40 | #[test] 41 | fn test_redirect_valid_start_with_dash_if_not_dup() { 42 | let path = word("-test"); 43 | let cases = vec![ 44 | ("4<-test", Redirect::Read(Some(4), path.clone())), 45 | ("4>-test", Redirect::Write(Some(4), path.clone())), 46 | ("4<>-test", Redirect::ReadWrite(Some(4), path.clone())), 47 | ("4>>-test", Redirect::Append(Some(4), path.clone())), 48 | ("4>|-test", Redirect::Clobber(Some(4), path)), 49 | ]; 50 | 51 | for (s, correct) in cases.into_iter() { 52 | match make_parser(s).redirect() { 53 | Ok(Some(Ok(ref r))) if *r == correct => {} 54 | Ok(r) => panic!( 55 | "Unexpectedly parsed the source \"{}\" as\n{:?} instead of\n{:?}", 56 | s, r, correct 57 | ), 58 | Err(err) => panic!("Failed to parse the source \"{}\": {}", s, err), 59 | } 60 | } 61 | } 62 | 63 | #[test] 64 | fn test_redirect_valid_return_word_if_no_redirect() { 65 | let mut p = make_parser("hello"); 66 | assert_eq!(Some(Err(word("hello"))), p.redirect().unwrap()); 67 | } 68 | 69 | #[test] 70 | fn test_redirect_valid_return_word_if_src_fd_is_definitely_non_numeric() { 71 | let mut p = make_parser("123$$'foo'>out"); 72 | let correct = TopLevelWord(Concat(vec![ 73 | lit("123"), 74 | Word::Simple(Param(Parameter::Dollar)), 75 | Word::SingleQuoted(String::from("foo")), 76 | ])); 77 | assert_eq!(Some(Err(correct)), p.redirect().unwrap()); 78 | } 79 | 80 | #[test] 81 | fn test_redirect_valid_return_word_if_src_fd_has_escaped_numerics() { 82 | let mut p = make_parser("\\2>"); 83 | let correct = word_escaped("2"); 84 | assert_eq!(Some(Err(correct)), p.redirect().unwrap()); 85 | } 86 | 87 | #[test] 88 | fn test_redirect_valid_dst_fd_can_have_escaped_numerics() { 89 | let mut p = make_parser(">\\2"); 90 | let correct = Redirect::Write(None, word_escaped("2")); 91 | assert_eq!(Some(Ok(correct)), p.redirect().unwrap()); 92 | } 93 | 94 | #[test] 95 | fn test_redirect_invalid_dup_if_dst_fd_is_definitely_non_numeric() { 96 | assert_eq!( 97 | Err(BadFd(src(2, 1, 3), src(12, 1, 13))), 98 | make_parser(">&123$$'foo'").redirect() 99 | ); 100 | } 101 | 102 | #[test] 103 | fn test_redirect_valid_dup_return_redirect_if_dst_fd_is_possibly_numeric() { 104 | let mut p = make_parser(">&123$$$(echo 2)`echo bar`"); 105 | let correct = Redirect::DupWrite( 106 | None, 107 | TopLevelWord(Concat(vec![ 108 | lit("123"), 109 | Word::Simple(Param(Parameter::Dollar)), 110 | subst(ParameterSubstitution::Command(vec![cmd_args( 111 | "echo", 112 | &["2"], 113 | )])), 114 | subst(ParameterSubstitution::Command(vec![cmd_args( 115 | "echo", 116 | &["bar"], 117 | )])), 118 | ])), 119 | ); 120 | assert_eq!(Some(Ok(correct)), p.redirect().unwrap()); 121 | } 122 | 123 | #[test] 124 | fn test_redirect_invalid_close_without_whitespace() { 125 | assert_eq!( 126 | Err(BadFd(src(2, 1, 3), src(7, 1, 8))), 127 | make_parser(">&-asdf").redirect() 128 | ); 129 | } 130 | 131 | #[test] 132 | fn test_redirect_invalid_close_with_whitespace() { 133 | assert_eq!( 134 | Err(BadFd(src(9, 1, 10), src(14, 1, 15))), 135 | make_parser("<& -asdf").redirect() 136 | ); 137 | } 138 | 139 | #[test] 140 | fn test_redirect_fd_immediately_preceeding_redirection() { 141 | let mut p = make_parser("foo 1>>out"); 142 | let cmd = p.simple_command().unwrap(); 143 | assert_eq!( 144 | cmd, 145 | simple_command_with_redirect("foo", Redirect::Append(Some(1), word("out"))) 146 | ); 147 | } 148 | 149 | #[test] 150 | fn test_redirect_fd_must_immediately_preceed_redirection() { 151 | let correct = Simple(Box::new(SimpleCommand { 152 | redirects_or_env_vars: vec![], 153 | redirects_or_cmd_words: vec![ 154 | RedirectOrCmdWord::CmdWord(word("foo")), 155 | RedirectOrCmdWord::CmdWord(word("1")), 156 | RedirectOrCmdWord::Redirect(Redirect::ReadWrite(None, word("out"))), 157 | ], 158 | })); 159 | 160 | let mut p = make_parser("foo 1 <>out"); 161 | assert_eq!(p.simple_command().unwrap(), correct); 162 | } 163 | 164 | #[test] 165 | fn test_redirect_valid_dup_with_fd() { 166 | let mut p = make_parser("foo 1>&2"); 167 | let cmd = p.simple_command().unwrap(); 168 | assert_eq!( 169 | cmd, 170 | simple_command_with_redirect("foo", Redirect::DupWrite(Some(1), word("2"))) 171 | ); 172 | } 173 | 174 | #[test] 175 | fn test_redirect_valid_dup_without_fd() { 176 | let correct = Simple(Box::new(SimpleCommand { 177 | redirects_or_env_vars: vec![], 178 | redirects_or_cmd_words: vec![ 179 | RedirectOrCmdWord::CmdWord(word("foo")), 180 | RedirectOrCmdWord::CmdWord(word("1")), 181 | RedirectOrCmdWord::Redirect(Redirect::DupRead(None, word("2"))), 182 | ], 183 | })); 184 | 185 | let mut p = make_parser("foo 1 <&2"); 186 | assert_eq!(p.simple_command().unwrap(), correct); 187 | } 188 | 189 | #[test] 190 | fn test_redirect_valid_dup_with_whitespace() { 191 | let mut p = make_parser("foo 1<& 2"); 192 | let cmd = p.simple_command().unwrap(); 193 | assert_eq!( 194 | cmd, 195 | simple_command_with_redirect("foo", Redirect::DupRead(Some(1), word("2"))) 196 | ); 197 | } 198 | 199 | #[test] 200 | fn test_redirect_valid_single_quoted_dup_fd() { 201 | let correct = Redirect::DupWrite(Some(1), single_quoted("2")); 202 | assert_eq!(Some(Ok(correct)), make_parser("1>&'2'").redirect().unwrap()); 203 | } 204 | 205 | #[test] 206 | fn test_redirect_valid_double_quoted_dup_fd() { 207 | let correct = Redirect::DupWrite(None, double_quoted("2")); 208 | assert_eq!( 209 | Some(Ok(correct)), 210 | make_parser(">&\"2\"").redirect().unwrap() 211 | ); 212 | } 213 | 214 | #[test] 215 | fn test_redirect_src_fd_need_not_be_single_token() { 216 | let mut p = make_parser_from_tokens(vec![ 217 | Token::Literal(String::from("foo")), 218 | Token::Whitespace(String::from(" ")), 219 | Token::Literal(String::from("12")), 220 | Token::Literal(String::from("34")), 221 | Token::LessAnd, 222 | Token::Dash, 223 | ]); 224 | 225 | let cmd = p.simple_command().unwrap(); 226 | assert_eq!( 227 | cmd, 228 | simple_command_with_redirect("foo", Redirect::DupRead(Some(1234), word("-"))) 229 | ); 230 | } 231 | 232 | #[test] 233 | fn test_redirect_dst_fd_need_not_be_single_token() { 234 | let mut p = make_parser_from_tokens(vec![ 235 | Token::GreatAnd, 236 | Token::Literal(String::from("12")), 237 | Token::Literal(String::from("34")), 238 | ]); 239 | 240 | let correct = Redirect::DupWrite(None, word("1234")); 241 | assert_eq!(Some(Ok(correct)), p.redirect().unwrap()); 242 | } 243 | 244 | #[test] 245 | fn test_redirect_accept_literal_and_name_tokens() { 246 | let mut p = make_parser_from_tokens(vec![Token::GreatAnd, Token::Literal(String::from("12"))]); 247 | assert_eq!( 248 | Some(Ok(Redirect::DupWrite(None, word("12")))), 249 | p.redirect().unwrap() 250 | ); 251 | 252 | let mut p = make_parser_from_tokens(vec![Token::GreatAnd, Token::Name(String::from("12"))]); 253 | assert_eq!( 254 | Some(Ok(Redirect::DupWrite(None, word("12")))), 255 | p.redirect().unwrap() 256 | ); 257 | } 258 | 259 | #[test] 260 | fn test_redirect_list_valid() { 261 | let mut p = make_parser("1>>out <& 2 2>&-"); 262 | let io = p.redirect_list().unwrap(); 263 | assert_eq!( 264 | io, 265 | vec!( 266 | Redirect::Append(Some(1), word("out")), 267 | Redirect::DupRead(None, word("2")), 268 | Redirect::DupWrite(Some(2), word("-")), 269 | ) 270 | ); 271 | } 272 | 273 | #[test] 274 | fn test_redirect_list_rejects_non_fd_words() { 275 | assert_eq!( 276 | Err(BadFd(src(16, 1, 17), src(19, 1, 20))), 277 | make_parser("1>>out &- abc").redirect_list() 278 | ); 279 | assert_eq!( 280 | Err(BadFd(src(7, 1, 8), src(10, 1, 11))), 281 | make_parser("1>>out abc&-").redirect_list() 282 | ); 283 | assert_eq!( 284 | Err(BadFd(src(7, 1, 8), src(10, 1, 11))), 285 | make_parser("1>>out abc &-").redirect_list() 286 | ); 287 | } 288 | -------------------------------------------------------------------------------- /tests/simple_command.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::PipeableCommand::*; 3 | use conch_parser::ast::Redirect::*; 4 | use conch_parser::ast::*; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_simple_command_valid_assignments_at_start_of_command() { 11 | let mut p = make_parser("var=val ENV=true BLANK= foo bar baz"); 12 | let correct = Simple(Box::new(SimpleCommand { 13 | redirects_or_env_vars: vec![ 14 | RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))), 15 | RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))), 16 | RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None), 17 | ], 18 | redirects_or_cmd_words: vec![ 19 | RedirectOrCmdWord::CmdWord(word("foo")), 20 | RedirectOrCmdWord::CmdWord(word("bar")), 21 | RedirectOrCmdWord::CmdWord(word("baz")), 22 | ], 23 | })); 24 | assert_eq!(correct, p.simple_command().unwrap()); 25 | } 26 | 27 | #[test] 28 | fn test_simple_command_assignments_after_start_of_command_should_be_args() { 29 | let mut p = make_parser("var=val ENV=true BLANK= foo var2=val2 bar baz var3=val3"); 30 | let correct = Simple(Box::new(SimpleCommand { 31 | redirects_or_env_vars: vec![ 32 | RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))), 33 | RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))), 34 | RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None), 35 | ], 36 | redirects_or_cmd_words: vec![ 37 | RedirectOrCmdWord::CmdWord(word("foo")), 38 | RedirectOrCmdWord::CmdWord(word("var2=val2")), 39 | RedirectOrCmdWord::CmdWord(word("bar")), 40 | RedirectOrCmdWord::CmdWord(word("baz")), 41 | RedirectOrCmdWord::CmdWord(word("var3=val3")), 42 | ], 43 | })); 44 | assert_eq!(correct, p.simple_command().unwrap()); 45 | } 46 | 47 | #[test] 48 | fn test_simple_command_redirections_at_start_of_command() { 49 | let mut p = make_parser("2>|clob 3<>rw |clob 3<>rw |clob var=val 3<>rw ENV=true BLANK= foo bar &-"); 92 | let correct = Simple(Box::new(SimpleCommand { 93 | redirects_or_env_vars: vec![ 94 | RedirectOrEnvVar::Redirect(Clobber(Some(2), word("clob"))), 95 | RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))), 96 | RedirectOrEnvVar::Redirect(ReadWrite(Some(3), word("rw"))), 97 | RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))), 98 | RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None), 99 | ], 100 | redirects_or_cmd_words: vec![ 101 | RedirectOrCmdWord::CmdWord(word("foo")), 102 | RedirectOrCmdWord::CmdWord(word("bar")), 103 | RedirectOrCmdWord::Redirect(Read(None, word("in"))), 104 | RedirectOrCmdWord::CmdWord(word("baz")), 105 | RedirectOrCmdWord::Redirect(DupWrite(Some(4), word("-"))), 106 | ], 107 | })); 108 | 109 | assert_eq!(correct, p.simple_command().unwrap()); 110 | } 111 | -------------------------------------------------------------------------------- /tests/subshell.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::builder::*; 3 | use conch_parser::parse::ParseError::*; 4 | use conch_parser::token::Token; 5 | 6 | mod parse_support; 7 | use crate::parse_support::*; 8 | 9 | #[test] 10 | fn test_subshell_valid() { 11 | let mut p = make_parser("( foo\nbar; baz\n#comment\n )"); 12 | let correct = CommandGroup { 13 | commands: vec![cmd("foo"), cmd("bar"), cmd("baz")], 14 | trailing_comments: vec![Newline(Some("#comment".into()))], 15 | }; 16 | assert_eq!(correct, p.subshell().unwrap()); 17 | } 18 | 19 | #[test] 20 | fn test_subshell_valid_separator_not_needed() { 21 | let correct = CommandGroup { 22 | commands: vec![cmd("foo")], 23 | trailing_comments: vec![], 24 | }; 25 | assert_eq!(correct, make_parser("( foo )").subshell().unwrap()); 26 | 27 | let correct_with_comment = CommandGroup { 28 | commands: vec![cmd("foo")], 29 | trailing_comments: vec![Newline(Some("#comment".into()))], 30 | }; 31 | assert_eq!( 32 | correct_with_comment, 33 | make_parser("( foo\n#comment\n )").subshell().unwrap() 34 | ); 35 | } 36 | 37 | #[test] 38 | fn test_subshell_space_between_parens_not_needed() { 39 | let mut p = make_parser("(foo )"); 40 | p.subshell().unwrap(); 41 | let mut p = make_parser("( foo)"); 42 | p.subshell().unwrap(); 43 | let mut p = make_parser("(foo)"); 44 | p.subshell().unwrap(); 45 | } 46 | 47 | #[test] 48 | fn test_subshell_invalid_missing_keyword() { 49 | assert_eq!( 50 | Err(Unmatched(Token::ParenOpen, src(0, 1, 1))), 51 | make_parser("( foo\nbar; baz").subshell() 52 | ); 53 | assert_eq!( 54 | Err(Unexpected(Token::Name(String::from("foo")), src(0, 1, 1))), 55 | make_parser("foo\nbar; baz; )").subshell() 56 | ); 57 | } 58 | 59 | #[test] 60 | fn test_subshell_invalid_quoted() { 61 | let cmds = [ 62 | ( 63 | "'(' foo\nbar; baz; )", 64 | Unexpected(Token::SingleQuote, src(0, 1, 1)), 65 | ), 66 | ( 67 | "( foo\nbar; baz; ')'", 68 | Unmatched(Token::ParenOpen, src(0, 1, 1)), 69 | ), 70 | ( 71 | "\"(\" foo\nbar; baz; )", 72 | Unexpected(Token::DoubleQuote, src(0, 1, 1)), 73 | ), 74 | ( 75 | "( foo\nbar; baz; \")\"", 76 | Unmatched(Token::ParenOpen, src(0, 1, 1)), 77 | ), 78 | ]; 79 | 80 | for (c, e) in &cmds { 81 | match make_parser(c).subshell() { 82 | Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result), 83 | Err(ref err) => { 84 | if err != e { 85 | panic!( 86 | "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`", 87 | c, e, err 88 | ); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | #[test] 96 | fn test_subshell_invalid_missing_body() { 97 | assert_eq!( 98 | Err(Unexpected(Token::ParenClose, src(2, 2, 1))), 99 | make_parser("(\n)").subshell() 100 | ); 101 | assert_eq!( 102 | Err(Unexpected(Token::ParenClose, src(1, 1, 2))), 103 | make_parser("()").subshell() 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /tests/word.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | use conch_parser::ast::ComplexWord::*; 3 | use conch_parser::ast::SimpleWord::*; 4 | use conch_parser::ast::*; 5 | use conch_parser::parse::ParseError::*; 6 | use conch_parser::token::Token; 7 | 8 | mod parse_support; 9 | use crate::parse_support::*; 10 | 11 | #[test] 12 | fn test_word_single_quote_valid() { 13 | let correct = single_quoted("abc&&||\n\n#comment\nabc"); 14 | assert_eq!( 15 | Some(correct), 16 | make_parser("'abc&&||\n\n#comment\nabc'").word().unwrap() 17 | ); 18 | } 19 | 20 | #[test] 21 | fn test_word_single_quote_valid_slash_remains_literal() { 22 | let correct = single_quoted("\\\n"); 23 | assert_eq!(Some(correct), make_parser("'\\\n'").word().unwrap()); 24 | } 25 | 26 | #[test] 27 | fn test_word_single_quote_valid_does_not_quote_single_quotes() { 28 | let correct = single_quoted("hello \\"); 29 | assert_eq!(Some(correct), make_parser("'hello \\'").word().unwrap()); 30 | } 31 | 32 | #[test] 33 | fn test_word_single_quote_invalid_missing_close_quote() { 34 | assert_eq!( 35 | Err(Unmatched(Token::SingleQuote, src(0, 1, 1))), 36 | make_parser("'hello").word() 37 | ); 38 | } 39 | 40 | #[test] 41 | fn test_word_double_quote_valid() { 42 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![Literal(String::from( 43 | "abc&&||\n\n#comment\nabc", 44 | ))]))); 45 | assert_eq!( 46 | Some(correct), 47 | make_parser("\"abc&&||\n\n#comment\nabc\"").word().unwrap() 48 | ); 49 | } 50 | 51 | #[test] 52 | fn test_word_double_quote_valid_recognizes_parameters() { 53 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 54 | Literal(String::from("test asdf")), 55 | Param(Parameter::Var(String::from("foo"))), 56 | Literal(String::from(" $")), 57 | ]))); 58 | 59 | assert_eq!( 60 | Some(correct), 61 | make_parser("\"test asdf$foo $\"").word().unwrap() 62 | ); 63 | } 64 | 65 | #[test] 66 | fn test_word_double_quote_valid_recognizes_backticks() { 67 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 68 | Literal(String::from("test asdf ")), 69 | Subst(Box::new(ParameterSubstitution::Command(vec![cmd("foo")]))), 70 | ]))); 71 | 72 | assert_eq!( 73 | Some(correct), 74 | make_parser("\"test asdf `foo`\"").word().unwrap() 75 | ); 76 | } 77 | 78 | #[test] 79 | fn test_word_double_quote_valid_slash_escapes_dollar() { 80 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 81 | Literal(String::from("test")), 82 | Escaped(String::from("$")), 83 | Literal(String::from("foo ")), 84 | Param(Parameter::At), 85 | ]))); 86 | 87 | assert_eq!( 88 | Some(correct), 89 | make_parser("\"test\\$foo $@\"").word().unwrap() 90 | ); 91 | } 92 | 93 | #[test] 94 | fn test_word_double_quote_valid_slash_escapes_backtick() { 95 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 96 | Literal(String::from("test")), 97 | Escaped(String::from("`")), 98 | Literal(String::from(" ")), 99 | Param(Parameter::Star), 100 | ]))); 101 | 102 | assert_eq!(Some(correct), make_parser("\"test\\` $*\"").word().unwrap()); 103 | } 104 | 105 | #[test] 106 | fn test_word_double_quote_valid_slash_escapes_double_quote() { 107 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 108 | Literal(String::from("test")), 109 | Escaped(String::from("\"")), 110 | Literal(String::from(" ")), 111 | Param(Parameter::Pound), 112 | ]))); 113 | 114 | assert_eq!( 115 | Some(correct), 116 | make_parser("\"test\\\" $#\"").word().unwrap() 117 | ); 118 | } 119 | 120 | #[test] 121 | fn test_word_double_quote_valid_slash_escapes_newline() { 122 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 123 | Literal(String::from("test")), 124 | Escaped(String::from("\n")), 125 | Literal(String::from(" ")), 126 | Param(Parameter::Question), 127 | Literal(String::from("\n")), 128 | ]))); 129 | 130 | assert_eq!( 131 | Some(correct), 132 | make_parser("\"test\\\n $?\n\"").word().unwrap() 133 | ); 134 | } 135 | 136 | #[test] 137 | fn test_word_double_quote_valid_slash_escapes_slash() { 138 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 139 | Literal(String::from("test")), 140 | Escaped(String::from("\\")), 141 | Literal(String::from(" ")), 142 | Param(Parameter::Positional(0)), 143 | ]))); 144 | 145 | assert_eq!( 146 | Some(correct), 147 | make_parser("\"test\\\\ $0\"").word().unwrap() 148 | ); 149 | } 150 | 151 | #[test] 152 | fn test_word_double_quote_valid_slash_remains_literal_in_general_case() { 153 | let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![ 154 | Literal(String::from("t\\est ")), 155 | Param(Parameter::Dollar), 156 | ]))); 157 | 158 | assert_eq!(Some(correct), make_parser("\"t\\est $$\"").word().unwrap()); 159 | } 160 | 161 | #[test] 162 | fn test_word_double_quote_slash_invalid_missing_close_quote() { 163 | assert_eq!( 164 | Err(Unmatched(Token::DoubleQuote, src(0, 1, 1))), 165 | make_parser("\"hello").word() 166 | ); 167 | assert_eq!( 168 | Err(Unmatched(Token::DoubleQuote, src(0, 1, 1))), 169 | make_parser("\"hello\\\"").word() 170 | ); 171 | } 172 | 173 | #[test] 174 | fn test_word_delegate_parameters() { 175 | let params = [ 176 | "$@", "$*", "$#", "$?", "$-", "$$", "$!", "$3", "${@}", "${*}", "${#}", "${?}", "${-}", 177 | "${$}", "${!}", "${foo}", "${3}", "${1000}", 178 | ]; 179 | 180 | for p in ¶ms { 181 | match make_parser(p).word() { 182 | Ok(Some(TopLevelWord(Single(Word::Simple(w))))) => { 183 | if let Param(_) = w { 184 | } else { 185 | panic!( 186 | "Unexpectedly parsed \"{}\" as a non-parameter word:\n{:#?}", 187 | p, w 188 | ); 189 | } 190 | } 191 | Ok(Some(w)) => panic!( 192 | "Unexpectedly parsed \"{}\" as a non-parameter word:\n{:#?}", 193 | p, w 194 | ), 195 | Ok(None) => panic!("Did not parse \"{}\" as a parameter", p), 196 | Err(e) => panic!("Did not parse \"{}\" as a parameter: {}", p, e), 197 | } 198 | } 199 | } 200 | 201 | #[test] 202 | fn test_word_literal_dollar_if_not_param() { 203 | let correct = word("$%asdf"); 204 | assert_eq!(correct, make_parser("$%asdf").word().unwrap().unwrap()); 205 | } 206 | 207 | #[test] 208 | fn test_word_does_not_capture_comments() { 209 | assert_eq!(Ok(None), make_parser("#comment\n").word()); 210 | assert_eq!(Ok(None), make_parser(" #comment\n").word()); 211 | let mut p = make_parser("word #comment\n"); 212 | p.word().unwrap().unwrap(); 213 | assert_eq!(Ok(None), p.word()); 214 | } 215 | 216 | #[test] 217 | fn test_word_pound_in_middle_is_not_comment() { 218 | let correct = word("abc#def"); 219 | assert_eq!(Ok(Some(correct)), make_parser("abc#def\n").word()); 220 | } 221 | 222 | #[test] 223 | fn test_word_tokens_which_become_literal_words() { 224 | let words = ["{", "}", "!", "name", "1notname"]; 225 | 226 | for w in &words { 227 | match make_parser(w).word() { 228 | Ok(Some(res)) => { 229 | let correct = word(*w); 230 | if correct != res { 231 | panic!( 232 | "Unexpectedly parsed \"{}\": expected:\n{:#?}\ngot:\n{:#?}", 233 | w, correct, res 234 | ); 235 | } 236 | } 237 | Ok(None) => panic!("Did not parse \"{}\" as a word", w), 238 | Err(e) => panic!("Did not parse \"{}\" as a word: {}", w, e), 239 | } 240 | } 241 | } 242 | 243 | #[test] 244 | fn test_word_concatenation_works() { 245 | let correct = TopLevelWord(Concat(vec![ 246 | lit("foo=bar"), 247 | Word::DoubleQuoted(vec![Literal(String::from("double"))]), 248 | Word::SingleQuoted(String::from("single")), 249 | ])); 250 | 251 | assert_eq!( 252 | Ok(Some(correct)), 253 | make_parser("foo=bar\"double\"'single'").word() 254 | ); 255 | } 256 | 257 | #[test] 258 | fn test_word_special_words_recognized_as_such() { 259 | assert_eq!( 260 | Ok(Some(TopLevelWord(Single(Word::Simple(Star))))), 261 | make_parser("*").word() 262 | ); 263 | assert_eq!( 264 | Ok(Some(TopLevelWord(Single(Word::Simple(Question))))), 265 | make_parser("?").word() 266 | ); 267 | assert_eq!( 268 | Ok(Some(TopLevelWord(Single(Word::Simple(Tilde))))), 269 | make_parser("~").word() 270 | ); 271 | assert_eq!( 272 | Ok(Some(TopLevelWord(Single(Word::Simple(SquareOpen))))), 273 | make_parser("[").word() 274 | ); 275 | assert_eq!( 276 | Ok(Some(TopLevelWord(Single(Word::Simple(SquareClose))))), 277 | make_parser("]").word() 278 | ); 279 | assert_eq!( 280 | Ok(Some(TopLevelWord(Single(Word::Simple(Colon))))), 281 | make_parser(":").word() 282 | ); 283 | } 284 | 285 | #[test] 286 | fn test_word_backslash_makes_things_literal() { 287 | let lit = ["a", "&", ";", "(", "*", "?", "$"]; 288 | 289 | for l in &lit { 290 | let src = format!("\\{}", l); 291 | match make_parser(&src).word() { 292 | Ok(Some(res)) => { 293 | let correct = word_escaped(l); 294 | if correct != res { 295 | panic!( 296 | "Unexpectedly parsed \"{}\": expected:\n{:#?}\ngot:\n{:#?}", 297 | src, correct, res 298 | ); 299 | } 300 | } 301 | Ok(None) => panic!("Did not parse \"{}\" as a word", src), 302 | Err(e) => panic!("Did not parse \"{}\" as a word: {}", src, e), 303 | } 304 | } 305 | } 306 | 307 | #[test] 308 | fn test_word_escaped_newline_becomes_whitespace() { 309 | let mut p = make_parser("foo\\\nbar"); 310 | assert_eq!(Ok(Some(word("foo"))), p.word()); 311 | assert_eq!(Ok(Some(word("bar"))), p.word()); 312 | } 313 | --------------------------------------------------------------------------------