├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── filenamegen.yml │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.md ├── README.md ├── build.rs ├── filenamegen ├── Cargo.toml ├── LICENSE.md ├── README.md └── src │ ├── lib.rs │ ├── node.rs │ ├── nodewalker.rs │ ├── parser.rs │ ├── recursivewalker.rs │ └── token.rs ├── pathsearch ├── Cargo.toml ├── LICENSE.md ├── README.md └── src │ ├── lib.rs │ ├── unix.rs │ └── windows.rs ├── shell_compiler ├── Cargo.toml ├── LICENSE.md └── src │ ├── lib.rs │ └── registeralloc.rs ├── shell_lexer ├── Cargo.toml ├── LICENSE.md └── src │ ├── errors.rs │ ├── lexer.rs │ ├── lib.rs │ ├── position.rs │ ├── reader.rs │ └── tokenenum.rs ├── shell_parser ├── Cargo.toml ├── LICENSE.md └── src │ ├── lib.rs │ ├── parser.rs │ ├── test.rs │ └── types.rs ├── shell_vm ├── Cargo.toml ├── LICENSE.md └── src │ ├── environment.rs │ ├── host.rs │ ├── ioenv.rs │ ├── lib.rs │ └── op.rs └── src ├── bin └── ls.rs ├── builtins ├── builtins.rs ├── colon.rs ├── echo.rs ├── env.rs ├── history.rs ├── jobcontrol.rs ├── mod.rs ├── truefalse.rs ├── which.rs └── workingdir.rs ├── errorprint.rs ├── exitstatus.rs ├── job.rs ├── main.rs ├── repl.rs ├── script.rs └── shellhost.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wez 2 | patreon: WezFurlong 3 | ko_fi: wezfurlong 4 | liberapay: wez 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | groups: 11 | all: 12 | patterns: 13 | - "*" 14 | 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | -------------------------------------------------------------------------------- /.github/workflows/filenamegen.yml: -------------------------------------------------------------------------------- 1 | name: filenamegen 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: "Install Rust" 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | toolchain: "stable" 24 | components: "rustfmt" 25 | - name: "Cache cargo registry" 26 | uses: actions/cache@v4 27 | with: 28 | path: "~/.cargo/registry" 29 | key: "filenamegen-${{ matrix.os }}-${{ hashFiles('Cargo.toml') }}-cargo-registry" 30 | - name: "Cache cargo index" 31 | uses: actions/cache@v4 32 | with: 33 | path: "~/.cargo/git" 34 | key: "filenamegen-${{ matrix.os }}-${{ hashFiles('Cargo.toml') }}-cargo-index" 35 | - name: "Cache cargo build" 36 | uses: actions/cache@v4 37 | with: 38 | path: "target" 39 | key: "filenamegen-${{ matrix.os }}-${{ hashFiles('Cargo.toml') }}-cargo-build-target" 40 | - name: Test 41 | run: cargo test -p filenamegen 42 | - name: Build 43 | run: cargo build -p filenamegen --release 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: "Install Rust" 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: "stable" 22 | components: "rustfmt" 23 | - name: "Cache cargo registry" 24 | uses: actions/cache@v4 25 | with: 26 | path: "~/.cargo/registry" 27 | key: "ubuntu-${{ hashFiles('Cargo.toml') }}-cargo-registry" 28 | - name: "Cache cargo index" 29 | uses: actions/cache@v4 30 | with: 31 | path: "~/.cargo/git" 32 | key: "ubuntu-${{ hashFiles('Cargo.toml') }}-cargo-index" 33 | - name: "Cache cargo build" 34 | uses: actions/cache@v4 35 | with: 36 | path: "target" 37 | key: "ubuntu-${{ hashFiles('Cargo.toml') }}-cargo-build-target" 38 | - name: Check formatting 39 | run: cargo fmt --all -- --check 40 | - name: Build 41 | run: cargo build --all --release 42 | - name: Test 43 | run: cargo test --all 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macos 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | runs-on: "macos-latest" 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: "Install Rust" 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: "stable" 22 | components: "rustfmt" 23 | - name: "Cache cargo registry" 24 | uses: actions/cache@v4 25 | with: 26 | path: "~/.cargo/registry" 27 | key: "macos-${{ hashFiles('Cargo.toml') }}-cargo-registry" 28 | - name: "Cache cargo index" 29 | uses: actions/cache@v4 30 | with: 31 | path: "~/.cargo/git" 32 | key: "macos-${{ hashFiles('Cargo.toml') }}-cargo-index" 33 | - name: "Cache cargo build" 34 | uses: actions/cache@v4 35 | with: 36 | path: "target" 37 | key: "macos-${{ hashFiles('Cargo.toml') }}-cargo-build-target" 38 | - name: Check formatting 39 | run: cargo fmt --all -- --check 40 | - name: Build 41 | run: cargo build --all --release 42 | - name: Test 43 | run: cargo test --all 44 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | runs-on: "windows-latest" 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: "Install Rust" 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: "stable" 22 | components: "rustfmt" 23 | target: "x86_64-pc-windows-msvc" 24 | - name: "Cache cargo registry" 25 | uses: actions/cache@v4 26 | with: 27 | path: "~/.cargo/registry" 28 | key: "windows-x86_64-pc-windows-msvc-${{ hashFiles('Cargo.toml') }}-cargo-registry" 29 | - name: "Cache cargo index" 30 | uses: actions/cache@v4 31 | with: 32 | path: "~/.cargo/git" 33 | key: "windows-x86_64-pc-windows-msvc-${{ hashFiles('Cargo.toml') }}-cargo-index" 34 | - name: "Cache cargo build" 35 | uses: actions/cache@v4 36 | with: 37 | path: "target" 38 | key: "windows-x86_64-pc-windows-msvc-${{ hashFiles('Cargo.toml') }}-cargo-build-target" 39 | - name: Check formatting 40 | run: cargo fmt --all -- --check 41 | - name: Build 42 | run: cargo build --all --release 43 | - name: Test 44 | run: cargo test --all 45 | - name: Move Windows Package 46 | shell: bash 47 | run: | 48 | mkdir pkg_ 49 | mv target/release/*.exe pkg_ 50 | - uses: actions/upload-artifact@master 51 | with: 52 | name: windows 53 | path: pkg_ 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /Cargo.lock 3 | /target/ 4 | **/*.rs.bk 5 | .*.sw* 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - beta 6 | 7 | os: 8 | - osx 9 | - linux 10 | - windows 11 | 12 | dist: xenial 13 | 14 | matrix: 15 | allow_failures: 16 | - rust: beta 17 | - os: windows 18 | 19 | script: 20 | - cargo test --all 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wzsh" 3 | version = "0.1.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | default-run = "wzsh" 7 | 8 | [build-dependencies] 9 | vergen = "3" 10 | 11 | [dependencies] 12 | atty = "0.2" 13 | cancel = "0.1" 14 | dirs-next = "2.0" 15 | anyhow = "1.0" 16 | filedescriptor = "0.8" 17 | lazy_static = "1.3" 18 | libc = "0.2" 19 | filenamegen = { path = "filenamegen" } 20 | shell_compiler = { path = "shell_compiler" } 21 | shell_lexer = { path = "shell_lexer" } 22 | shell_parser = { path = "shell_parser" } 23 | shell_vm = { path = "shell_vm" } 24 | structopt = "0.2" 25 | pathsearch = { path = "pathsearch" } 26 | chrono = "0.4" 27 | sqlite = "0.25" 28 | 29 | [dependencies.tabout] 30 | version="0.3" 31 | #path = "../wezterm/tabout" 32 | #git = "https://github.com/wez/wezterm.git" 33 | 34 | [dependencies.termwiz] 35 | version="0.8" 36 | #path = "../wezterm/termwiz" 37 | #git = "https://github.com/wez/wezterm.git" 38 | 39 | [target."cfg(windows)".dependencies] 40 | winapi = { version = "0.3", features = [ 41 | "aclapi", 42 | "winuser", 43 | "handleapi", 44 | "synchapi", 45 | "fileapi", 46 | "processthreadsapi", 47 | ]} 48 | 49 | [dev-dependencies] 50 | pretty_assertions = "0.6" 51 | 52 | [workspace] 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wez Furlong 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wzsh - Wez's Shell 2 | 3 | A unixy interactive shell for Posix and Windows systems 4 | 5 | [![Build Status](https://travis-ci.org/wez/wzsh.svg?branch=master)](https://travis-ci.org/wez/wzsh) 6 | 7 | ## Goals 8 | 9 | * Be a convenient interactive shell 10 | * Feel familiar to long-time unix users by using the Bourne syntax 11 | * Have discoverable builtins and help 12 | * Run on Windows without requiring cygwin, msys or wsl 13 | 14 | ## Non-Goals 15 | 16 | * I don't want to replace `/bin/sh` or `/bin/bash` shebang usage. 17 | I don't believe in long shell scripts and I don't think wzsh 18 | should try to compete in that space. 19 | [More information](https://github.com/wez/wzsh/issues/2) 20 | 21 | ## Implementation Status 22 | 23 | In no particular order, except that completed items bubble up to the top: 24 | 25 | * [x] - Executes simple commands, pipelines, input/output redirection 26 | * [x] - Parameter substitution ($FOO) 27 | * [x] - Globbing and filename generation 28 | * [x] - Basic job control (ctrl-z to background, `bg` and `fg` to manage a backgrounded job) 29 | * [x] - Define and execute functions 30 | * [x] - Conditionals of the form `true && echo yes` and `if`/`then`/`else`/`elif`/`fi` 31 | * [x] - line editor functions that can search and match history (ctrl-R!) 32 | * [x] - persistent history and builtins for examining history 33 | * [ ] - looping constructs such as `for`, `while`, `until` 34 | * [ ] - `case`/`esac` matching construct 35 | * [ ] - tab completion of commands, filesystem entries 36 | * [ ] - command substitution `$(date)` 37 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{generate_cargo_keys, ConstantsFlags}; 2 | 3 | fn main() { 4 | // Setup the flags, toggling off the 'SEMVER_FROM_CARGO_PKG' flag 5 | let mut flags = ConstantsFlags::all(); 6 | flags.toggle(ConstantsFlags::SEMVER_FROM_CARGO_PKG); 7 | 8 | // Generate the 'cargo:' key output 9 | generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); 10 | } 11 | -------------------------------------------------------------------------------- /filenamegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "filenamegen" 3 | version = "0.2.7" 4 | authors = ["Wez Furlong"] 5 | edition = "2021" 6 | description = "Shell-style filename generation aka globbing" 7 | license = "MIT" 8 | documentation = "https://docs.rs/filenamegen" 9 | repository = "https://github.com/wez/wzsh" 10 | keywords = ["glob", "wildmatch", "filenamegen", "fnmatch"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | bstr = "1.0" 15 | anyhow = "1.0" 16 | regex = "1.10" 17 | walkdir = "2.5" 18 | 19 | [dev-dependencies] 20 | pretty_assertions = "1.4" 21 | tempfile = "3.10" 22 | 23 | [target.'cfg(unix)'.dev-dependencies] 24 | nix = {version="0.28", features=["feature"]} 25 | -------------------------------------------------------------------------------- /filenamegen/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /filenamegen/README.md: -------------------------------------------------------------------------------- 1 | # filenamegen 2 | 3 | ### Filename Generation, aka Globbing. 4 | 5 | This crate implements shell style file name generation a.k.a.: globbing. 6 | The provided globber can expand globs relative to a specified directory (or 7 | just the current working directory). `filenamegen` tries to avoid 8 | walking down paths that will never match a glob in order to reduce 9 | pressure on the underlying filesystem. 10 | 11 | This simple example recursively finds all of the rust source files under 12 | the current directory. 13 | 14 | ```rust 15 | use filenamegen::Glob; 16 | 17 | fn main() -> anyhow::Result<()> { 18 | let glob = Glob::new("**/*.rs")?; 19 | for path in glob.walk(std::env::current_dir()?) { 20 | println!("{}", path.display()); 21 | } 22 | Ok(()) 23 | } 24 | ``` 25 | 26 | License: MIT 27 | -------------------------------------------------------------------------------- /filenamegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ## Filename Generation, aka Globbing. 2 | //! 3 | //! This crate implements shell style file name generation a.k.a.: globbing. 4 | //! The provided globber can expand globs relative to a specified directory (or 5 | //! just the current working directory). `filenamegen` tries to avoid 6 | //! walking down paths that will never match a glob in order to reduce 7 | //! pressure on the underlying filesystem. 8 | //! 9 | //! This simple example recursively finds all of the rust source files under 10 | //! the current directory. 11 | //! 12 | //! ``` 13 | //! use filenamegen::Glob; 14 | //! 15 | //! fn main() -> anyhow::Result<()> { 16 | //! let glob = Glob::new("**/*.rs")?; 17 | //! for path in glob.walk(std::env::current_dir()?) { 18 | //! println!("{}", path.display()); 19 | //! } 20 | //! Ok(()) 21 | //! } 22 | //! ``` 23 | 24 | use std::collections::VecDeque; 25 | use std::path::{Component, Path, PathBuf}; 26 | 27 | mod node; 28 | mod nodewalker; 29 | mod parser; 30 | mod recursivewalker; 31 | mod token; 32 | use node::Node; 33 | use nodewalker::NodeWalker; 34 | use parser::parse; 35 | use recursivewalker::RecursiveWalker; 36 | 37 | /// Represents a compiled glob expression. 38 | /// Depending on the pattern, evaluating the glob may use a conservative 39 | /// walker that tries to minimize the number of syscalls to just the 40 | /// directories in which pattern matching needs to occur. 41 | /// If the recursive glob `**` pattern is used then we have no choice 42 | /// but to perform a full tree walk for the appropriate portions of 43 | /// the filesystem. 44 | #[derive(Debug)] 45 | pub struct Glob { 46 | nodes: Vec, 47 | } 48 | 49 | impl Glob { 50 | /// Compile pattern into a `Glob` 51 | /// Special characters allowed in pattern: 52 | /// `?` match any single non-directory separator character, 53 | /// except for a leading `.` in the directory entry name 54 | /// (this allows hiding files using the unix convention 55 | /// of a leading dot) 56 | /// 57 | /// `*` like `?` except matches 0 or more characters. 58 | /// 59 | /// `\` quotes the character that follows it, preventing it from 60 | /// interpreted as a special character. 61 | /// 62 | /// `**`, when used in its own non-leaf directory component, acts as a 63 | /// recursive wildcard, matching any number of directories. 64 | /// When used in the leaf position it acts the same as `*`. 65 | /// 66 | /// `{foo,bar}.rs` matches both `foo.rs` and `bar.rs`. The curly braces 67 | /// define an alternation regex. 68 | pub fn new(pattern: &str) -> anyhow::Result { 69 | let mut nodes: Vec = vec![]; 70 | for comp in Path::new(pattern).components() { 71 | let token = match comp { 72 | Component::Prefix(s) => { 73 | nodes.clear(); 74 | Node::LiteralComponents(PathBuf::from(s.as_os_str())) 75 | } 76 | Component::RootDir => { 77 | if nodes.len() == 1 && nodes[0].is_literal_prefix_component() { 78 | // Retain any prefix component; it it logically part 79 | // of this new RootDir 80 | } else { 81 | nodes.clear(); 82 | } 83 | Node::LiteralComponents(PathBuf::from("/")) 84 | } 85 | Component::CurDir => continue, 86 | Component::ParentDir => Node::LiteralComponents(PathBuf::from("..")), 87 | Component::Normal(s) => { 88 | let s = s.to_str().expect("str input to be representable as string"); 89 | 90 | // Let's see if this component contains a pattern 91 | match s { 92 | "**" => Node::RecursiveMatch, 93 | _ => parse(s)?, 94 | } 95 | } 96 | }; 97 | 98 | // Collapse contiguous LiteralComponents into a single Node 99 | match (&token, nodes.last_mut()) { 100 | ( 101 | Node::LiteralComponents(ref literal), 102 | Some(Node::LiteralComponents(ref mut path)), 103 | ) => *path = path.join(literal), 104 | _ => nodes.push(token), 105 | } 106 | } 107 | 108 | Ok(Glob { nodes }) 109 | } 110 | 111 | /// Walk the filesystem starting at `path` and execute the glob. 112 | /// Returns all matching entries in sorted order. The entries are 113 | /// relative to `path`. 114 | pub fn walk>(&self, path: P) -> Vec { 115 | let walker = Walker::new(path.as_ref(), &self.nodes); 116 | 117 | let mut results: Vec = walker.collect(); 118 | results.sort(); 119 | 120 | results 121 | } 122 | } 123 | 124 | /// Disable unicode mode so that we can match non-utf8 filenames 125 | fn new_binary_pattern_string() -> String { 126 | String::from(if cfg!(windows) { "^(?i-u)" } else { "^(?-u)" }) 127 | } 128 | 129 | /// This is triply gross because the string needs to be translated 130 | /// from WTF-8 to UCS-2, normalized, and then re-encoded back to WTF-8 131 | #[cfg(windows)] 132 | fn normalize_slashes(path: PathBuf) -> PathBuf { 133 | use std::ffi::OsString; 134 | use std::os::windows::ffi::{OsStrExt, OsStringExt}; 135 | 136 | let mut normalized: Vec = path 137 | .into_os_string() 138 | .encode_wide() 139 | .map(|c| if c == b'\\' as u16 { b'/' as u16 } else { c }) 140 | .collect(); 141 | 142 | // Strip off the normalized long filename prefix. 143 | const LONG_FILE_NAME_PREFIX: [u16; 4] = [b'/' as u16, b'/' as u16, b'?' as u16, b'/' as u16]; 144 | if normalized.starts_with(&LONG_FILE_NAME_PREFIX) { 145 | for _ in 0..LONG_FILE_NAME_PREFIX.len() { 146 | normalized.remove(0); 147 | } 148 | } 149 | 150 | OsString::from_wide(&normalized).into() 151 | } 152 | 153 | #[cfg(not(windows))] 154 | fn normalize_slashes(path: PathBuf) -> PathBuf { 155 | path 156 | } 157 | 158 | /// `Walker` is the iterator implementation that drives 159 | /// executing the glob. It tracks both the regular walker 160 | /// and the recursive walkers. The regular walkers are evaluated 161 | /// before the recursive walkers. 162 | struct Walker<'a> { 163 | root: &'a Path, 164 | stack: VecDeque>, 165 | recursive: VecDeque, 166 | } 167 | 168 | impl<'a> Walker<'a> { 169 | fn new(root: &'a Path, nodes: &'a [Node]) -> Self { 170 | let route = NodeWalker::new(nodes); 171 | let mut stack = VecDeque::new(); 172 | stack.push_back(route); 173 | Self { 174 | root, 175 | stack, 176 | recursive: VecDeque::new(), 177 | } 178 | } 179 | } 180 | 181 | impl<'a> Iterator for Walker<'a> { 182 | type Item = PathBuf; 183 | 184 | fn next(&mut self) -> Option { 185 | while let Some(mut route) = self.stack.pop_front() { 186 | if let Some(path) = route.next(self) { 187 | self.stack.push_front(route); 188 | return Some(path); 189 | } 190 | } 191 | 192 | while let Some(mut route) = self.recursive.pop_front() { 193 | if let Some(path) = route.next(self) { 194 | self.recursive.push_front(route); 195 | return Some(path); 196 | } 197 | } 198 | 199 | None 200 | } 201 | } 202 | 203 | #[cfg(test)] 204 | mod test { 205 | use super::*; 206 | use pretty_assertions::assert_eq; 207 | use tempfile::TempDir; 208 | 209 | #[allow(unused)] 210 | fn make_dirs_in(root: &TempDir, dirs: &[&str]) -> anyhow::Result<()> { 211 | for d in dirs { 212 | let p = root.path().join(d); 213 | std::fs::create_dir_all(p)?; 214 | } 215 | Ok(()) 216 | } 217 | 218 | fn touch_file>(path: P) -> anyhow::Result<()> { 219 | eprintln!("touch_file {}", path.as_ref().display()); 220 | let _file = std::fs::OpenOptions::new() 221 | .write(true) 222 | .create_new(true) 223 | .open(path.as_ref())?; 224 | Ok(()) 225 | } 226 | 227 | fn touch_files_in(root: &TempDir, files: &[&str]) -> anyhow::Result<()> { 228 | for f in files { 229 | let p = root.path().join(f); 230 | let d = p.parent().unwrap(); 231 | std::fs::create_dir_all(d)?; 232 | touch_file(p)?; 233 | } 234 | Ok(()) 235 | } 236 | 237 | fn make_fixture() -> anyhow::Result { 238 | #[cfg(unix)] 239 | { 240 | // Canonicalize the temp dir; on macos this location 241 | // is a symlink and that messes with some test assertions 242 | std::env::set_var("TMPDIR", std::env::temp_dir().canonicalize()?); 243 | } 244 | Ok(TempDir::new()?) 245 | } 246 | 247 | #[test] 248 | fn test_simple() -> anyhow::Result<()> { 249 | let root = make_fixture()?; 250 | touch_files_in(&root, &["src/lib.rs"])?; 251 | let glob = Glob::new("src/*.rs")?; 252 | assert_eq!(glob.walk(root), vec![PathBuf::from("src/lib.rs")]); 253 | Ok(()) 254 | } 255 | 256 | #[test] 257 | fn test_non_relative() -> anyhow::Result<()> { 258 | let root = make_fixture()?; 259 | touch_files_in(&root, &["src/lib.rs"])?; 260 | let glob = Glob::new(&format!( 261 | "{}/src/*.rs", 262 | normalize_slashes(root.path().to_path_buf()).display() 263 | ))?; 264 | assert_eq!( 265 | glob.walk(&std::env::current_dir()?), 266 | vec![normalize_slashes(root.path().join("src/lib.rs"))] 267 | ); 268 | Ok(()) 269 | } 270 | 271 | #[test] 272 | fn non_utf8_node_match() -> anyhow::Result<()> { 273 | let node = parse("*.rs")?; 274 | use bstr::BStr; 275 | use bstr::B; 276 | let pound = BStr::new(B(b"\xa3.rs")); 277 | 278 | eprintln!("pound is {:?}", pound); 279 | eprintln!("node is {:?}", node); 280 | assert_eq!(node.is_match(£), true); 281 | 282 | Ok(()) 283 | } 284 | 285 | #[test] 286 | fn spaces_and_parens() -> anyhow::Result<()> { 287 | let root = make_fixture()?; 288 | touch_files_in(&root, &["Program Files (x86)/Foo Bar/baz.exe"])?; 289 | 290 | let glob = Glob::new("Program Files (x86)/*")?; 291 | assert_eq!( 292 | glob.walk(&root.path()), 293 | vec![PathBuf::from("Program Files (x86)/Foo Bar")] 294 | ); 295 | 296 | let glob = Glob::new( 297 | normalize_slashes(root.path().join("Program Files (x86)/*")) 298 | .to_str() 299 | .unwrap(), 300 | )?; 301 | 302 | assert_eq!( 303 | glob.walk(&root), 304 | vec![root.path().join("Program Files (x86)/Foo Bar")] 305 | ); 306 | assert_eq!( 307 | glob.walk(&std::env::current_dir()?), 308 | vec![root.path().join("Program Files (x86)/Foo Bar")] 309 | ); 310 | 311 | let glob = Glob::new( 312 | normalize_slashes(root.path().join("Program Files (x86)/*/baz.exe")) 313 | .to_str() 314 | .unwrap(), 315 | )?; 316 | assert_eq!( 317 | glob.walk(&std::env::current_dir()?), 318 | vec![root.path().join("Program Files (x86)/Foo Bar/baz.exe")] 319 | ); 320 | Ok(()) 321 | } 322 | 323 | #[test] 324 | #[cfg(windows)] 325 | fn case_insensitive() -> anyhow::Result<()> { 326 | let node = parse("foo/bar.rs")?; 327 | use bstr::B; 328 | use bstr::BStr; 329 | let upper = B(b"FOO/bAr.rs"); 330 | 331 | assert_eq!(node.is_match(BStr::new(&upper)), true); 332 | Ok(()) 333 | } 334 | 335 | #[test] 336 | #[cfg(all(unix, not(target_os = "macos")))] 337 | fn test_non_utf8_on_disk() -> anyhow::Result<()> { 338 | use bstr::ByteSlice; 339 | use bstr::B; 340 | 341 | if nix::sys::utsname::uname() 342 | .unwrap() 343 | .release() 344 | .to_str() 345 | .unwrap() 346 | .contains("Microsoft") 347 | { 348 | // If we're running on WSL the filesystem has 349 | // tigher restrictions 350 | return Ok(()); 351 | } 352 | 353 | let root = make_fixture()?; 354 | let pound = B(b"\xa3.rs").to_path()?; 355 | // Some operating systems/filesystems won't allow us to create invalid utf8 names 356 | if touch_file(root.path().join(£)).is_ok() { 357 | let glob = Glob::new("*.rs")?; 358 | assert_eq!(glob.walk(root), vec![pound.to_path_buf()]); 359 | } 360 | Ok(()) 361 | } 362 | 363 | #[test] 364 | fn test_lua_toml() -> anyhow::Result<()> { 365 | let root = make_fixture()?; 366 | touch_files_in( 367 | &root, 368 | &["simple.lua", "assets/policy-extras/bar.lua", "assets/policy-extras/shaping.toml"], 369 | )?; 370 | let glob = Glob::new("**/*.{lua,toml}")?; 371 | assert_eq!( 372 | glob.walk(&root), 373 | vec![ 374 | PathBuf::from("assets/policy-extras/bar.lua"), 375 | PathBuf::from("assets/policy-extras/shaping.toml"), 376 | PathBuf::from("simple.lua"), 377 | ] 378 | ); 379 | 380 | Ok(()) 381 | } 382 | 383 | #[test] 384 | fn test_more() -> anyhow::Result<()> { 385 | let root = make_fixture()?; 386 | touch_files_in( 387 | &root, 388 | &["foo/src/foo.rs", "bar/src/bar.rs", "bar/src/.bar.rs"], 389 | )?; 390 | let glob = Glob::new("*/src/*.rs")?; 391 | assert_eq!( 392 | glob.walk(&root), 393 | vec![ 394 | PathBuf::from("bar/src/bar.rs"), 395 | PathBuf::from("foo/src/foo.rs") 396 | ] 397 | ); 398 | 399 | let glob = Glob::new("foo/src/*.rs")?; 400 | assert_eq!(glob.walk(&root), vec![PathBuf::from("foo/src/foo.rs")]); 401 | 402 | let glob = Glob::new("*/src/.*.rs")?; 403 | assert_eq!(glob.walk(&root), vec![PathBuf::from("bar/src/.bar.rs")]); 404 | 405 | let glob = Glob::new("*")?; 406 | assert_eq!( 407 | glob.walk(&root), 408 | vec![PathBuf::from("bar"), PathBuf::from("foo")] 409 | ); 410 | Ok(()) 411 | } 412 | 413 | #[test] 414 | fn test_doublestar() -> anyhow::Result<()> { 415 | let root = make_fixture()?; 416 | touch_files_in( 417 | &root, 418 | &[ 419 | "foo/src/foo.rs", 420 | "bar/src/bar.rs", 421 | "woot/woot.rs", 422 | "woot/.woot.rs", 423 | ], 424 | )?; 425 | let glob = Glob::new("**/*.rs")?; 426 | assert_eq!( 427 | glob.walk(&root), 428 | vec![ 429 | PathBuf::from("bar/src/bar.rs"), 430 | PathBuf::from("foo/src/foo.rs"), 431 | PathBuf::from("woot/woot.rs") 432 | ] 433 | ); 434 | 435 | let glob = Glob::new("**")?; 436 | assert_eq!( 437 | glob.walk(&root), 438 | vec![ 439 | PathBuf::from("bar"), 440 | PathBuf::from("foo"), 441 | PathBuf::from("woot") 442 | ] 443 | ); 444 | Ok(()) 445 | } 446 | 447 | #[test] 448 | fn glob_up() -> anyhow::Result<()> { 449 | let root = make_fixture()?; 450 | touch_files_in( 451 | &root, 452 | &[ 453 | "foo/src/foo.rs", 454 | "bar/src/bar.rs", 455 | "woot/woot.rs", 456 | "woot/.woot.rs", 457 | ], 458 | )?; 459 | let glob = Glob::new("../*/*.rs")?; 460 | assert_eq!( 461 | glob.walk(root.path().join("woot")), 462 | vec![PathBuf::from("../woot/woot.rs")] 463 | ); 464 | Ok(()) 465 | } 466 | 467 | #[test] 468 | fn alternative() -> anyhow::Result<()> { 469 | let root = make_fixture()?; 470 | touch_files_in(&root, &["foo.rs", "bar.rs"])?; 471 | let glob = Glob::new("{foo,bar}.rs")?; 472 | assert_eq!( 473 | glob.walk(&root), 474 | vec![PathBuf::from("bar.rs"), PathBuf::from("foo.rs")] 475 | ); 476 | Ok(()) 477 | } 478 | 479 | #[test] 480 | fn bogus_alternative() -> anyhow::Result<()> { 481 | assert_eq!( 482 | format!("{}", Glob::new("{{").unwrap_err()), 483 | "cannot start an alternative inside an alternative" 484 | ); 485 | assert_eq!( 486 | format!("{}", Glob::new("{").unwrap_err()), 487 | "missing closing alternative" 488 | ); 489 | Ok(()) 490 | } 491 | 492 | #[test] 493 | fn class() -> anyhow::Result<()> { 494 | let root = make_fixture()?; 495 | touch_files_in(&root, &["foo.o", "foo.a"])?; 496 | let glob = Glob::new("foo.[oa]")?; 497 | assert_eq!( 498 | glob.walk(&root), 499 | vec![PathBuf::from("foo.a"), PathBuf::from("foo.o")] 500 | ); 501 | let glob = Glob::new("foo.[[:alnum:]]")?; 502 | assert_eq!( 503 | glob.walk(&root), 504 | vec![PathBuf::from("foo.a"), PathBuf::from("foo.o")] 505 | ); 506 | let glob = Glob::new("foo.[![:alnum:]]")?; 507 | assert_eq!(glob.walk(&root), Vec::::new()); 508 | Ok(()) 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /filenamegen/src/node.rs: -------------------------------------------------------------------------------- 1 | use crate::token::Token; 2 | use bstr::BStr; 3 | use bstr::BString; 4 | use bstr::ByteSlice; 5 | use bstr::ByteVec; 6 | use regex::bytes::Regex; 7 | use std::path::PathBuf; 8 | 9 | #[derive(Debug)] 10 | pub enum Node { 11 | LiteralComponents(PathBuf), 12 | RecursiveMatch, 13 | Regex(RegexAndTokens), 14 | } 15 | 16 | #[derive(Debug)] 17 | pub struct RegexAndTokens { 18 | regex: Regex, 19 | tokens: Vec, 20 | } 21 | 22 | impl RegexAndTokens { 23 | pub fn new(regex: Regex, tokens: Vec) -> Self { 24 | Self { regex, tokens } 25 | } 26 | } 27 | 28 | #[allow(unused)] 29 | fn normalize_and_lower_case(s: &BStr) -> BString { 30 | let mut norm = BString::new(vec![]); 31 | for c in s.chars() { 32 | if c == '/' { 33 | norm.push_char('\\'); 34 | } else { 35 | for lower in c.to_lowercase() { 36 | norm.push_char(lower); 37 | } 38 | } 39 | } 40 | norm 41 | } 42 | 43 | impl Node { 44 | pub fn is_match(&self, s: &BStr) -> bool { 45 | match self { 46 | #[cfg(not(windows))] 47 | Node::LiteralComponents(p) => p.as_path() == s.to_path_lossy(), 48 | #[cfg(windows)] 49 | Node::LiteralComponents(p) => { 50 | if let Some(bytes) = <[u8]>::from_path(p.as_path()) { 51 | normalize_and_lower_case(BStr::new(bytes)) == normalize_and_lower_case(s) 52 | } else { 53 | // If we couldn't convert ourselves to a bstr, then it 54 | // cannot possibly be compared to a bstr 55 | false 56 | } 57 | } 58 | Node::RecursiveMatch => true, 59 | Node::Regex(RegexAndTokens { regex, .. }) => regex.is_match(s.as_bytes()), 60 | } 61 | } 62 | 63 | pub fn is_literal_prefix_component(&self) -> bool { 64 | match self { 65 | Node::LiteralComponents(p) => match p.components().next() { 66 | Some(std::path::Component::Prefix(_)) => true, 67 | _ => false, 68 | }, 69 | 70 | _ => false, 71 | } 72 | } 73 | 74 | /// Convenience for testing whether Node is RecursiveMatch 75 | pub fn is_recursive(&self) -> bool { 76 | match self { 77 | Node::RecursiveMatch => true, 78 | _ => false, 79 | } 80 | } 81 | 82 | /// Append a regex representation of Node to the supplied pattern string 83 | pub fn append_regex(&self, pattern: &mut String) { 84 | match self { 85 | Node::LiteralComponents(path) => pattern.push_str(®ex::escape( 86 | path.to_str() 87 | .expect("pattern to be convertible back to String"), 88 | )), 89 | #[cfg(windows)] 90 | Node::RecursiveMatch => pattern.push_str("([^./\\\\][^/\\\\]*[/\\\\]?)*"), 91 | #[cfg(not(windows))] 92 | Node::RecursiveMatch => pattern.push_str("([^./][^/]*/?)*"), 93 | Node::Regex(RegexAndTokens { tokens, .. }) => { 94 | for (i, token) in tokens.iter().enumerate() { 95 | token.append_regex(pattern, i == 0); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /filenamegen/src/nodewalker.rs: -------------------------------------------------------------------------------- 1 | use crate::node::Node; 2 | use crate::normalize_slashes; 3 | use crate::recursivewalker::RecursiveWalker; 4 | use crate::Walker; 5 | use bstr::BStr; 6 | use bstr::ByteSlice; 7 | use std::ffi::OsStr; 8 | use std::path::PathBuf; 9 | 10 | /// A simple node-by-node walker 11 | #[derive(Debug)] 12 | pub struct NodeWalker<'a> { 13 | node: std::iter::Peekable>, 14 | node_to_match: Option<&'a Node>, 15 | current_dir: PathBuf, 16 | dir: Option, 17 | current_dir_has_literals: bool, 18 | } 19 | 20 | fn entry_may_be_dir(entry: &std::fs::DirEntry) -> bool { 21 | match entry.file_type() { 22 | // The entry is a regular file and can't be opened as a dir. 23 | Ok(file_type) if file_type.is_file() => false, 24 | // Could be a dir or a symlink. For the symlink case 25 | // we won't know if the target is a file or a dir without 26 | // stating it. In the case that that stat call tells us it 27 | // is a symlink we then need to perform an opendir() on it. 28 | // We can save the stat and just try to open the dir, so we 29 | // return true for both dir and symlink and let the kernel 30 | // tell us that the opendir failed. 31 | Ok(_) => true, 32 | // Failed to query the file type, which most likely means that 33 | // we lack access rights, so we skip it. 34 | _ => false, 35 | } 36 | } 37 | 38 | impl<'a> NodeWalker<'a> { 39 | pub fn new(nodes: &'a [Node]) -> Self { 40 | Self { 41 | node: nodes.iter().peekable(), 42 | node_to_match: None, 43 | current_dir: PathBuf::new(), 44 | dir: None, 45 | current_dir_has_literals: false, 46 | } 47 | } 48 | 49 | /// Fork off a new NodeWalker to follow a sub-dir 50 | fn fork(&self, child_dir: &OsStr) -> Self { 51 | Self { 52 | node: self.node.clone(), 53 | node_to_match: None, 54 | current_dir: self.current_dir.join(child_dir), 55 | dir: None, 56 | current_dir_has_literals: false, 57 | } 58 | } 59 | 60 | // Advance to the next directory component 61 | fn next_candidate_path(&mut self) -> Option<&'a Node> { 62 | while let Some(node) = self.node.next() { 63 | match node { 64 | Node::LiteralComponents(literal) => { 65 | self.current_dir = self.current_dir.join(literal); 66 | // Since we're combining a run of LiteralComponents 67 | // together without returning a corresponding Node, 68 | // we need to set a flag to remind ourselves to 69 | // check the resultant path later. 70 | self.current_dir_has_literals = true; 71 | } 72 | _ => return Some(node), 73 | } 74 | } 75 | None 76 | } 77 | 78 | /// Attempt to match the next entry by reading the dir 79 | fn next_from_dir(&mut self, walker: &mut Walker<'a>) -> Option { 80 | while let Some(entry) = self.dir.as_mut().unwrap().next() { 81 | match entry { 82 | Err(_) => continue, 83 | Ok(entry) => { 84 | let file_name = entry.path(); 85 | let base_name = file_name.file_name().unwrap(); 86 | if let Some(bstr) = <[u8]>::from_os_str(base_name) { 87 | if self 88 | .node_to_match 89 | .as_ref() 90 | .unwrap() 91 | .is_match(BStr::new(bstr)) 92 | { 93 | let is_leaf = self.node.peek().is_none(); 94 | if is_leaf { 95 | let rel_path = self.current_dir.join(base_name); 96 | return Some(normalize_slashes(rel_path)); 97 | } else if entry_may_be_dir(&entry) { 98 | // We can only really match if this non-leaf node 99 | // is a directory 100 | let route = self.fork(base_name); 101 | walker.stack.push_back(route); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | return None; 109 | } 110 | 111 | pub(crate) fn next(&mut self, walker: &mut Walker<'a>) -> Option { 112 | loop { 113 | if self.dir.is_some() { 114 | return self.next_from_dir(walker); 115 | } 116 | 117 | // Advance to the next directory component 118 | self.node_to_match = match self.next_candidate_path() { 119 | None => { 120 | // If we walked a sequence of LiteralComponents at the end 121 | // of the pattern we'll end up here without yielding a node. 122 | // In this case current_dir is the candidate path to match. 123 | if self.current_dir_has_literals { 124 | self.current_dir_has_literals = false; 125 | let candidate = walker.root.join(&self.current_dir); 126 | if candidate.exists() { 127 | return Some(normalize_slashes(self.current_dir.clone())); 128 | } 129 | } 130 | return None; 131 | } 132 | node => node, 133 | }; 134 | 135 | if self.node_to_match.as_ref().unwrap().is_recursive() { 136 | if self.node.peek().is_some() { 137 | walker.recursive.push_back(RecursiveWalker::new( 138 | self.node.clone(), 139 | walker.root.join(&self.current_dir), 140 | )); 141 | return None; 142 | } 143 | // Otherwise: a leaf recursive match is equivalent to ZeroOrMore 144 | } 145 | 146 | let name = walker.root.join(&self.current_dir); 147 | match std::fs::read_dir(&name) { 148 | Err(_) => return None, 149 | Ok(dir) => { 150 | self.dir = Some(dir); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /filenamegen/src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::new_binary_pattern_string; 2 | use crate::node::{Node, RegexAndTokens}; 3 | use crate::token::Token; 4 | use anyhow::{anyhow, ensure}; 5 | use regex::bytes::Regex; 6 | 7 | struct Parser<'a> { 8 | chars: std::iter::Peekable>, 9 | tokens: Vec, 10 | in_alternative: bool, 11 | in_class: usize, 12 | } 13 | 14 | impl<'a> Parser<'a> { 15 | fn parse(&mut self) -> anyhow::Result<()> { 16 | while let Some(c) = self.next() { 17 | match c { 18 | '\\' => { 19 | if let Some(c) = self.next() { 20 | self.tokens.push(Token::Literal(c)); 21 | } else { 22 | self.tokens.push(Token::Literal('\\')); 23 | } 24 | } 25 | '[' => { 26 | if self.in_class == 0 { 27 | self.tokens.push(Token::StartClass) 28 | } else { 29 | self.tokens.push(Token::ClassContent('[')) 30 | } 31 | self.in_class += 1; 32 | } 33 | ']' if self.in_class > 0 => { 34 | if self.in_class == 1 { 35 | self.tokens.push(Token::EndClass); 36 | } else { 37 | self.tokens.push(Token::ClassContent(']')); 38 | } 39 | self.in_class -= 1; 40 | } 41 | '!' if self.in_class > 0 => self.tokens.push(Token::NegateClass), 42 | c if self.in_class > 0 => self.tokens.push(Token::ClassContent(c)), 43 | '{' if self.in_class == 0 => { 44 | ensure!( 45 | !self.in_alternative, 46 | "cannot start an alternative inside an alternative" 47 | ); 48 | self.in_alternative = true; 49 | self.tokens.push(Token::StartAlternative) 50 | } 51 | ',' if self.in_alternative => self.tokens.push(Token::NextAlternative), 52 | '}' if self.in_alternative => { 53 | ensure!( 54 | self.in_alternative, 55 | "cannot end an alternative when not already inside an alternative" 56 | ); 57 | self.in_alternative = false; 58 | self.tokens.push(Token::EndAlternative) 59 | } 60 | '?' => self.tokens.push(Token::Any), 61 | '*' => self.tokens.push(Token::ZeroOrMore), 62 | c => self.tokens.push(Token::Literal(c)), 63 | } 64 | } 65 | ensure!(!self.in_alternative, "missing closing alternative"); 66 | ensure!(self.in_class == 0, "missing closing class"); 67 | Ok(()) 68 | } 69 | 70 | /// If the series of tokens is composed entirely of literals, 71 | /// returns them combined into a string 72 | fn collapse_literals(&mut self) -> Option { 73 | let mut s = String::with_capacity(self.tokens.len()); 74 | for t in &self.tokens { 75 | match t { 76 | Token::Literal(c) => s.push(*c), 77 | _ => return None, 78 | } 79 | } 80 | Some(s) 81 | } 82 | 83 | fn compile_to_regex(&mut self) -> anyhow::Result { 84 | let mut pattern = new_binary_pattern_string(); 85 | for (i, token) in self.tokens.iter().enumerate() { 86 | token.append_regex(&mut pattern, i == 0); 87 | } 88 | pattern.push('$'); 89 | Regex::new(&pattern).map_err(|e| anyhow!("error compiling regex: {}: {}", pattern, e)) 90 | } 91 | 92 | fn next(&mut self) -> Option { 93 | self.chars.next() 94 | } 95 | 96 | #[allow(unused)] 97 | fn peek(&mut self) -> Option { 98 | self.chars.peek().map(|&ch| ch) 99 | } 100 | } 101 | 102 | /// Parse a pattern string into a Node. 103 | pub fn parse(pattern: &str) -> anyhow::Result { 104 | let mut parser = Parser { 105 | chars: pattern.chars().peekable(), 106 | tokens: vec![], 107 | in_alternative: false, 108 | in_class: 0, 109 | }; 110 | 111 | parser.parse()?; 112 | 113 | if let Some(literal) = parser.collapse_literals() { 114 | Ok(Node::LiteralComponents(literal.into())) 115 | } else { 116 | Ok(Node::Regex(RegexAndTokens::new( 117 | parser.compile_to_regex()?, 118 | parser.tokens, 119 | ))) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /filenamegen/src/recursivewalker.rs: -------------------------------------------------------------------------------- 1 | use crate::node::Node; 2 | use crate::{new_binary_pattern_string, normalize_slashes, Walker}; 3 | use bstr::ByteSlice; 4 | use regex::bytes::Regex; 5 | use std::path::{Path, PathBuf}; 6 | 7 | #[derive(Debug)] 8 | pub struct RecursiveWalker { 9 | walk_root: PathBuf, 10 | walk: Option, 11 | regex: Regex, 12 | } 13 | 14 | impl RecursiveWalker { 15 | pub fn new<'a>( 16 | nodes: std::iter::Peekable>, 17 | walk_root: PathBuf, 18 | ) -> Self { 19 | let mut pattern = new_binary_pattern_string(); 20 | Node::RecursiveMatch.append_regex(&mut pattern); 21 | for node in nodes { 22 | #[cfg(not(windows))] 23 | pattern.push_str("/?"); 24 | #[cfg(windows)] 25 | pattern.push_str("[/\\\\]?"); 26 | node.append_regex(&mut pattern); 27 | } 28 | pattern.push('$'); 29 | let regex = Regex::new(&pattern).expect("regex to compile"); 30 | 31 | Self { 32 | regex, 33 | walk_root, 34 | walk: None, 35 | } 36 | } 37 | 38 | pub(crate) fn next<'a>(&mut self, walker: &mut Walker<'a>) -> Option { 39 | if self.walk.is_none() { 40 | self.walk = Some( 41 | walkdir::WalkDir::new(&self.walk_root) 42 | .follow_links(true) 43 | .into_iter(), 44 | ); 45 | } 46 | 47 | while let Some(entry) = self.walk.as_mut().unwrap().next() { 48 | if let Ok(entry) = entry { 49 | let path = entry 50 | .path() 51 | .strip_prefix(&self.walk_root) 52 | .expect("walk is always relative to self.walk_root"); 53 | if self.is_match(path) { 54 | return Some(normalize_slashes( 55 | entry 56 | .path() 57 | .strip_prefix(&walker.root) 58 | .expect("walk is always relative to walker.root") 59 | .to_path_buf(), 60 | )); 61 | } 62 | } 63 | } 64 | 65 | None 66 | } 67 | 68 | fn is_match(&self, path: &Path) -> bool { 69 | let matched = if let Some(bytes) = <[u8]>::from_path(path) { 70 | self.regex.is_match(bytes) 71 | } else { 72 | false 73 | }; 74 | matched 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /filenamegen/src/token.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Token { 3 | Literal(char), 4 | /// `?` 5 | Any, 6 | /// `*` 7 | ZeroOrMore, 8 | /// `{` 9 | StartAlternative, 10 | /// `,` 11 | NextAlternative, 12 | /// `}` 13 | EndAlternative, 14 | /// `[` 15 | StartClass, 16 | /// `!` 17 | NegateClass, 18 | /// `]` 19 | EndClass, 20 | ClassContent(char), 21 | } 22 | 23 | impl Token { 24 | /// Append a regex representation of Token to the supplied pattern string 25 | pub fn append_regex(&self, pattern: &mut String, is_first_in_component: bool) { 26 | match self { 27 | Token::Literal(c) => pattern.push_str(®ex::escape(&c.to_string())), 28 | // `?` matches any single character, except for `.` at the start of 29 | // a filename. 30 | Token::Any => { 31 | if is_first_in_component { 32 | #[cfg(not(windows))] 33 | pattern.push_str("[^./]"); 34 | #[cfg(windows)] 35 | pattern.push_str("[^./\\\\]"); 36 | } else { 37 | #[cfg(not(windows))] 38 | pattern.push_str("[^/]"); 39 | #[cfg(windows)] 40 | pattern.push_str("[^/\\\\]"); 41 | } 42 | } 43 | // `*` matches 0 or more of any character, 44 | // except for `.` at the start of a filename. 45 | Token::ZeroOrMore => { 46 | if is_first_in_component { 47 | #[cfg(not(windows))] 48 | pattern.push_str("[^./][^/]*"); 49 | #[cfg(windows)] 50 | pattern.push_str("[^./\\\\][^/\\\\]*"); 51 | } else { 52 | #[cfg(not(windows))] 53 | pattern.push_str("[^/]*"); 54 | #[cfg(windows)] 55 | pattern.push_str("[^/\\\\]*"); 56 | } 57 | } 58 | Token::StartAlternative => pattern.push('('), 59 | Token::NextAlternative => pattern.push('|'), 60 | Token::EndAlternative => pattern.push(')'), 61 | Token::StartClass => pattern.push('['), 62 | Token::NegateClass => pattern.push('^'), 63 | Token::EndClass => pattern.push(']'), 64 | Token::ClassContent(c) => pattern.push(*c), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pathsearch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pathsearch" 3 | version = "0.2.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | repository = "https://github.com/wez/wzsh" 7 | description = "Search for files in PATH" 8 | license = "MIT" 9 | documentation = "https://docs.rs/pathsearch" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | anyhow = "1.0" 14 | libc = "0.2" 15 | 16 | -------------------------------------------------------------------------------- /pathsearch/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /pathsearch/README.md: -------------------------------------------------------------------------------- 1 | # pathsearch 2 | 3 | This crate provides functions that can be used to search for an 4 | executable based on the PATH environment on both POSIX and Windows 5 | systems. 6 | 7 | `find_executable_in_path` is the most convenient function exported 8 | by this crate; given the name of an executable, it will yield the 9 | absolute path of the first matching file. 10 | 11 | ```rust 12 | use pathsearch::find_executable_in_path; 13 | 14 | if let Some(exe) = find_executable_in_path("ls") { 15 | println!("Found ls at {}", exe.display()); 16 | } 17 | ``` 18 | 19 | `PathSearcher` is platform-independent struct that encompasses the 20 | path searching algorithm used by `find_executable_in_path`. Construct 21 | it by passing in the PATH and PATHEXT (for Windows) environment variables 22 | and iterate it to incrementally produce all candidate results. This 23 | is useful when implementing utilities such as `which` that want to show 24 | all possible paths. 25 | 26 | ```rust 27 | use pathsearch::PathSearcher; 28 | use std::ffi::OsString; 29 | 30 | let path = std::env::var_os("PATH"); 31 | let path_ext = std::env::var_os("PATHEXT"); 32 | 33 | for exe in PathSearcher::new( 34 | "zsh", 35 | path.as_ref().map(OsString::as_os_str), 36 | path_ext.as_ref().map(OsString::as_os_str), 37 | ) { 38 | println!("{}", exe.display()); 39 | } 40 | ``` 41 | 42 | `SimplePathSearcher` is a simple iterator that can be used to search 43 | an arbitrary path for an arbitrary file that doesn't have to be executable. 44 | 45 | License: MIT 46 | -------------------------------------------------------------------------------- /pathsearch/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides functions that can be used to search for an 2 | //! executable based on the PATH environment on both POSIX and Windows 3 | //! systems. 4 | //! 5 | //! `find_executable_in_path` is the most convenient function exported 6 | //! by this crate; given the name of an executable, it will yield the 7 | //! absolute path of the first matching file. 8 | //! 9 | //! ``` 10 | //! use pathsearch::find_executable_in_path; 11 | //! 12 | //! if let Some(exe) = find_executable_in_path("ls") { 13 | //! println!("Found ls at {}", exe.display()); 14 | //! } 15 | //! ``` 16 | //! 17 | //! `PathSearcher` is platform-independent struct that encompasses the 18 | //! path searching algorithm used by `find_executable_in_path`. Construct 19 | //! it by passing in the PATH and PATHEXT (for Windows) environment variables 20 | //! and iterate it to incrementally produce all candidate results. This 21 | //! is useful when implementing utilities such as `which` that want to show 22 | //! all possible paths. 23 | //! 24 | //! ``` 25 | //! use pathsearch::PathSearcher; 26 | //! use std::ffi::OsString; 27 | //! 28 | //! let path = std::env::var_os("PATH"); 29 | //! let path_ext = std::env::var_os("PATHEXT"); 30 | //! 31 | //! for exe in PathSearcher::new( 32 | //! "zsh", 33 | //! path.as_ref().map(OsString::as_os_str), 34 | //! path_ext.as_ref().map(OsString::as_os_str), 35 | //! ) { 36 | //! println!("{}", exe.display()); 37 | //! } 38 | //! ``` 39 | //! 40 | //! `SimplePathSearcher` is a simple iterator that can be used to search 41 | //! an arbitrary path for an arbitrary file that doesn't have to be executable. 42 | use std::ffi::{OsStr, OsString}; 43 | use std::path::PathBuf; 44 | 45 | #[cfg(unix)] 46 | pub mod unix; 47 | 48 | #[cfg(windows)] 49 | pub mod windows; 50 | 51 | /// SimplePathSearcher is an iterator that yields candidate PathBuf inthstances 52 | /// generated from searching the supplied path string following the 53 | /// standard rules: explode path by the system path separator character 54 | /// and then for each entry, concatenate the candidate command and test 55 | /// whether that is a file. 56 | pub struct SimplePathSearcher<'a> { 57 | path_iter: std::env::SplitPaths<'a>, 58 | command: &'a OsStr, 59 | } 60 | 61 | impl<'a> SimplePathSearcher<'a> { 62 | /// Create a new SimplePathSearcher that will yield candidate paths for 63 | /// the specified command 64 | pub fn new + ?Sized>(command: &'a T, path: Option<&'a OsStr>) -> Self { 65 | let path = path.unwrap_or_else(|| OsStr::new("")); 66 | let path_iter = std::env::split_paths(path); 67 | let command = command.as_ref(); 68 | Self { path_iter, command } 69 | } 70 | } 71 | 72 | impl<'a> Iterator for SimplePathSearcher<'a> { 73 | type Item = PathBuf; 74 | 75 | /// Returns the next candidate path 76 | fn next(&mut self) -> Option { 77 | loop { 78 | let entry = self.path_iter.next()?; 79 | let candidate = entry.join(self.command); 80 | 81 | if candidate.is_file() { 82 | return Some(candidate); 83 | } 84 | } 85 | } 86 | } 87 | 88 | #[cfg(unix)] 89 | pub type PathSearcher<'a> = unix::ExecutablePathSearcher<'a>; 90 | 91 | #[cfg(windows)] 92 | pub type PathSearcher<'a> = windows::WindowsPathSearcher<'a>; 93 | 94 | /// Resolves the first matching candidate command from the current 95 | /// process environment using the platform appropriate rules. 96 | /// On Unix systems this will search the PATH environment variable 97 | /// for an executable file. 98 | /// On Windows systems this will search each entry in PATH and 99 | /// return the first file that has an extension listed in the PATHEXT 100 | /// environment variable. 101 | pub fn find_executable_in_path + ?Sized>(command: &O) -> Option { 102 | let path = std::env::var_os("PATH"); 103 | let path_ext = std::env::var_os("PATHEXT"); 104 | PathSearcher::new( 105 | command, 106 | path.as_ref().map(OsString::as_os_str), 107 | path_ext.as_ref().map(OsString::as_os_str), 108 | ) 109 | .next() 110 | } 111 | -------------------------------------------------------------------------------- /pathsearch/src/unix.rs: -------------------------------------------------------------------------------- 1 | use crate::SimplePathSearcher; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | /// Returns true if the specified path has executable access permissions 7 | /// for the current process. The check is made using the access(2) 8 | /// syscall. 9 | pub fn is_executable(path: &Path) -> anyhow::Result { 10 | use libc::{access, X_OK}; 11 | 12 | let cstr = std::ffi::CString::new(path.as_os_str().as_bytes().to_vec())?; 13 | let res = unsafe { access(cstr.as_ptr(), X_OK) }; 14 | Ok(res == 0) 15 | } 16 | 17 | /// Returns an OsString composed from `a` with `b` appended 18 | pub fn concat_osstr(a: &OsStr, b: &OsStr) -> OsString { 19 | let a = a.as_bytes(); 20 | let b = b.as_bytes(); 21 | let mut res = Vec::with_capacity(a.len() + b.len()); 22 | res.extend_from_slice(a); 23 | res.extend_from_slice(b); 24 | OsStringExt::from_vec(res) 25 | } 26 | 27 | /// ExecutablePathSearcher is an iterator that yields candidate PathBuf instances 28 | /// generated from searching the supplied path string following the 29 | /// standard rules: explode path by the system path separator character 30 | /// and then for each entry, concatenate the candidate command and test 31 | /// whether that is an executable file. 32 | pub struct ExecutablePathSearcher<'a>(SimplePathSearcher<'a>); 33 | 34 | impl<'a> ExecutablePathSearcher<'a> { 35 | /// Create a new ExecutablePathSearcher that will yield candidate paths for 36 | /// the specified command. 37 | /// `path` should be the path string to be split and searched. This is typically 38 | /// the value of the `PATH` environment variable. 39 | /// The `_path_ext` parameter is present for easier cross platform support 40 | /// when running on Windows. It is ignored on unix systems, but on 41 | /// windows systems this is typically the value of the `PATHEXT` environment variable. 42 | pub fn new + ?Sized>( 43 | command: &'a T, 44 | path: Option<&'a OsStr>, 45 | _path_ext: Option<&'a OsStr>, 46 | ) -> Self { 47 | Self(SimplePathSearcher::new(command, path)) 48 | } 49 | } 50 | 51 | impl<'a> Iterator for ExecutablePathSearcher<'a> { 52 | type Item = PathBuf; 53 | 54 | /// Returns the next candidate executable file 55 | fn next(&mut self) -> Option { 56 | while let Some(candidate) = self.0.next() { 57 | if let Ok(true) = is_executable(&candidate) { 58 | return Some(candidate); 59 | } 60 | } 61 | None 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pathsearch/src/windows.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::os::windows::ffi::{OsStrExt, OsStringExt}; 3 | use std::path::PathBuf; 4 | 5 | /// Returns an OsString composed from `a` with `b` appended 6 | pub fn concat_osstr(a: &OsStr, b: &OsStr) -> OsString { 7 | let mut res: Vec = a.encode_wide().collect(); 8 | for c in b.encode_wide() { 9 | res.push(c); 10 | } 11 | OsStringExt::from_wide(&res) 12 | } 13 | 14 | /// WindowsPathSearcher is an iterator that yields candidate PathBuf instances 15 | /// generated from searching the PATH environment variable following the 16 | /// standard windows rules: explode PATH by the system path separator character 17 | /// and then for each entry, concatenate the candidate command and test 18 | /// whether that is a file. Additional candidates are produced by taking 19 | /// each of the filename extensions specified by the PATHEXT environment 20 | /// variable and concatenating those with the command. 21 | pub struct WindowsPathSearcher<'a> { 22 | path_ext: &'a OsStr, 23 | path_iter: std::env::SplitPaths<'a>, 24 | path_ext_iter: Option>, 25 | command: &'a OsStr, 26 | candidate: Option, 27 | } 28 | 29 | impl<'a> WindowsPathSearcher<'a> { 30 | pub fn new + ?Sized>( 31 | command: &'a T, 32 | path: Option<&'a OsStr>, 33 | path_ext: Option<&'a OsStr>, 34 | ) -> Self { 35 | let path = path.unwrap_or_else(|| OsStr::new("")); 36 | let path_ext = path_ext.unwrap_or_else(|| OsStr::new(".EXE")); 37 | let path_iter = std::env::split_paths(path); 38 | let path_ext_iter = None; 39 | let command = command.as_ref(); 40 | let candidate = None; 41 | Self { 42 | path_iter, 43 | path_ext, 44 | path_ext_iter, 45 | command, 46 | candidate, 47 | } 48 | } 49 | } 50 | 51 | impl<'a> Iterator for WindowsPathSearcher<'a> { 52 | type Item = PathBuf; 53 | 54 | /// Returns the next candidate executable file 55 | fn next(&mut self) -> Option { 56 | loop { 57 | if let Some(iter) = self.path_ext_iter.as_mut() { 58 | while let Some(ext) = iter.next() { 59 | let extended = PathBuf::from(concat_osstr( 60 | self.candidate.as_ref().unwrap().as_os_str(), 61 | ext.as_os_str(), 62 | )); 63 | if extended.is_file() { 64 | return Some(extended); 65 | } 66 | } 67 | } 68 | 69 | let entry = self.path_iter.next()?; 70 | let candidate = entry.join(self.command); 71 | self.candidate = Some(candidate.clone()); 72 | self.path_ext_iter = Some(std::env::split_paths(self.path_ext)); 73 | 74 | if candidate.is_file() { 75 | return Some(candidate); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /shell_compiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell_compiler" 3 | version = "0.1.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | thiserror = "1.0" 10 | filedescriptor = "0.7" 11 | lazy_static = "1.3" 12 | shell_lexer = { path = "../shell_lexer" } 13 | shell_parser = { path = "../shell_parser" } 14 | shell_vm = { path = "../shell_vm" } 15 | 16 | [dev-dependencies] 17 | pretty_assertions = "0.6" 18 | -------------------------------------------------------------------------------- /shell_compiler/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /shell_compiler/src/registeralloc.rs: -------------------------------------------------------------------------------- 1 | /// A very simple register allocation scheme 2 | #[derive(Debug, Clone, Default)] 3 | pub struct RegisterAllocator { 4 | /// Total number of registers allocated 5 | num_allocated: usize, 6 | 7 | /// Simple free list of registers that have 8 | /// been returned to us 9 | free_list: Vec, 10 | } 11 | 12 | impl RegisterAllocator { 13 | pub fn new() -> Self { 14 | Default::default() 15 | } 16 | 17 | /// Returns the size of the required register frame 18 | pub fn frame_size(&self) -> usize { 19 | self.num_allocated 20 | } 21 | 22 | /// Allocate a register. 23 | /// This may re-use a previously free register. 24 | #[must_use = "allocated a RegisterAddress which must be used"] 25 | pub fn allocate(&mut self) -> usize { 26 | if let Some(idx) = self.free_list.pop() { 27 | idx 28 | } else { 29 | let idx = self.num_allocated; 30 | self.num_allocated += 1; 31 | // Since the returned index will be used relative to 32 | // the top, we can't return a 0 here. Instead we 33 | // always add one so that we are always below the 34 | // top of the stack. 35 | idx + 1 36 | } 37 | } 38 | 39 | /// Free a register, allowing it to potentially be re-used. 40 | pub fn free(&mut self, reg: usize) { 41 | self.free_list.push(reg); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shell_lexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell_lexer" 3 | version = "0.1.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | thiserror = "1.0" 10 | lazy_static = "1.3" 11 | regex = "1" 12 | 13 | [dev-dependencies] 14 | pretty_assertions = "0.6" 15 | -------------------------------------------------------------------------------- /shell_lexer/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /shell_lexer/src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::position::Span; 2 | use thiserror::*; 3 | 4 | #[derive(Debug, Clone, Copy, Error)] 5 | pub enum LexErrorKind { 6 | #[error("EOF while lexing backslash escape")] 7 | EofDuringBackslash, 8 | #[error("EOF while lexing comment")] 9 | EofDuringComment, 10 | #[error("EOF while lexing single quoted string")] 11 | EofDuringSingleQuotedString, 12 | #[error("EOF while lexing double quoted string")] 13 | EofDuringDoubleQuotedString, 14 | #[error("EOF while lexing parameter expansion")] 15 | EofDuringParameterExpansion, 16 | #[error("EOF while lexing assignment word")] 17 | EofDuringAssignmentWord, 18 | #[error("EOF while lexing command substitution")] 19 | EofDuringCommandSubstitution, 20 | #[error("IO Error")] 21 | IoError, 22 | } 23 | 24 | impl LexErrorKind { 25 | pub fn at(self, span: Span) -> LexError { 26 | LexError::new(self, span) 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone, Error)] 31 | #[error("{} at {}", kind, span)] 32 | pub struct LexError { 33 | pub kind: LexErrorKind, 34 | pub span: Span, 35 | } 36 | 37 | impl LexError { 38 | pub fn new(kind: LexErrorKind, span: Span) -> Self { 39 | Self { kind, span } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shell_lexer/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | mod lexer; 3 | mod position; 4 | mod reader; 5 | #[macro_use] 6 | mod tokenenum; 7 | 8 | pub use errors::{LexError, LexErrorKind}; 9 | pub use lexer::{Assignment, Lexer, ParamExpr, ParamOper, Token, WordComponent, WordComponentKind}; 10 | pub use position::{Pos, Span}; 11 | pub use reader::CharReader; 12 | pub use tokenenum::LiteralMatcher; 13 | 14 | TokenEnum!( 15 | Operator, 16 | OPERATORS, 17 | "<<-": DoubleLessDash, 18 | "<<": DoubleLess, 19 | "<&": LessAnd, 20 | "<>": LessGreat, 21 | ">>": DoubleGreat, 22 | ">|": Clobber, 23 | ">&": GreatAnd, 24 | "&&": AndIf, 25 | "||": OrIf, 26 | ";;": DoubleSemicolon, 27 | "<": Less, 28 | "&": Ampersand, 29 | "|": Pipe, 30 | ";": Semicolon, 31 | ">": Great, 32 | "(": LeftParen, 33 | ")": RightParen 34 | ); 35 | 36 | TokenEnum!( 37 | ReservedWord, 38 | RESERVED_WORDS, 39 | "if": If, 40 | "then": Then, 41 | "else": Else, 42 | "elif": Elif, 43 | "fi": Fi, 44 | "do": Do, 45 | "done": Done, 46 | "case": Case, 47 | "esac": Esac, 48 | "while": While, 49 | "until": Until, 50 | "for": For, 51 | "{": LeftBrace, 52 | "}": RightBrace, 53 | "!": Bang, 54 | "in": In 55 | ); 56 | -------------------------------------------------------------------------------- /shell_lexer/src/position.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Error, Formatter}; 2 | 3 | /// A position within the input text 4 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] 5 | pub struct Pos { 6 | pub line: usize, 7 | pub col: usize, 8 | } 9 | 10 | impl Pos { 11 | pub fn new(line: usize, col: usize) -> Self { 12 | Self { line, col } 13 | } 14 | } 15 | 16 | impl Display for Pos { 17 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> { 18 | write!(fmt, "line {} column {}", self.line, self.col) 19 | } 20 | } 21 | 22 | /// A token may span multiple positions; this struct 23 | /// represents the span of such a thing. 24 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] 25 | pub struct Span { 26 | pub start: Pos, 27 | pub end: Pos, 28 | } 29 | 30 | impl Span { 31 | pub fn new_pos(line: usize, col: usize) -> Self { 32 | let start = Pos::new(line, col); 33 | Self { start, end: start } 34 | } 35 | 36 | pub fn new(start: Pos, end: Pos) -> Self { 37 | Self { start, end } 38 | } 39 | 40 | pub fn new_to(line: usize, col: usize, endcol: usize) -> Self { 41 | let start = Pos::new(line, col); 42 | let end = Pos::new(line, endcol); 43 | Self { start, end } 44 | } 45 | } 46 | 47 | impl From for Span { 48 | fn from(pos: Pos) -> Span { 49 | Span::new_pos(pos.line, pos.col) 50 | } 51 | } 52 | 53 | impl Display for Span { 54 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> { 55 | if self.start == self.end { 56 | self.start.fmt(fmt) 57 | } else if self.start.line == self.end.line { 58 | write!( 59 | fmt, 60 | "line {} column {} thru {}", 61 | self.start.line, self.start.col, self.end.col 62 | ) 63 | } else { 64 | write!( 65 | fmt, 66 | "line {} col {} thru line {} col {}", 67 | self.start.line, self.start.col, self.end.line, self.end.col 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /shell_lexer/src/reader.rs: -------------------------------------------------------------------------------- 1 | use crate::tokenenum::{LiteralMatcher, MatchResult}; 2 | use crate::{Pos, Span}; 3 | use anyhow::Error; 4 | use lazy_static::lazy_static; 5 | use regex::{Captures, Regex}; 6 | use std::io::{BufRead, BufReader, Read}; 7 | 8 | lazy_static! { 9 | static ref IO_NUMBER_RE: Regex = 10 | Regex::new(r"^[0-9]+[<>]").expect("failed to compile IO_NUMBER_RE"); 11 | static ref ASSIGNMENT_WORD_RE: Regex = 12 | Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*=").expect("failed to compile ASSIGNMENT_WORD_RE"); 13 | } 14 | 15 | pub struct CharReader { 16 | stream: BufReader, 17 | line_buffer: String, 18 | line_idx: usize, 19 | position: Pos, 20 | } 21 | 22 | impl std::fmt::Debug for CharReader { 23 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 24 | fmt.debug_struct("CharReader") 25 | .field("line_buffer", &self.line_buffer) 26 | .field("line_idx", &self.line_idx) 27 | .field("position", &self.position) 28 | .finish() 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone, Copy)] 33 | pub struct PositionedChar { 34 | pub c: char, 35 | pub pos: Pos, 36 | } 37 | 38 | #[derive(Debug)] 39 | pub enum Next { 40 | Char(PositionedChar), 41 | Eof(Pos), 42 | Error(Error, Pos), 43 | } 44 | 45 | impl CharReader { 46 | pub fn new(stream: R) -> Self { 47 | Self { 48 | stream: BufReader::new(stream), 49 | line_buffer: String::new(), 50 | line_idx: 0, 51 | position: Pos::new(0, 0), 52 | } 53 | } 54 | 55 | pub fn matches_literal( 56 | &mut self, 57 | matcher: &LiteralMatcher, 58 | ) -> anyhow::Result> { 59 | match self.check_and_fill_buffer() { 60 | Next::Eof(_) => Ok(MatchResult::No), 61 | Next::Error(err, pos) => return Err(err.context(pos).into()), 62 | _ => Ok(matcher.matches(&self.line_buffer[self.line_idx..])), 63 | } 64 | } 65 | 66 | pub fn next_literal( 67 | &mut self, 68 | matcher: &LiteralMatcher, 69 | ) -> anyhow::Result> { 70 | match self.matches_literal(matcher)? { 71 | MatchResult::No => Ok(None), 72 | MatchResult::Match(value, len) => { 73 | self.line_idx += len; 74 | let start = self.position; 75 | let end = Pos::new(start.line, start.col + len - 1); 76 | self.position.col += len; 77 | Ok(Some((value, Span::new(start, end)))) 78 | } 79 | } 80 | } 81 | 82 | pub fn matches_regex(&mut self, regex: &Regex) -> anyhow::Result> { 83 | match self.check_and_fill_buffer() { 84 | Next::Eof(_) => Ok(None), 85 | Next::Error(err, pos) => return Err(err.context(pos).into()), 86 | _ => Ok(regex 87 | .captures(&self.line_buffer[self.line_idx..]) 88 | .map(|c| (c, self.position))), 89 | } 90 | } 91 | 92 | /// Use this after calling matches_regex to fixup the matched length 93 | pub fn fixup_matched_length(&mut self, length: usize) { 94 | self.line_idx += length; 95 | self.position.col += length; 96 | } 97 | 98 | pub fn matches_io_number(&mut self) -> anyhow::Result { 99 | match self.check_and_fill_buffer() { 100 | Next::Eof(_) => Ok(false), 101 | Next::Error(err, pos) => return Err(err.context(pos).into()), 102 | _ => Ok(IO_NUMBER_RE.is_match(&self.line_buffer[self.line_idx..])), 103 | } 104 | } 105 | 106 | pub fn next_io_number(&mut self) -> anyhow::Result> { 107 | match self.check_and_fill_buffer() { 108 | Next::Eof(_) => Ok(None), 109 | Next::Error(err, pos) => return Err(err.context(pos).into()), 110 | _ => { 111 | if let Some(m) = IO_NUMBER_RE.find(&self.line_buffer[self.line_idx..]) { 112 | let num_str = m.as_str(); 113 | let len = m.end() - 1; 114 | let num = usize::from_str_radix(&num_str[..len], 10) 115 | .expect("number to parse as number"); 116 | let start = self.position; 117 | let end = Pos::new(start.line, start.col + len); 118 | self.line_idx += len; 119 | self.position.col += len; 120 | Ok(Some((num, Span::new(start, end)))) 121 | } else { 122 | Ok(None) 123 | } 124 | } 125 | } 126 | } 127 | 128 | pub fn matches_assignment_word(&mut self) -> anyhow::Result { 129 | match self.check_and_fill_buffer() { 130 | Next::Eof(_) => Ok(false), 131 | Next::Error(err, pos) => return Err(err.context(pos).into()), 132 | _ => Ok(ASSIGNMENT_WORD_RE.is_match(&self.line_buffer[self.line_idx..])), 133 | } 134 | } 135 | 136 | pub fn next_assignment_word(&mut self) -> anyhow::Result> { 137 | match self.check_and_fill_buffer() { 138 | Next::Eof(_) => Ok(None), 139 | Next::Error(err, pos) => return Err(err.context(pos).into()), 140 | _ => { 141 | if let Some(m) = ASSIGNMENT_WORD_RE.find(&self.line_buffer[self.line_idx..]) { 142 | let num_str = m.as_str(); 143 | let len = m.end() - 1; 144 | let name = num_str[..len].to_string(); 145 | let start = self.position; 146 | let end = Pos::new(start.line, start.col + len + 1); 147 | self.line_idx += len + 1; 148 | self.position.col += len + 1; 149 | Ok(Some((name, Span::new(start, end)))) 150 | } else { 151 | Ok(None) 152 | } 153 | } 154 | } 155 | } 156 | 157 | fn check_and_fill_buffer(&mut self) -> Next { 158 | if self.line_buffer.is_empty() || self.line_idx >= self.line_buffer.len() { 159 | let bump_line = !self.line_buffer.is_empty(); 160 | self.line_buffer.clear(); 161 | match self.stream.read_line(&mut self.line_buffer) { 162 | Ok(0) => return Next::Eof(self.position), 163 | Err(e) => return Next::Error(e.into(), self.position), 164 | _ => { 165 | self.line_idx = 0; 166 | self.position.col = 0; 167 | if bump_line { 168 | self.position.line += 1; 169 | } 170 | } 171 | } 172 | } 173 | // Dummy result 174 | Next::Char(PositionedChar { 175 | c: ' ', 176 | pos: self.position, 177 | }) 178 | } 179 | 180 | pub fn next_char(&mut self) -> Next { 181 | match self.check_and_fill_buffer() { 182 | fail @ Next::Eof(..) | fail @ Next::Error(..) => return fail, 183 | _ => {} 184 | } 185 | match (&self.line_buffer[self.line_idx..]).chars().next() { 186 | Some(c) => { 187 | let result = Next::Char(PositionedChar { 188 | c, 189 | pos: self.position, 190 | }); 191 | self.line_idx += c.len_utf8(); 192 | self.position.col += 1; 193 | result 194 | } 195 | None => { 196 | // No more to be read from this line; bump us over 197 | // the edge and recursse so that we trigger reading 198 | // the next line 199 | self.line_idx += 1; 200 | self.next_char() 201 | } 202 | } 203 | } 204 | 205 | pub fn unget(&mut self, c: PositionedChar) { 206 | let len = c.c.len_utf8(); 207 | assert!(self.line_idx > 0); 208 | let (new_idx, overflow) = self.line_idx.overflowing_sub(len); 209 | assert!( 210 | !overflow, 211 | "overflowed while putting back a token: len={} line_idx={} c={:?} line_buffer={:?}", 212 | len, self.line_idx, c, self.line_buffer 213 | ); 214 | self.line_idx = new_idx; 215 | self.position.col -= 1; 216 | assert_eq!( 217 | self.line_buffer[self.line_idx..].chars().next().unwrap(), 218 | c.c 219 | ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /shell_lexer/src/tokenenum.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::collections::HashMap; 3 | 4 | macro_rules! TokenEnum { 5 | ($Enum:ident, $Matcher:ident, $( 6 | $text:literal : $variant:ident 7 | ),+) => { 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] 10 | pub enum $Enum { 11 | $( 12 | $variant 13 | ),+ 14 | } 15 | 16 | impl std::fmt::Display for $Enum { 17 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 18 | match self { 19 | $( 20 | $Enum::$variant => write!(fmt, "{}", $text) 21 | ),+ 22 | } 23 | } 24 | } 25 | 26 | lazy_static::lazy_static! { 27 | static ref $Matcher: $crate::tokenenum::LiteralMatcher<$Enum> = { 28 | $crate::tokenenum::LiteralMatcher::new(&[ 29 | $( 30 | ($text, $Enum::$variant) 31 | ),+ 32 | ]) 33 | }; 34 | } 35 | 36 | } 37 | } 38 | 39 | #[derive(Debug)] 40 | pub struct LiteralMatcher { 41 | re: Regex, 42 | map: HashMap<&'static str, T>, 43 | } 44 | 45 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 46 | pub enum MatchResult { 47 | Match(T, usize), 48 | No, 49 | } 50 | 51 | impl LiteralMatcher { 52 | pub fn new(literals: &[(&'static str, T)]) -> Self { 53 | let mut pattern = String::new(); 54 | let mut map = HashMap::new(); 55 | pattern.push_str("^("); 56 | for (idx, lit) in literals.iter().enumerate() { 57 | if idx > 0 { 58 | pattern.push('|'); 59 | } 60 | pattern.push_str(®ex::escape(lit.0)); 61 | map.insert(lit.0, lit.1); 62 | } 63 | pattern.push_str(")"); 64 | 65 | Self { 66 | re: Regex::new(&pattern).unwrap(), 67 | map, 68 | } 69 | } 70 | 71 | pub fn lookup(&self, text: &str) -> Option { 72 | self.map.get(text).map(|v| *v) 73 | } 74 | 75 | pub fn matches(&self, text: &str) -> MatchResult { 76 | if let Some(m) = self.re.find(text) { 77 | MatchResult::Match(*self.map.get(m.as_str()).unwrap(), m.as_str().len()) 78 | } else { 79 | MatchResult::No 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /shell_parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell_parser" 3 | version = "0.1.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | thiserror = "1.0" 10 | lazy_static = "1.3" 11 | shell_lexer = { path = "../shell_lexer" } 12 | 13 | [dev-dependencies] 14 | pretty_assertions = "0.6" 15 | -------------------------------------------------------------------------------- /shell_parser/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /shell_parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod parser; 2 | mod types; 3 | 4 | pub use parser::*; 5 | pub use types::*; 6 | 7 | #[cfg(test)] 8 | mod test; 9 | -------------------------------------------------------------------------------- /shell_parser/src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::types::*; 2 | use anyhow::{bail, Error}; 3 | use shell_lexer::{Lexer, Operator, ReservedWord, Token}; 4 | use std::collections::VecDeque; 5 | use std::io::Read; 6 | use thiserror::*; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub enum ParseErrorContext { 10 | List, 11 | PipelineStartingWithBang, 12 | PipeSequence, 13 | IoFileAfterIoNumber, 14 | FileNameAfterRedirectionOperator, 15 | ExpectingPipelineAfter(Operator), 16 | FdRedirectionExpectsNumber, 17 | ExpectingRightBrace, 18 | ExpectingRightParen, 19 | ExpectingThen, 20 | ExpectingFi, 21 | } 22 | 23 | #[derive(Debug, Clone, PartialEq, Eq, Error)] 24 | pub enum ParseErrorKind { 25 | #[error("Unexpected token {:?} while parsing {:?}", .0, .1)] 26 | UnexpectedToken(Token, ParseErrorContext), 27 | } 28 | 29 | pub struct Parser { 30 | lexer: Lexer, 31 | lookahead: VecDeque, 32 | } 33 | 34 | impl Parser { 35 | pub fn new(stream: R) -> Self { 36 | let lexer = Lexer::new(stream); 37 | Self { 38 | lexer, 39 | lookahead: VecDeque::new(), 40 | } 41 | } 42 | 43 | /// Main entry point to the parser; parses a program 44 | pub fn parse(&mut self) -> anyhow::Result { 45 | self.program() 46 | } 47 | } 48 | 49 | impl Parser { 50 | fn unexpected_next_token(&mut self, context: ParseErrorContext) -> Error { 51 | match self.next_token() { 52 | Ok(tok) => ParseErrorKind::UnexpectedToken(tok, context).into(), 53 | Err(e) => e, 54 | } 55 | } 56 | 57 | /// If the next token is an operator with a kind that matches 58 | /// any of those in the candidates slice, get that token and 59 | /// return it. Otherwise returns None. 60 | fn next_token_is_operator(&mut self, candidates: &[Operator]) -> anyhow::Result> { 61 | let tok = self.next_token()?; 62 | if let Token::Operator(operator, ..) = &tok { 63 | for op in candidates { 64 | if op == operator { 65 | return Ok(Some(tok)); 66 | } 67 | } 68 | } 69 | self.unget_token(tok); 70 | Ok(None) 71 | } 72 | 73 | /// If the next token is the requested reserved word, consume it 74 | /// and return true. Otherwise, put it back and return false. 75 | fn next_token_is_reserved_word(&mut self, reserved: ReservedWord) -> anyhow::Result { 76 | let tok = self.next_token()?; 77 | if tok.is_reserved_word(reserved) { 78 | return Ok(true); 79 | } 80 | self.unget_token(tok); 81 | Ok(false) 82 | } 83 | 84 | /// Consume the next token 85 | fn next_token(&mut self) -> anyhow::Result { 86 | if let Some(tok) = self.lookahead.pop_front() { 87 | Ok(tok) 88 | } else { 89 | self.lexer.next_token() 90 | } 91 | } 92 | 93 | /// Place a token into the lookahead. 94 | /// The lookahead must be vacant, or else this will cause 95 | /// a panic; we only support a single lookahead. 96 | fn unget_token(&mut self, tok: Token) { 97 | self.lookahead.push_front(tok); 98 | } 99 | } 100 | 101 | impl Parser { 102 | /// Parse the top level program syntax, a potentially empty list 103 | /// of child commands. 104 | fn program(&mut self) -> anyhow::Result { 105 | let mut cmd = match self.and_or()? { 106 | Some(cmd) => cmd, 107 | None => { 108 | let tok = self.next_token()?; 109 | if let Token::Eof(..) = &tok { 110 | // We parsed an empty program 111 | return Ok(CommandType::SimpleCommand(Default::default()).into()); 112 | } 113 | return Err(self.unexpected_next_token(ParseErrorContext::List)); 114 | } 115 | }; 116 | cmd.asynchronous = self.separator_is_async()?; 117 | let mut commands = vec![cmd]; 118 | 119 | while let Some(mut cmd) = self.and_or()? { 120 | cmd.asynchronous = self.separator_is_async()?; 121 | commands.push(cmd); 122 | } 123 | 124 | if commands.len() == 1 { 125 | Ok(commands.pop().unwrap()) 126 | } else { 127 | let is_async = commands.last().unwrap().asynchronous; 128 | 129 | let mut command: Command = CommandType::Program(CompoundList { commands }).into(); 130 | command.asynchronous = is_async; 131 | Ok(command) 132 | } 133 | } 134 | 135 | fn and_or(&mut self) -> anyhow::Result> { 136 | if let Some(pipeline) = self.pipeline()? { 137 | match self.next_token_is_operator(&[Operator::AndIf, Operator::OrIf])? { 138 | Some(Token::Operator(operator, ..)) => { 139 | self.pipeline_conditional(pipeline, operator) 140 | } 141 | _ => Ok(Some(pipeline.into())), 142 | } 143 | } else { 144 | Ok(None) 145 | } 146 | } 147 | 148 | fn pipeline_conditional( 149 | &mut self, 150 | condition: Pipeline, 151 | op: Operator, 152 | ) -> anyhow::Result> { 153 | self.linebreak()?; 154 | 155 | let then: CompoundList = Command::from(self.pipeline()?.ok_or_else(|| { 156 | self.unexpected_next_token(ParseErrorContext::ExpectingPipelineAfter(op)) 157 | })?) 158 | .into(); 159 | let condition: CompoundList = Command::from(condition).into(); 160 | 161 | let (true_part, false_part) = if op == Operator::AndIf { 162 | (Some(then), None) 163 | } else { 164 | (None, Some(then)) 165 | }; 166 | 167 | Ok(Some( 168 | CommandType::If(If { 169 | condition: condition.into(), 170 | true_part, 171 | false_part, 172 | }) 173 | .into(), 174 | )) 175 | } 176 | 177 | fn pipeline(&mut self) -> anyhow::Result> { 178 | let inverted = self.next_token_is_reserved_word(ReservedWord::Bang)?; 179 | if let Some(commands) = self.pipe_sequence()? { 180 | Ok(Some(Pipeline { inverted, commands })) 181 | } else if inverted { 182 | Err(self.unexpected_next_token(ParseErrorContext::PipelineStartingWithBang)) 183 | } else { 184 | Ok(None) 185 | } 186 | } 187 | 188 | fn pipe_sequence(&mut self) -> anyhow::Result>> { 189 | let command = match self.command()? { 190 | None => return Ok(None), 191 | Some(cmd) => cmd, 192 | }; 193 | 194 | let mut commands = vec![command]; 195 | 196 | while self.next_token_is_operator(&[Operator::Pipe])?.is_some() { 197 | self.linebreak()?; 198 | match self.command()? { 199 | Some(cmd) => commands.push(cmd), 200 | None => return Err(self.unexpected_next_token(ParseErrorContext::PipeSequence)), 201 | } 202 | } 203 | 204 | for cmd in commands.iter_mut().rev().skip(1) { 205 | cmd.asynchronous = true; 206 | } 207 | 208 | Ok(Some(commands)) 209 | } 210 | 211 | fn command(&mut self) -> anyhow::Result> { 212 | if let Some(command) = self.function_definition()? { 213 | Ok(Some(command)) 214 | } else if let Some(cmd) = self.compound_command()? { 215 | Ok(Some(cmd)) 216 | } else if let Some(group) = self.subshell()? { 217 | Ok(Some(Command { 218 | command: CommandType::Subshell(group), 219 | asynchronous: false, 220 | redirects: vec![], 221 | })) 222 | } else if let Some(command) = self.simple_command()? { 223 | Ok(Some(Command { 224 | command: CommandType::SimpleCommand(command), 225 | asynchronous: false, 226 | redirects: vec![], 227 | })) 228 | } else { 229 | Ok(None) 230 | } 231 | } 232 | 233 | fn function_definition(&mut self) -> anyhow::Result> { 234 | if let Some(fname) = self.fname()? { 235 | if self 236 | .next_token_is_operator(&[Operator::LeftParen])? 237 | .is_none() 238 | { 239 | self.unget_token(fname); 240 | return Ok(None); 241 | } 242 | 243 | if self 244 | .next_token_is_operator(&[Operator::RightParen])? 245 | .is_none() 246 | { 247 | return Err(self.unexpected_next_token(ParseErrorContext::ExpectingRightParen)); 248 | } 249 | 250 | if let Some(cmd) = self.compound_command()? { 251 | Ok(Some(Command { 252 | command: CommandType::FunctionDefinition { 253 | name: fname 254 | .as_single_literal_word_string() 255 | .expect("already verified fname is single literal") 256 | .to_owned(), 257 | body: Box::new(cmd), 258 | }, 259 | asynchronous: false, 260 | redirects: vec![], 261 | })) 262 | } else { 263 | Ok(None) 264 | } 265 | } else { 266 | Ok(None) 267 | } 268 | } 269 | 270 | fn fname(&mut self) -> anyhow::Result> { 271 | let tok = self.next_token()?; 272 | 273 | if tok.is_any_reserved_word() { 274 | self.unget_token(tok); 275 | return Ok(None); 276 | } 277 | 278 | match tok.as_single_literal_word_string() { 279 | None => { 280 | self.unget_token(tok); 281 | Ok(None) 282 | } 283 | Some(_) => Ok(Some(tok)), 284 | } 285 | } 286 | 287 | fn compound_command(&mut self) -> anyhow::Result> { 288 | let mut command = if let Some(group) = self.brace_group()? { 289 | Command { 290 | command: CommandType::BraceGroup(group), 291 | asynchronous: false, 292 | redirects: vec![], 293 | } 294 | } else if let Some(group) = self.subshell()? { 295 | Command { 296 | command: CommandType::Subshell(group), 297 | asynchronous: false, 298 | redirects: vec![], 299 | } 300 | } else if let Some(if_) = self.if_clause()? { 301 | Command { 302 | command: CommandType::If(if_), 303 | asynchronous: false, 304 | redirects: vec![], 305 | } 306 | } else { 307 | // TODO: for_clause, case_clause, while_clause, until_clause 308 | return Ok(None); 309 | }; 310 | 311 | command.redirects = self.redirect_list()?; 312 | Ok(Some(command)) 313 | } 314 | 315 | fn if_clause(&mut self) -> anyhow::Result> { 316 | if !self.next_token_is_reserved_word(ReservedWord::If)? { 317 | return Ok(None); 318 | } 319 | let condition = self.compound_list()?; 320 | if !self.next_token_is_reserved_word(ReservedWord::Then)? { 321 | return Err(self.unexpected_next_token(ParseErrorContext::ExpectingThen)); 322 | } 323 | let true_part = self.compound_list()?; 324 | let false_part = self.else_part()?; 325 | 326 | if !self.next_token_is_reserved_word(ReservedWord::Fi)? { 327 | Err(self.unexpected_next_token(ParseErrorContext::ExpectingFi)) 328 | } else { 329 | Ok(Some(If { 330 | condition, 331 | true_part: Some(true_part), 332 | false_part, 333 | })) 334 | } 335 | } 336 | 337 | fn else_part(&mut self) -> anyhow::Result> { 338 | if self.next_token_is_reserved_word(ReservedWord::Else)? { 339 | let false_part = self.compound_list()?; 340 | Ok(Some(false_part)) 341 | } else if self.next_token_is_reserved_word(ReservedWord::Elif)? { 342 | let condition = self.compound_list()?; 343 | if !self.next_token_is_reserved_word(ReservedWord::Then)? { 344 | return Err(self.unexpected_next_token(ParseErrorContext::ExpectingThen)); 345 | } 346 | let true_part = self.compound_list()?; 347 | let false_part = self.else_part()?; 348 | 349 | Ok(Some(CompoundList { 350 | commands: vec![Command { 351 | asynchronous: false, 352 | command: CommandType::If(If { 353 | condition, 354 | true_part: Some(true_part), 355 | false_part, 356 | }), 357 | redirects: vec![], 358 | }], 359 | })) 360 | } else { 361 | Ok(None) 362 | } 363 | } 364 | 365 | fn subshell(&mut self) -> anyhow::Result> { 366 | let left_paren = match self.next_token_is_operator(&[Operator::LeftParen])? { 367 | None => return Ok(None), 368 | Some(tok) => tok, 369 | }; 370 | 371 | // Slightly gross hack to disambiguate with function definition 372 | if let Some(right_paren) = self.next_token_is_operator(&[Operator::RightParen])? { 373 | self.unget_token(right_paren); 374 | self.unget_token(left_paren); 375 | return Ok(None); 376 | } 377 | 378 | let list = self.compound_list()?; 379 | 380 | if self 381 | .next_token_is_operator(&[Operator::RightParen])? 382 | .is_some() 383 | { 384 | Ok(Some(list)) 385 | } else { 386 | Err(self.unexpected_next_token(ParseErrorContext::ExpectingRightParen)) 387 | } 388 | } 389 | 390 | fn compound_list(&mut self) -> anyhow::Result { 391 | let mut commands = vec![]; 392 | 393 | loop { 394 | self.newline_list()?; 395 | 396 | if let Some(cmd) = self.and_or()? { 397 | commands.push(cmd); 398 | } else { 399 | break; 400 | } 401 | 402 | self.separator()?; 403 | } 404 | 405 | Ok(CompoundList { commands }) 406 | } 407 | 408 | fn brace_group(&mut self) -> anyhow::Result> { 409 | if !self.next_token_is_reserved_word(ReservedWord::LeftBrace)? { 410 | return Ok(None); 411 | } 412 | 413 | let list = self.compound_list()?; 414 | 415 | if self.next_token_is_reserved_word(ReservedWord::RightBrace)? { 416 | Ok(Some(list)) 417 | } else { 418 | Err(self.unexpected_next_token(ParseErrorContext::ExpectingRightBrace)) 419 | } 420 | } 421 | 422 | fn redirect_list(&mut self) -> anyhow::Result> { 423 | let mut redirections = vec![]; 424 | loop { 425 | if let Some(redir) = self.io_redirect()? { 426 | redirections.push(redir); 427 | } else { 428 | return Ok(redirections); 429 | } 430 | } 431 | } 432 | 433 | fn io_redirect(&mut self) -> anyhow::Result> { 434 | let t = self.next_token()?; 435 | if let Token::IoNumber(fd_number, ..) = &t { 436 | match self.io_file(Some(*fd_number))? { 437 | Some(redir) => return Ok(Some(redir)), 438 | None => { 439 | return Err(self.unexpected_next_token(ParseErrorContext::IoFileAfterIoNumber)); 440 | } 441 | } 442 | } 443 | self.unget_token(t); 444 | self.io_file(None) 445 | } 446 | 447 | fn io_file(&mut self, fd_number: Option) -> anyhow::Result> { 448 | let t = self.next_token()?; 449 | let oper = if let Token::Operator(oper, ..) = &t { 450 | match oper { 451 | Operator::Less 452 | | Operator::LessAnd 453 | | Operator::Great 454 | | Operator::GreatAnd 455 | | Operator::DoubleGreat 456 | | Operator::LessGreat 457 | | Operator::Clobber => oper, 458 | _ => { 459 | self.unget_token(t); 460 | return Ok(None); 461 | } 462 | } 463 | } else { 464 | self.unget_token(t); 465 | return Ok(None); 466 | }; 467 | 468 | match oper { 469 | Operator::GreatAnd | Operator::LessAnd => { 470 | if let Some(src_fd_number) = self.number()? { 471 | let dest_fd_number = 472 | fd_number.unwrap_or(if *oper == Operator::GreatAnd { 1 } else { 0 }); 473 | return Ok(Some(Redirection::Fd(FdDuplication { 474 | src_fd_number, 475 | dest_fd_number, 476 | }))); 477 | } else { 478 | return Err( 479 | self.unexpected_next_token(ParseErrorContext::FdRedirectionExpectsNumber) 480 | ); 481 | } 482 | } 483 | _ => {} 484 | } 485 | 486 | let file_name = self.next_token()?; 487 | if let Token::Word(file_name) = file_name { 488 | Ok(Some(match oper { 489 | Operator::Less => Redirection::File(FileRedirection { 490 | fd_number: fd_number.unwrap_or(0), 491 | file_name, 492 | input: true, 493 | output: false, 494 | clobber: false, 495 | append: false, 496 | }), 497 | Operator::LessGreat => Redirection::File(FileRedirection { 498 | fd_number: fd_number.unwrap_or(0), 499 | file_name, 500 | input: true, 501 | output: true, 502 | clobber: false, 503 | append: false, 504 | }), 505 | Operator::Great => Redirection::File(FileRedirection { 506 | fd_number: fd_number.unwrap_or(1), 507 | file_name, 508 | input: false, 509 | output: true, 510 | clobber: false, 511 | append: false, 512 | }), 513 | Operator::DoubleGreat => Redirection::File(FileRedirection { 514 | fd_number: fd_number.unwrap_or(1), 515 | file_name, 516 | input: false, 517 | output: true, 518 | clobber: false, 519 | append: true, 520 | }), 521 | Operator::Clobber => Redirection::File(FileRedirection { 522 | fd_number: fd_number.unwrap_or(1), 523 | file_name, 524 | input: false, 525 | output: true, 526 | clobber: true, 527 | append: false, 528 | }), 529 | _ => bail!("impossible redirection oper {:?}", oper), 530 | })) 531 | } else { 532 | self.unget_token(file_name); 533 | Err(self.unexpected_next_token(ParseErrorContext::FileNameAfterRedirectionOperator)) 534 | } 535 | } 536 | 537 | fn simple_command(&mut self) -> anyhow::Result> { 538 | let mut assignments = vec![]; 539 | let mut words = vec![]; 540 | let mut redirects = vec![]; 541 | 542 | // Apply shell grammar rule 1: the first word of a command, if it 543 | // matches a reserved word, is parsed as that token rather than a 544 | // command 545 | let tok = self.next_token()?; 546 | let is_reserved = tok.is_any_reserved_word(); 547 | self.unget_token(tok); 548 | if is_reserved { 549 | return Ok(None); 550 | } 551 | 552 | loop { 553 | if let Some(redir) = self.io_redirect()? { 554 | redirects.push(redir); 555 | continue; 556 | } 557 | 558 | let token = self.next_token()?; 559 | match &token { 560 | Token::Assignment(assign) => { 561 | if words.is_empty() { 562 | assignments.push(assign.clone()); 563 | } else { 564 | words.push(assign.into()); 565 | } 566 | } 567 | Token::Word(word) => { 568 | if token.is_reserved_word(ReservedWord::RightBrace) { 569 | self.unget_token(token); 570 | break; 571 | } 572 | words.push(word.clone()); 573 | } 574 | 575 | _ => { 576 | self.unget_token(token); 577 | break; 578 | } 579 | } 580 | } 581 | 582 | if assignments.is_empty() && words.is_empty() && redirects.is_empty() { 583 | return Ok(None); 584 | } 585 | 586 | Ok(Some(SimpleCommand { 587 | assignments, 588 | redirects, 589 | words, 590 | })) 591 | } 592 | } 593 | 594 | impl Parser { 595 | /// Consumes an optional sequence of newline tokens. 596 | fn linebreak(&mut self) -> anyhow::Result<()> { 597 | self.newline_list()?; 598 | Ok(()) 599 | } 600 | 601 | /// Consumes a sequence of newline tokens. 602 | /// Returns true if any newline tokens were seen, 603 | /// false if none were seen. 604 | fn newline_list(&mut self) -> anyhow::Result { 605 | let mut seen = false; 606 | loop { 607 | let t = self.next_token()?; 608 | if let Token::Newline(..) = &t { 609 | seen = true; 610 | } else { 611 | self.unget_token(t); 612 | return Ok(seen); 613 | } 614 | } 615 | } 616 | 617 | /// Parse a decimal number. 618 | /// This is used in fd redirection for constructs 619 | /// like `1>&1`. The first number is recognized as 620 | /// an IoNumber and we are called for the second number. 621 | fn number(&mut self) -> anyhow::Result> { 622 | let t = self.next_token()?; 623 | if let Some(word) = t.as_single_literal_word_string() { 624 | if let Ok(num) = usize::from_str_radix(word, 10) { 625 | return Ok(Some(num)); 626 | } 627 | } 628 | self.unget_token(t); 629 | Ok(None) 630 | } 631 | 632 | /// Matches a single `;` or `&` separator operator 633 | fn separator_op(&mut self) -> anyhow::Result> { 634 | match self.next_token_is_operator(&[Operator::Semicolon, Operator::Ampersand])? { 635 | Some(Token::Operator(Operator::Semicolon, ..)) => Ok(Some(Separator::Sync)), 636 | Some(Token::Operator(Operator::Ampersand, ..)) => Ok(Some(Separator::Async)), 637 | _ => Ok(None), 638 | } 639 | } 640 | 641 | /// Matches either an explicit separator operator or an implicit 642 | /// synchronous separator in the form of a newline. 643 | fn separator(&mut self) -> anyhow::Result> { 644 | if let Some(sep) = self.separator_op()? { 645 | self.linebreak()?; 646 | Ok(Some(sep)) 647 | } else if self.newline_list()? { 648 | Ok(Some(Separator::Sync)) 649 | } else { 650 | Ok(None) 651 | } 652 | } 653 | 654 | /// Matches an optional separator, returning true if that separator 655 | /// is async, or false if it is sync or not present. 656 | fn separator_is_async(&mut self) -> anyhow::Result { 657 | Ok(self.separator()?.unwrap_or(Separator::Sync) == Separator::Async) 658 | } 659 | } 660 | 661 | #[derive(PartialEq, Eq)] 662 | enum Separator { 663 | Sync, 664 | Async, 665 | } 666 | -------------------------------------------------------------------------------- /shell_parser/src/types.rs: -------------------------------------------------------------------------------- 1 | /// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_10_02 2 | use shell_lexer::{Assignment, WordComponent}; 3 | 4 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 5 | pub struct SimpleCommand { 6 | pub assignments: Vec, 7 | pub words: Vec>, 8 | pub redirects: Vec, 9 | } 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq)] 12 | pub struct Command { 13 | pub asynchronous: bool, 14 | pub command: CommandType, 15 | pub redirects: Vec, 16 | } 17 | 18 | #[derive(Debug, Clone, PartialEq, Eq)] 19 | pub enum CommandType { 20 | Pipeline(Pipeline), 21 | SimpleCommand(SimpleCommand), 22 | Program(CompoundList), 23 | BraceGroup(CompoundList), 24 | Subshell(CompoundList), 25 | ForEach(ForEach), 26 | If(If), 27 | UntilLoop(UntilLoop), 28 | WhileLoop(WhileLoop), 29 | FunctionDefinition { name: String, body: Box }, 30 | // TODO: Case 31 | } 32 | 33 | #[derive(Debug, Clone, PartialEq, Eq)] 34 | pub struct Pipeline { 35 | /// true if the pipeline starts with a bang 36 | pub inverted: bool, 37 | pub commands: Vec, 38 | } 39 | 40 | #[derive(Debug, Clone, PartialEq, Eq)] 41 | pub struct CompoundList { 42 | pub commands: Vec, 43 | } 44 | 45 | #[derive(Debug, Clone, PartialEq, Eq)] 46 | pub struct If { 47 | pub condition: CompoundList, 48 | pub true_part: Option, 49 | pub false_part: Option, 50 | } 51 | 52 | #[derive(Debug, Clone, PartialEq, Eq)] 53 | pub struct UntilLoop { 54 | pub body: CompoundList, 55 | pub condition: CompoundList, 56 | } 57 | 58 | #[derive(Debug, Clone, PartialEq, Eq)] 59 | pub struct WhileLoop { 60 | pub condition: CompoundList, 61 | pub body: CompoundList, 62 | } 63 | 64 | #[derive(Debug, Clone, PartialEq, Eq)] 65 | pub struct ForEach { 66 | pub wordlist: Vec>, 67 | pub body: CompoundList, 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq)] 71 | pub enum Redirection { 72 | File(FileRedirection), 73 | Fd(FdDuplication), 74 | } 75 | 76 | #[derive(Debug, Clone, PartialEq, Eq)] 77 | pub struct FileRedirection { 78 | pub fd_number: usize, 79 | pub file_name: Vec, 80 | /// `<` or `<>` 81 | pub input: bool, 82 | /// `>` or `<>` 83 | pub output: bool, 84 | /// `>|` 85 | pub clobber: bool, 86 | /// `>>` 87 | pub append: bool, 88 | } 89 | 90 | #[derive(Debug, Clone, PartialEq, Eq)] 91 | pub struct FdDuplication { 92 | /// Dup `src_fd_number` ... 93 | pub src_fd_number: usize, 94 | /// ... into `dest_fd_number` for the child 95 | pub dest_fd_number: usize, 96 | } 97 | 98 | impl From for Command { 99 | fn from(command: CommandType) -> Command { 100 | Command { 101 | command, 102 | redirects: vec![], 103 | asynchronous: false, 104 | } 105 | } 106 | } 107 | 108 | impl From for Command { 109 | fn from(pipeline: Pipeline) -> Command { 110 | // Simplify a pipeline to the command itself if possible 111 | if !pipeline.inverted && pipeline.commands.len() == 1 { 112 | pipeline.commands.into_iter().next().unwrap() 113 | } else { 114 | CommandType::Pipeline(pipeline).into() 115 | } 116 | } 117 | } 118 | 119 | impl From for CompoundList { 120 | fn from(cmd: Command) -> CompoundList { 121 | CompoundList { 122 | commands: vec![cmd], 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /shell_vm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell_vm" 3 | version = "0.1.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | bstr = "0.1" 9 | caseless = "0.2" 10 | anyhow = "1.0" 11 | filedescriptor = "0.7" 12 | filenamegen = { path = "../filenamegen" } 13 | lazy_static = "1.3" 14 | 15 | [dev-dependencies] 16 | pretty_assertions = "0.6" 17 | -------------------------------------------------------------------------------- /shell_vm/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /shell_vm/src/environment.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use caseless::{canonical_caseless_match_str, Caseless}; 3 | use std::cmp::Ordering; 4 | use std::collections::BTreeMap; 5 | use std::ffi::{OsStr, OsString}; 6 | use std::hash::{Hash, Hasher}; 7 | 8 | #[derive(Debug, Clone)] 9 | struct CaseInsensitiveOsString(OsString); 10 | 11 | impl CaseInsensitiveOsString { 12 | fn compare(&self, other: &OsStr) -> Ordering { 13 | match (self.0.to_str(), other.to_str()) { 14 | (Some(a), Some(b)) => { 15 | let mut a = a.chars().default_case_fold(); 16 | let mut b = b.chars().default_case_fold(); 17 | 18 | loop { 19 | match (a.next(), b.next()) { 20 | (None, None) => return Ordering::Equal, 21 | (None, Some(_)) => return Ordering::Less, 22 | (Some(_), None) => return Ordering::Greater, 23 | (Some(a), Some(b)) => { 24 | let ordering = a.cmp(&b); 25 | if ordering != Ordering::Equal { 26 | return ordering; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | _ => self.0.as_os_str().cmp(other), 33 | } 34 | } 35 | } 36 | 37 | impl PartialEq for CaseInsensitiveOsString { 38 | fn eq(&self, other: &CaseInsensitiveOsString) -> bool { 39 | match (self.0.to_str(), other.0.to_str()) { 40 | (Some(a), Some(b)) => canonical_caseless_match_str(a, b), 41 | _ => self.0.eq(&other.0), 42 | } 43 | } 44 | } 45 | 46 | impl PartialEq for CaseInsensitiveOsString { 47 | fn eq(&self, other: &OsStr) -> bool { 48 | match (self.0.to_str(), other.to_str()) { 49 | (Some(a), Some(b)) => canonical_caseless_match_str(a, b), 50 | _ => self.0.eq(&other), 51 | } 52 | } 53 | } 54 | 55 | impl PartialEq<&OsStr> for CaseInsensitiveOsString { 56 | fn eq(&self, other: &&OsStr) -> bool { 57 | match (self.0.to_str(), other.to_str()) { 58 | (Some(a), Some(b)) => canonical_caseless_match_str(a, b), 59 | _ => self.0.eq(other), 60 | } 61 | } 62 | } 63 | 64 | impl Eq for CaseInsensitiveOsString {} 65 | 66 | impl Ord for CaseInsensitiveOsString { 67 | fn cmp(&self, other: &CaseInsensitiveOsString) -> Ordering { 68 | self.compare(other.0.as_os_str()) 69 | } 70 | } 71 | 72 | impl PartialOrd for CaseInsensitiveOsString { 73 | fn partial_cmp(&self, other: &CaseInsensitiveOsString) -> Option { 74 | Some(self.compare(other.0.as_os_str())) 75 | } 76 | } 77 | 78 | impl PartialOrd for CaseInsensitiveOsString { 79 | fn partial_cmp(&self, other: &OsStr) -> Option { 80 | Some(self.compare(other)) 81 | } 82 | } 83 | 84 | impl PartialOrd<&OsStr> for CaseInsensitiveOsString { 85 | fn partial_cmp(&self, other: &&OsStr) -> Option { 86 | Some(self.compare(other)) 87 | } 88 | } 89 | 90 | impl Hash for CaseInsensitiveOsString { 91 | fn hash(&self, state: &mut H) { 92 | if let Some(s) = self.0.to_str() { 93 | for c in s.chars().default_case_fold() { 94 | c.hash(state); 95 | } 96 | } else { 97 | self.0.hash(state); 98 | } 99 | } 100 | } 101 | 102 | #[derive(Debug, Clone, Eq, PartialEq)] 103 | enum EnvMap { 104 | Posix(BTreeMap), 105 | Windows(BTreeMap), 106 | } 107 | 108 | impl Default for EnvMap { 109 | fn default() -> Self { 110 | if cfg!(windows) { 111 | Self::windows() 112 | } else { 113 | Self::posix() 114 | } 115 | } 116 | } 117 | 118 | impl EnvMap { 119 | fn posix() -> Self { 120 | EnvMap::Posix(BTreeMap::new()) 121 | } 122 | 123 | fn windows() -> Self { 124 | EnvMap::Windows(BTreeMap::new()) 125 | } 126 | 127 | fn set(&mut self, key: OsString, value: OsString) { 128 | match self { 129 | EnvMap::Posix(map) => map.insert(key, value), 130 | EnvMap::Windows(map) => map.insert(CaseInsensitiveOsString(key), value), 131 | }; 132 | } 133 | 134 | fn get(&self, key: &OsStr) -> Option<&OsStr> { 135 | match self { 136 | EnvMap::Posix(map) => map.get(key), 137 | EnvMap::Windows(map) => map.get(&CaseInsensitiveOsString(key.to_os_string())), 138 | } 139 | .map(OsString::as_os_str) 140 | } 141 | 142 | fn unset(&mut self, key: &OsStr) { 143 | match self { 144 | EnvMap::Posix(map) => map.remove(key), 145 | EnvMap::Windows(map) => map.remove(&CaseInsensitiveOsString(key.to_os_string())), 146 | }; 147 | } 148 | 149 | fn iter(&self) -> impl Iterator { 150 | // Using this technique to avoid incompatible match arms errors: 151 | // https://stackoverflow.com/a/54728634/149111 152 | let mut posix = None; 153 | let mut windows = None; 154 | match self { 155 | EnvMap::Posix(map) => posix = Some(map.iter()), 156 | EnvMap::Windows(map) => windows = Some(map.iter().map(|(k, v)| (&k.0, v))), 157 | }; 158 | 159 | posix 160 | .into_iter() 161 | .flatten() 162 | .chain(windows.into_iter().flatten()) 163 | } 164 | } 165 | 166 | /// The environment represents the environmental variables 167 | /// associated with the shell and the processes that it spawns. 168 | #[derive(Clone, Debug, PartialEq, Eq)] 169 | pub struct Environment { 170 | map: EnvMap, 171 | } 172 | 173 | impl Environment { 174 | pub fn new() -> Self { 175 | let mut environ = Self { 176 | map: Default::default(), 177 | }; 178 | for (key, value) in std::env::vars_os() { 179 | environ.set(key, value); 180 | } 181 | environ 182 | } 183 | 184 | pub fn new_empty() -> Self { 185 | Self { 186 | map: Default::default(), 187 | } 188 | } 189 | 190 | pub fn get_str + std::fmt::Debug>( 191 | &self, 192 | key: K, 193 | ) -> anyhow::Result> { 194 | match self.get(key.as_ref()) { 195 | None => Ok(None), 196 | Some(v) => match v.to_str() { 197 | Some(s) => Ok(Some(s)), 198 | None => bail!( 199 | "unable to convert environment value {:?} for key {:?} to String", 200 | v, 201 | key 202 | ), 203 | }, 204 | } 205 | } 206 | 207 | pub fn set + ?Sized, V: Into + ?Sized>( 208 | &mut self, 209 | key: K, 210 | value: V, 211 | ) { 212 | self.map.set(key.into(), value.into()); 213 | } 214 | 215 | pub fn append_path + ?Sized, V: Into + ?Sized>( 216 | &mut self, 217 | key: K, 218 | value: V, 219 | ) -> Result<(), std::env::JoinPathsError> { 220 | let key = key.into(); 221 | let mut current_path: Vec<_> = self 222 | .get(&key) 223 | .map(|p| std::env::split_paths(p).collect()) 224 | .unwrap_or_else(Vec::new); 225 | 226 | current_path.push(value.into().into()); 227 | let new_path = std::env::join_paths(current_path.iter())?; 228 | self.set(key, new_path); 229 | Ok(()) 230 | } 231 | 232 | pub fn prepend_path + ?Sized, V: Into + ?Sized>( 233 | &mut self, 234 | key: K, 235 | value: V, 236 | ) -> Result<(), std::env::JoinPathsError> { 237 | let key = key.into(); 238 | let mut current_path: Vec<_> = self 239 | .get(&key) 240 | .map(|p| std::env::split_paths(p).collect()) 241 | .unwrap_or_else(Vec::new); 242 | 243 | current_path.insert(0, value.into().into()); 244 | let new_path = std::env::join_paths(current_path.iter())?; 245 | self.set(key, new_path); 246 | Ok(()) 247 | } 248 | 249 | pub fn get>(&self, key: K) -> Option<&OsStr> { 250 | self.map.get(key.as_ref()) 251 | } 252 | 253 | pub fn unset>(&mut self, key: K) { 254 | self.map.unset(key.as_ref()); 255 | } 256 | 257 | pub fn iter(&self) -> impl Iterator { 258 | self.map.iter() 259 | } 260 | } 261 | 262 | #[cfg(test)] 263 | mod test { 264 | use super::*; 265 | 266 | fn case_insensitive() { 267 | let foo = CaseInsensitiveOsString("foo".into()); 268 | let food = CaseInsensitiveOsString("food".into()); 269 | let big_foo = CaseInsensitiveOsString("FOO".into()); 270 | assert_eq!(foo, big_foo); 271 | assert_ne!(foo, food); 272 | assert_eq!(foo.cmp(&big_foo), Ordering::Equal); 273 | assert_eq!(foo.cmp(&food), Ordering::Less); 274 | assert_eq!(food.cmp(&foo), Ordering::Greater); 275 | 276 | let foo_os_str = OsStr::new("foo"); 277 | assert_eq!(foo, foo_os_str); 278 | assert_eq!(foo.partial_cmp(foo_os_str), Some(Ordering::Equal)); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /shell_vm/src/host.rs: -------------------------------------------------------------------------------- 1 | use crate::{Environment, IoEnvironment, Program, Status, Value}; 2 | use std::ffi::OsString; 3 | use std::path::PathBuf; 4 | use std::sync::Arc; 5 | 6 | /// The WaitForStatus trait allows waiting on a spawned command. 7 | /// Since the command could be a child process, some action 8 | /// running in a another thread, or perhaps even be an inline 9 | /// or immediately ready thing, the trait gives some flexibility 10 | /// in waiting on whatever that implementation may be. 11 | pub trait WaitForStatus: std::fmt::Debug { 12 | /// Non-blocking check for the status of the item 13 | fn poll(&self) -> Option; 14 | /// Block until the status of the item changes from Running 15 | /// to some other status. It is possible that this may be 16 | /// subject to a spurious wakeup and that the returned 17 | /// status still shows as Running. 18 | fn wait(&self) -> Option; 19 | } 20 | 21 | /// Status is always immediately ready with its own value. 22 | impl WaitForStatus for Status { 23 | fn wait(&self) -> Option { 24 | Some(self.clone()) 25 | } 26 | 27 | fn poll(&self) -> Option { 28 | self.wait() 29 | } 30 | } 31 | 32 | impl From for WaitableStatus { 33 | fn from(status: Status) -> WaitableStatus { 34 | WaitableStatus::new(Arc::new(status)) 35 | } 36 | } 37 | 38 | /// The WaitableStatus type is a little wrapper around the WaitForStatus 39 | /// trait that allows embedding a concrete type into the Value enum 40 | /// so that the status is visible to the vm. 41 | #[derive(Clone, Debug)] 42 | pub struct WaitableStatus { 43 | waiter: Arc, 44 | } 45 | 46 | /// PartialEq is required by the Value enum. This is a simple test for 47 | /// equality based on the polled Status. 48 | impl PartialEq for WaitableStatus { 49 | fn eq(&self, rhs: &WaitableStatus) -> bool { 50 | let lhs = self.poll(); 51 | let rhs = rhs.poll(); 52 | lhs == rhs 53 | } 54 | } 55 | 56 | /// Eq is required by the Value enum 57 | impl Eq for WaitableStatus {} 58 | 59 | impl WaitableStatus { 60 | pub fn new(waiter: Arc) -> Self { 61 | Self { waiter } 62 | } 63 | 64 | /// Non-blocking check for the status of the item 65 | pub fn poll(&self) -> Option { 66 | self.waiter.poll() 67 | } 68 | 69 | /// Block until the status of the item changes from Running 70 | /// to some other status. It is possible that this may be 71 | /// subject to a spurious wakeup and that the returned 72 | /// status still shows as Running. 73 | pub fn wait(&self) -> Option { 74 | self.waiter.wait() 75 | } 76 | } 77 | 78 | pub trait ShellHost: std::fmt::Debug { 79 | /// Look up the home directory for the specified user. 80 | /// If user is not specified, look it up for the current user. 81 | fn lookup_homedir(&self, user: Option<&str>) -> anyhow::Result; 82 | 83 | /// Spawn a command. 84 | /// The argument vector is pre-built, and the cwd, IO and environment 85 | /// variables are set up according to any redirection or other overrides 86 | /// specified by the user, so the role of the ShellHost in dispatching 87 | /// this method is to determine how to spawn the command. 88 | /// Most shells offer one of the following mechanisms, depending on 89 | /// the command: 90 | /// * Spawning a child process 91 | /// * Execution a function defined by the shell language script 92 | /// * Executing a builtin function. 93 | /// This interface allows for any of these and for others to 94 | /// occur by allowing the host to initiate running the command, 95 | /// but not requiring that it be complete upon returning from 96 | /// this method. Instead, the WaitableStatus type allows the 97 | /// shell VM to poll or block until the command is complete. 98 | /// 99 | /// argv: 100 | /// The argument vector. Element 0 contains the name of 101 | /// the command to be run. The elements should generally 102 | /// be either String or OsString, but we allow other types 103 | /// for potential future flexibility (eg: passing a list directly 104 | /// to a function without joining/splitting). 105 | /// 106 | /// environment: 107 | /// The set of environment variables to propagate to the 108 | /// command. Note that this is mutable in order to allow 109 | /// builtins such as `cd` to update the environment accordingly. 110 | /// 111 | /// current_directory: 112 | /// The current working directory. This is mutable to 113 | /// support the `cd` builtin. 114 | /// 115 | /// io_env: 116 | /// The io environment allows the command to read or write 117 | /// to the stdio streams, or other defined descriptor numbers. 118 | fn spawn_command( 119 | &self, 120 | argv: &Vec, 121 | environment: &mut Environment, 122 | current_directory: &mut PathBuf, 123 | io_env: &IoEnvironment, 124 | ) -> anyhow::Result; 125 | 126 | fn define_function(&self, name: &str, program: &Arc) -> anyhow::Result<()>; 127 | } 128 | -------------------------------------------------------------------------------- /shell_vm/src/ioenv.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use filedescriptor::FileDescriptor; 3 | use std::collections::HashMap; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | #[derive(Clone)] 7 | pub struct IoEnvironment { 8 | fds: HashMap>>, 9 | } 10 | 11 | impl std::fmt::Debug for IoEnvironment { 12 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 13 | fmt.debug_struct("IoEnvironment").finish() 14 | } 15 | } 16 | 17 | pub struct Readable { 18 | fd: Arc>, 19 | } 20 | 21 | impl Readable { 22 | pub fn dup(&self) -> anyhow::Result { 23 | self.fd.lock().unwrap().try_clone() 24 | } 25 | } 26 | 27 | impl std::io::Read for Readable { 28 | fn read(&mut self, buf: &mut [u8]) -> Result { 29 | self.fd.lock().unwrap().read(buf) 30 | } 31 | } 32 | 33 | pub struct Writable { 34 | fd: Arc>, 35 | } 36 | 37 | impl Writable { 38 | pub fn dup(&self) -> anyhow::Result { 39 | self.fd.lock().unwrap().try_clone() 40 | } 41 | } 42 | 43 | impl std::io::Write for Writable { 44 | fn write(&mut self, buf: &[u8]) -> Result { 45 | self.fd.lock().unwrap().write(buf) 46 | } 47 | 48 | fn flush(&mut self) -> Result<(), std::io::Error> { 49 | self.fd.lock().unwrap().flush() 50 | } 51 | } 52 | 53 | impl IoEnvironment { 54 | pub fn new() -> anyhow::Result { 55 | let mut fds = HashMap::new(); 56 | 57 | macro_rules! stdio { 58 | ($fd:literal, $func:path) => { 59 | fds.insert($fd, Arc::new(Mutex::new(FileDescriptor::dup(&$func())?))); 60 | }; 61 | } 62 | 63 | stdio!(0, std::io::stdin); 64 | stdio!(1, std::io::stdout); 65 | stdio!(2, std::io::stderr); 66 | 67 | Ok(Self { fds }) 68 | } 69 | 70 | pub fn stdin(&self) -> Readable { 71 | let fd = Arc::clone(self.fds.get(&0).expect("stdin fd is missing")); 72 | Readable { fd } 73 | } 74 | 75 | pub fn stdout(&self) -> Writable { 76 | let fd = Arc::clone(self.fds.get(&1).expect("stdout fd is missing")); 77 | Writable { fd } 78 | } 79 | 80 | pub fn stderr(&self) -> Writable { 81 | let fd = Arc::clone(self.fds.get(&2).expect("stdout fd is missing")); 82 | Writable { fd } 83 | } 84 | 85 | pub fn assign_fd(&mut self, fd_number: usize, fd: FileDescriptor) { 86 | self.fds.insert(fd_number, Arc::new(Mutex::new(fd))); 87 | } 88 | 89 | pub fn duplicate_to(&mut self, src_fd: usize, dest_fd: usize) -> anyhow::Result<()> { 90 | let fd = Arc::clone( 91 | self.fds 92 | .get(&src_fd) 93 | .ok_or_else(|| anyhow!("duplicate_to: src_fd {} not present", src_fd))?, 94 | ); 95 | self.fds.insert(dest_fd, fd); 96 | Ok(()) 97 | } 98 | 99 | pub fn fd_as_stdio(&self, fd_number: usize) -> anyhow::Result { 100 | let fd = self 101 | .fds 102 | .get(&fd_number) 103 | .ok_or_else(|| anyhow!("fd_as_stdio: fd {} not present", fd_number))?; 104 | fd.lock().unwrap().as_stdio() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /shell_vm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use anyhow::{anyhow, bail, Error}; 3 | use bstr::{BStr, BString}; 4 | use filedescriptor::FileDescriptor; 5 | use std::collections::VecDeque; 6 | use std::ffi::{OsStr, OsString}; 7 | use std::path::{Path, PathBuf}; 8 | use std::sync::Arc; 9 | 10 | mod environment; 11 | mod host; 12 | mod ioenv; 13 | 14 | pub mod op; 15 | pub use environment::*; 16 | pub use host::*; 17 | pub use ioenv::*; 18 | pub use op::Operation; 19 | use op::*; 20 | 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum Value { 23 | None, 24 | String(String), 25 | OsString(OsString), 26 | List(Vec), 27 | Integer(isize), 28 | WaitableStatus(WaitableStatus), 29 | } 30 | 31 | impl Value { 32 | pub fn as_os_str(&self) -> Option<&OsStr> { 33 | match self { 34 | Value::String(s) => Some(s.as_ref()), 35 | Value::OsString(s) => Some(s.as_os_str()), 36 | _ => None, 37 | } 38 | } 39 | 40 | pub fn as_str(&self) -> Option<&str> { 41 | match self { 42 | Value::String(s) => Some(s.as_ref()), 43 | Value::OsString(s) => s.to_str(), 44 | _ => None, 45 | } 46 | } 47 | 48 | pub fn as_bstr(&self) -> Option<&BStr> { 49 | match self { 50 | Value::String(s) => Some(s.as_str().into()), 51 | Value::OsString(s) => BStr::from_os_str(s), 52 | Value::None => Some("".into()), 53 | _ => None, 54 | } 55 | } 56 | 57 | pub fn into_bstring(self) -> Option { 58 | match self { 59 | Value::String(s) => Some(s.into()), 60 | Value::OsString(s) => BString::from_os_string(s).ok(), 61 | _ => None, 62 | } 63 | } 64 | 65 | pub fn truthy(&self) -> bool { 66 | match self { 67 | Value::None => false, 68 | Value::String(s) => !s.is_empty(), 69 | Value::OsString(s) => !s.is_empty(), 70 | Value::List(list) => !list.is_empty(), 71 | Value::Integer(n) => *n != 0, 72 | Value::WaitableStatus(status) => { 73 | match status.poll() { 74 | // Invert for the program return code: 0 is success 75 | Some(Status::Complete(value)) => !value.truthy(), 76 | _ => false, 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | impl> From<&T> for Value { 84 | fn from(s: &T) -> Value { 85 | Value::String(s.as_ref().to_owned()) 86 | } 87 | } 88 | 89 | impl From for Value { 90 | fn from(s: String) -> Value { 91 | Value::String(s) 92 | } 93 | } 94 | 95 | impl From for Value { 96 | fn from(s: OsString) -> Value { 97 | Value::OsString(s) 98 | } 99 | } 100 | 101 | impl From for Value { 102 | fn from(s: isize) -> Value { 103 | Value::Integer(s) 104 | } 105 | } 106 | 107 | impl From> for Value { 108 | fn from(s: Vec) -> Value { 109 | Value::List(s) 110 | } 111 | } 112 | 113 | impl std::convert::TryFrom for Value { 114 | type Error = Error; 115 | fn try_from(b: BString) -> Result { 116 | match b.into_string() { 117 | Ok(s) => Ok(Value::String(s)), 118 | Err(e) => match e.into_bstring().into_os_string() { 119 | Ok(os) => Ok(Value::OsString(os)), 120 | Err(_) => bail!("BString is neither UTF-8 nor representable as an OsString"), 121 | }, 122 | } 123 | } 124 | } 125 | 126 | #[derive(Debug, Clone, PartialEq, Eq)] 127 | pub enum Operand { 128 | /// A value known at compilation time 129 | Immediate(Value), 130 | /// A value located from the stack at runtime. 131 | /// The number is relative to the current frame 132 | /// pointer. 133 | FrameRelative(usize), 134 | /// The status from the most recently waited command 135 | LastWaitStatus, 136 | } 137 | 138 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] 139 | pub enum InstructionAddress { 140 | /// Relative to the start of the program 141 | Absolute(usize), 142 | /// Relative to the current program position 143 | Relative(isize), 144 | } 145 | 146 | #[derive(Debug, Default, PartialEq, Eq)] 147 | pub struct Program { 148 | opcodes: Vec, 149 | } 150 | 151 | impl Program { 152 | pub fn new(opcodes: Vec) -> Arc { 153 | Arc::new(Self { opcodes }) 154 | } 155 | 156 | pub fn opcodes(&self) -> &[Operation] { 157 | &self.opcodes 158 | } 159 | } 160 | 161 | #[derive(Debug, Default)] 162 | pub struct Frame { 163 | /// Absolute index to the top of the stack including the 164 | /// data required for this frame. FrameRelative addressing 165 | /// is relative to this position. 166 | frame_pointer: usize, 167 | 168 | /// The number of stack entries occupied by this frame. 169 | /// When the frame is popped, the stack is trimmed to 170 | /// (frame_pointer-frame_size) entries. 171 | frame_size: usize, 172 | } 173 | 174 | #[derive(Debug, Default)] 175 | pub struct Machine { 176 | stack: VecDeque, 177 | frames: VecDeque, 178 | environment: VecDeque, 179 | io_env: VecDeque, 180 | positional: Vec, 181 | cwd: PathBuf, 182 | host: Option>, 183 | pipes: VecDeque, 184 | 185 | program: Arc, 186 | program_counter: usize, 187 | 188 | last_wait_status: Option, 189 | } 190 | 191 | /// This enum is essentially why this vm exists; it allows stepping 192 | /// through a program and returning control to the host application 193 | /// in the event that a process being waited upon is stopped. 194 | #[derive(Debug, Clone, PartialEq, Eq)] 195 | pub enum Status { 196 | /// The program is still running; call step() again to make 197 | /// or check for progress. 198 | Running, 199 | /// The program is waiting on a process that has been put into 200 | /// the background and stopped. Calling step() again will 201 | /// continue to return Stopped until the status of that process 202 | /// has changed. 203 | Stopped, 204 | /// The program has completed and yielded a value. 205 | Complete(Value), 206 | } 207 | 208 | fn split_by_ifs<'a>(value: &'a str, ifs: &str) -> Vec<&'a str> { 209 | let ifs: std::collections::HashSet = ifs.chars().collect(); 210 | let mut split = vec![]; 211 | let mut run_start = None; 212 | 213 | for (idx, c) in value.char_indices() { 214 | if ifs.contains(&c) { 215 | if let Some(start) = run_start.take() { 216 | if idx > start { 217 | split.push(&value[start..idx]); 218 | } 219 | } 220 | continue; 221 | } 222 | if run_start.is_none() { 223 | run_start = Some(idx); 224 | } 225 | } 226 | 227 | if let Some(start) = run_start.take() { 228 | split.push(&value[start..]); 229 | } 230 | 231 | split 232 | } 233 | 234 | impl Machine { 235 | pub fn new( 236 | program: &Arc, 237 | env: Option, 238 | cwd: &Path, 239 | ) -> anyhow::Result { 240 | let mut environment = VecDeque::new(); 241 | environment.push_back(env.unwrap_or_else(Environment::new)); 242 | 243 | let mut io_env = VecDeque::new(); 244 | io_env.push_back(IoEnvironment::new()?); 245 | 246 | Ok(Self { 247 | program: Arc::clone(program), 248 | environment, 249 | io_env, 250 | cwd: cwd.to_path_buf(), 251 | ..Default::default() 252 | }) 253 | } 254 | 255 | pub fn set_positional(&mut self, argv: Vec) { 256 | self.positional = argv; 257 | } 258 | 259 | pub fn top_environment(&self) -> (PathBuf, Environment) { 260 | (self.cwd.clone(), self.environment.front().unwrap().clone()) 261 | } 262 | 263 | pub fn set_host(&mut self, host: Arc) { 264 | self.host = Some(host) 265 | } 266 | 267 | fn environment(&self) -> anyhow::Result<&Environment> { 268 | self.environment 269 | .back() 270 | .ok_or_else(|| anyhow!("no current environment")) 271 | } 272 | 273 | fn environment_mut(&mut self) -> anyhow::Result<&mut Environment> { 274 | self.environment 275 | .back_mut() 276 | .ok_or_else(|| anyhow!("no current environment")) 277 | } 278 | 279 | /// Compute the effective value of IFS 280 | fn ifs(&self) -> anyhow::Result<&str> { 281 | Ok(self.environment()?.get_str("IFS")?.unwrap_or(" \t\n")) 282 | } 283 | 284 | pub fn io_env(&self) -> anyhow::Result<&IoEnvironment> { 285 | self.io_env 286 | .back() 287 | .ok_or_else(|| anyhow!("no current IoEnvironment")) 288 | } 289 | 290 | pub fn io_env_mut(&mut self) -> anyhow::Result<&mut IoEnvironment> { 291 | self.io_env 292 | .back_mut() 293 | .ok_or_else(|| anyhow!("no current IoEnvironment")) 294 | } 295 | 296 | /// Attempt to make a single step of progress with the program. 297 | pub fn step(&mut self) -> anyhow::Result { 298 | let program = Arc::clone(&self.program); 299 | let op = program 300 | .opcodes 301 | .get(self.program_counter) 302 | .ok_or_else(|| anyhow!("walked off the end of the program"))?; 303 | let pc = self.program_counter; 304 | self.program_counter += 1; 305 | match op.dispatch(self) { 306 | status @ Ok(Status::Stopped) => { 307 | // Rewind so that we retry this same op next time around 308 | self.program_counter -= 1; 309 | status 310 | } 311 | Err(e) => Err(anyhow!("PC={}: {}", pc, e)), 312 | status => status, 313 | } 314 | } 315 | 316 | /// Continually invoke step() while the status == Running. 317 | /// Returns either Stopped or Complete at the appropriate time. 318 | pub fn run(&mut self) -> anyhow::Result { 319 | loop { 320 | match self.step()? { 321 | Status::Running => continue, 322 | done => return Ok(done), 323 | } 324 | } 325 | } 326 | 327 | /// Resolve an operand for write. 328 | pub fn operand_mut(&mut self, operand: &Operand) -> anyhow::Result<&mut Value> { 329 | match operand { 330 | Operand::Immediate(_) => bail!("cannot mutably reference an Immediate operand"), 331 | Operand::LastWaitStatus => bail!("cannot mutably reference LastWaitStatus"), 332 | Operand::FrameRelative(offset) => self 333 | .stack 334 | .get_mut( 335 | self.frames 336 | .back() 337 | .ok_or_else(|| anyhow!("no frame?"))? 338 | .frame_pointer 339 | - offset, 340 | ) 341 | .ok_or_else(|| anyhow!("FrameRelative offset out of range")), 342 | } 343 | } 344 | 345 | /// Resolve an operand for read. 346 | pub fn operand<'a>(&'a self, operand: &'a Operand) -> anyhow::Result<&'a Value> { 347 | match operand { 348 | Operand::Immediate(value) => Ok(value), 349 | Operand::LastWaitStatus => self 350 | .last_wait_status 351 | .as_ref() 352 | .ok_or_else(|| anyhow!("cannot reference LastWaitStatus as it has not been set")), 353 | Operand::FrameRelative(offset) => self 354 | .stack 355 | .get( 356 | self.frames 357 | .back() 358 | .ok_or_else(|| anyhow!("no frame?"))? 359 | .frame_pointer 360 | - offset, 361 | ) 362 | .ok_or_else(|| anyhow!("FrameRelative offset out of range")), 363 | } 364 | } 365 | 366 | pub fn operand_as_os_str<'a>(&'a self, operand: &'a Operand) -> anyhow::Result<&'a OsStr> { 367 | let value = self.operand(operand)?; 368 | value.as_os_str().ok_or_else(|| { 369 | anyhow!( 370 | "operand {:?} of value {:?} is not representable as OsStr", 371 | operand, 372 | value 373 | ) 374 | }) 375 | } 376 | 377 | pub fn operand_as_str<'a>(&'a self, operand: &'a Operand) -> anyhow::Result<&'a str> { 378 | let value = self.operand(operand)?; 379 | value.as_str().ok_or_else(|| { 380 | anyhow!( 381 | "operand {:?} of value {:?} is not representable as String", 382 | operand, 383 | value 384 | ) 385 | }) 386 | } 387 | 388 | /// Resolve an operand for read, and return true if its value 389 | /// evaluates as true in a trutihness test. 390 | pub fn operand_truthy(&self, operand: &Operand) -> anyhow::Result { 391 | Ok(self.operand(operand)?.truthy()) 392 | } 393 | 394 | fn push_with_glob( 395 | &self, 396 | list: &mut Vec, 397 | glob: bool, 398 | remove_backslash: bool, 399 | v: Value, 400 | ) -> anyhow::Result<()> { 401 | if glob && contains_glob_specials(&v) { 402 | let pattern = v 403 | .as_str() 404 | .ok_or_else(|| anyhow!("contains_glob_specials returned true for non String?"))?; 405 | let glob = filenamegen::Glob::new(pattern)?; 406 | for item in glob.walk(&self.cwd) { 407 | list.push(item.into_os_string().into()) 408 | } 409 | } else { 410 | match (remove_backslash, v.as_str()) { 411 | (true, Some(s)) => { 412 | let mut string = String::with_capacity(s.len()); 413 | let mut current = s.chars(); 414 | while let Some(c) = current.next() { 415 | if c == '\\' { 416 | if let Some(n) = current.next() { 417 | string.push(n); 418 | } else { 419 | string.push(c); 420 | } 421 | } else { 422 | string.push(c); 423 | } 424 | } 425 | list.push(string.into()); 426 | } 427 | _ => list.push(v), 428 | } 429 | } 430 | Ok(()) 431 | } 432 | } 433 | 434 | fn contains_glob_specials(v: &Value) -> bool { 435 | match v.as_str() { 436 | Some(s) => { 437 | for c in s.chars() { 438 | if c == '*' || c == '[' || c == '{' { 439 | return true; 440 | } 441 | } 442 | false 443 | } 444 | _ => false, 445 | } 446 | } 447 | 448 | #[cfg(test)] 449 | mod test { 450 | use super::*; 451 | use pretty_assertions::assert_eq; 452 | 453 | fn prog(ops: &[Operation]) -> Arc { 454 | Program::new(ops.to_vec()) 455 | } 456 | 457 | fn machine(ops: &[Operation]) -> Machine { 458 | Machine::new(&prog(ops), None, &std::env::current_dir().unwrap()).unwrap() 459 | } 460 | 461 | fn run_err(m: &mut Machine) -> String { 462 | format!("{}", m.run().unwrap_err()) 463 | } 464 | 465 | #[test] 466 | fn test_exit() -> anyhow::Result<()> { 467 | let mut m = machine(&[Operation::Exit(Exit { 468 | value: Operand::Immediate(Value::None), 469 | })]); 470 | assert_eq!(m.step()?, Status::Complete(Value::None)); 471 | Ok(()) 472 | } 473 | 474 | #[test] 475 | fn test_read_invalid_operand_no_frame() { 476 | let mut m = machine(&[Operation::Exit(Exit { 477 | value: Operand::FrameRelative(0), 478 | })]); 479 | assert_eq!(run_err(&mut m), "PC=0: no frame?"); 480 | } 481 | 482 | #[test] 483 | fn test_pop_too_many_frames() { 484 | let mut m = machine(&[Operation::PopFrame(PopFrame {})]); 485 | assert_eq!(run_err(&mut m), "PC=0: frame underflow"); 486 | } 487 | 488 | #[test] 489 | fn test_read_invalid_operand() { 490 | let mut m = machine(&[ 491 | Operation::PushFrame(PushFrame { size: 1 }), 492 | Operation::Exit(Exit { 493 | value: Operand::FrameRelative(0), 494 | }), 495 | ]); 496 | assert_eq!(run_err(&mut m), "PC=1: FrameRelative offset out of range"); 497 | } 498 | 499 | #[test] 500 | fn test_write_invalid_operand() { 501 | let mut m = machine(&[Operation::Copy(Copy { 502 | source: Operand::Immediate(Value::None), 503 | destination: Operand::Immediate(Value::Integer(1)), 504 | })]); 505 | assert_eq!( 506 | run_err(&mut m), 507 | "PC=0: cannot mutably reference an Immediate operand" 508 | ); 509 | } 510 | 511 | #[test] 512 | fn test_unterminated() { 513 | let mut m = machine(&[]); 514 | assert_eq!(run_err(&mut m), "walked off the end of the program"); 515 | } 516 | 517 | #[test] 518 | fn test_copy() -> anyhow::Result<()> { 519 | let mut m = machine(&[ 520 | Operation::PushFrame(PushFrame { size: 1 }), 521 | Operation::Copy(Copy { 522 | source: Operand::Immediate(Value::Integer(42)), 523 | destination: Operand::FrameRelative(1), 524 | }), 525 | Operation::Exit(Exit { 526 | value: Operand::FrameRelative(1), 527 | }), 528 | ]); 529 | assert_eq!(m.run()?, Status::Complete(Value::Integer(42))); 530 | Ok(()) 531 | } 532 | 533 | #[test] 534 | fn test_split_by_ifs() { 535 | let ifs = " \t\n"; 536 | assert_eq!(split_by_ifs("a b", ifs), vec!["a", "b"]); 537 | assert_eq!(split_by_ifs("foo", ifs), vec!["foo"]); 538 | assert_eq!(split_by_ifs("foo bar", ifs), vec!["foo", "bar"]); 539 | assert_eq!(split_by_ifs("foo bar ", ifs), vec!["foo", "bar"]); 540 | assert_eq!(split_by_ifs("\t foo bar ", ifs), vec!["foo", "bar"]); 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /src/builtins/builtins.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use cancel::Token; 4 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 5 | use std::io::Write; 6 | use std::path::PathBuf; 7 | use std::sync::Arc; 8 | use structopt::*; 9 | 10 | #[derive(Debug, StructOpt)] 11 | /// List builtin commands 12 | pub struct BuiltinsCommand {} 13 | impl Builtin for BuiltinsCommand { 14 | fn name() -> &'static str { 15 | "builtins" 16 | } 17 | 18 | fn run( 19 | &mut self, 20 | _environment: &mut Environment, 21 | _current_directory: &mut PathBuf, 22 | io_env: &IoEnvironment, 23 | _cancel: Arc, 24 | _functions: &Arc, 25 | ) -> anyhow::Result { 26 | let mut builtins: Vec<&'static str> = super::BUILTINS.iter().map(|(k, _)| *k).collect(); 27 | builtins.sort_unstable(); 28 | for k in builtins { 29 | writeln!(io_env.stdout(), "{}", k)?; 30 | } 31 | Ok(Status::Complete(0.into()).into()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/builtins/colon.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use cancel::Token; 4 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 5 | use std::path::PathBuf; 6 | use std::sync::Arc; 7 | use structopt::*; 8 | 9 | #[derive(StructOpt)] 10 | /// The `:` command only expand command arguments. It is used when a command is needed, as in the 11 | /// then condition of an if command, but nothing is done by the command. 12 | pub struct ColonCommand {} 13 | 14 | impl Builtin for ColonCommand { 15 | fn name() -> &'static str { 16 | ":" 17 | } 18 | 19 | fn run( 20 | &mut self, 21 | _environment: &mut Environment, 22 | _current_directory: &mut PathBuf, 23 | _io_env: &IoEnvironment, 24 | _cancel: Arc, 25 | _functions: &Arc, 26 | ) -> anyhow::Result { 27 | Ok(Status::Complete(0.into()).into()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/builtins/echo.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use cancel::Token; 4 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 5 | use std::io::Write; 6 | use std::path::PathBuf; 7 | use std::sync::Arc; 8 | use structopt::*; 9 | 10 | #[derive(StructOpt)] 11 | /// Write arguments to standard output 12 | pub struct EchoCommand { 13 | /// Do not output the trailing newline 14 | #[structopt(short = "n")] 15 | no_newline: bool, 16 | 17 | /// Enable interpretation of backslash escapes 18 | #[structopt(short = "e", overrides_with = "disable_escapes")] 19 | enable_escapes: bool, 20 | 21 | /// Disable interpretation of backslash escapes (default) 22 | #[structopt(short = "E", overrides_with = "enable_escapes")] 23 | _disable_escapes: bool, 24 | 25 | /// The strings to output 26 | strings: Vec, 27 | } 28 | 29 | fn maybe_octal(s: &str) -> Option<(char, usize)> { 30 | if s.len() >= 3 { 31 | if let Ok(num) = u8::from_str_radix(&s[..3], 8) { 32 | return Some((num as char, 3)); 33 | } 34 | } 35 | if s.len() >= 2 { 36 | if let Ok(num) = u8::from_str_radix(&s[..2], 8) { 37 | return Some((num as char, 2)); 38 | } 39 | } 40 | if s.len() >= 1 { 41 | if let Ok(num) = u8::from_str_radix(&s[..1], 8) { 42 | return Some((num as char, 1)); 43 | } 44 | } 45 | None 46 | } 47 | 48 | fn maybe_hex(s: &str) -> Option<(char, usize)> { 49 | if s.len() >= 2 { 50 | if let Ok(num) = u8::from_str_radix(&s[..2], 16) { 51 | return Some((num as char, 2)); 52 | } 53 | } 54 | if s.len() >= 1 { 55 | if let Ok(num) = u8::from_str_radix(&s[..1], 16) { 56 | return Some((num as char, 1)); 57 | } 58 | } 59 | None 60 | } 61 | 62 | fn echo_escapes(mut s: &str) -> String { 63 | let mut result = String::new(); 64 | 65 | while let Some(pos) = s.find('\\') { 66 | // Emit text preceding this 67 | if pos > 0 { 68 | result.push_str(&s[..pos]); 69 | } 70 | 71 | s = &s[pos..]; 72 | 73 | if s.len() < 2 { 74 | break; 75 | } 76 | 77 | match s.chars().nth(1).unwrap() { 78 | '\\' => result.push('\\'), 79 | 'a' => result.push('\x07'), 80 | 'b' => result.push('\x08'), 81 | 'c' => { 82 | // produce no further output! 83 | return result; 84 | } 85 | 'e' => result.push('\x1b'), 86 | 'f' => result.push('\x0c'), 87 | 'n' => result.push('\x0a'), 88 | 'r' => result.push('\x0d'), 89 | 't' => result.push('\t'), 90 | 'v' => result.push('\x0b'), 91 | '0' => { 92 | // Octal number with 1-3 digits 93 | if let Some((c, len)) = maybe_octal(&s[2..]) { 94 | result.push(c); 95 | s = &s[2 + len..]; 96 | continue; 97 | } 98 | // Wasn't a valid escape, so just emit 99 | // that portion as-is 100 | result.push_str(&s[..2]); 101 | } 102 | 'x' => { 103 | // hex number with 1-2 digits 104 | if let Some((c, len)) = maybe_hex(&s[2..]) { 105 | result.push(c); 106 | s = &s[2 + len..]; 107 | continue; 108 | } 109 | // Wasn't a valid escape, so just emit 110 | // that portion as-is 111 | result.push_str(&s[..2]); 112 | } 113 | _ => { 114 | // Unknown escape 115 | result.push_str(&s[..2]); 116 | } 117 | } 118 | 119 | s = &s[2..]; 120 | } 121 | result.push_str(s); 122 | 123 | result 124 | } 125 | 126 | #[cfg(test)] 127 | mod test { 128 | use super::*; 129 | 130 | #[test] 131 | fn escapes() { 132 | assert_eq!(echo_escapes("foo"), "foo"); 133 | assert_eq!(echo_escapes("foo\\\\"), "foo\\"); 134 | assert_eq!(echo_escapes("foo\\twoot"), "foo\twoot"); 135 | assert_eq!(echo_escapes("foo\\cnot me"), "foo"); 136 | assert_eq!(echo_escapes("foo\\x20"), "foo\x20"); 137 | assert_eq!(echo_escapes("foo\\x2 "), "foo\x02 "); 138 | assert_eq!(echo_escapes("foo\\0003"), "foo\x03"); 139 | assert_eq!(echo_escapes("foo\\003"), "foo\x03"); 140 | assert_eq!(echo_escapes("foo\\03"), "foo\x03"); 141 | 142 | // o777 is 511 and is out of range for ascii, 143 | // so the octal escape resolves as o77 which is 144 | // 63 in decimal -- the question mark. 145 | // That leaves the final 7 as the next char 146 | assert_eq!(echo_escapes("foo\\0777"), "foo?7"); 147 | } 148 | } 149 | 150 | impl Builtin for EchoCommand { 151 | fn name() -> &'static str { 152 | "echo" 153 | } 154 | 155 | fn run( 156 | &mut self, 157 | _environment: &mut Environment, 158 | _current_directory: &mut PathBuf, 159 | io_env: &IoEnvironment, 160 | cancel: Arc, 161 | _functions: &Arc, 162 | ) -> anyhow::Result { 163 | let joined = self.strings.join(" "); 164 | 165 | cancel.check_cancel()?; 166 | if self.enable_escapes { 167 | let escaped = echo_escapes(&joined); 168 | cancel.check_cancel()?; 169 | write!(io_env.stdout(), "{}", escaped)?; 170 | } else { 171 | write!(io_env.stdout(), "{}", joined)?; 172 | } 173 | 174 | if !self.no_newline { 175 | writeln!(io_env.stdout(), "")?; 176 | } 177 | Ok(Status::Complete(0.into()).into()) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/builtins/env.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use cancel::Token; 4 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 5 | use std::collections::HashSet; 6 | use std::io::Write; 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | use structopt::*; 10 | 11 | #[derive(StructOpt)] 12 | /// Set the export attribute for variables (note that this is always 13 | /// set in the current version of wzsh) 14 | pub struct ExportCommand { 15 | names: Vec, 16 | /// Print exported variables in a syntax compatible with the shell 17 | #[structopt(short = "p", conflicts_with = "names")] 18 | print: bool, 19 | } 20 | 21 | impl Builtin for ExportCommand { 22 | fn name() -> &'static str { 23 | "export" 24 | } 25 | 26 | fn run( 27 | &mut self, 28 | environment: &mut Environment, 29 | _current_directory: &mut PathBuf, 30 | io_env: &IoEnvironment, 31 | cancel: Arc, 32 | _functions: &Arc, 33 | ) -> anyhow::Result { 34 | if self.print { 35 | for (k, v) in environment.iter() { 36 | cancel.check_cancel()?; 37 | match (k.to_str(), v.to_str()) { 38 | (Some(k), Some(v)) => writeln!(io_env.stdout(), "export {}={}", k, v)?, 39 | _ => writeln!( 40 | io_env.stderr(), 41 | "{:?}={:?} cannot be formatted as utf-8", 42 | k, 43 | v 44 | )?, 45 | } 46 | } 47 | } else { 48 | for name in &self.names { 49 | // parse `name=value` and assign 50 | let split: Vec<&str> = name.splitn(2, '=').collect(); 51 | if split.len() == 2 { 52 | environment.set(split[0], split[1]); 53 | } 54 | } 55 | } 56 | Ok(Status::Complete(0.into()).into()) 57 | } 58 | } 59 | 60 | #[derive(StructOpt)] 61 | /// Unset a variable from the environment 62 | pub struct UnsetCommand { 63 | names: Vec, 64 | } 65 | 66 | impl Builtin for UnsetCommand { 67 | fn name() -> &'static str { 68 | "unset" 69 | } 70 | 71 | fn run( 72 | &mut self, 73 | environment: &mut Environment, 74 | _current_directory: &mut PathBuf, 75 | _io_env: &IoEnvironment, 76 | _cancel: Arc, 77 | _functions: &Arc, 78 | ) -> anyhow::Result { 79 | for name in &self.names { 80 | environment.unset(name); 81 | } 82 | Ok(Status::Complete(0.into()).into()) 83 | } 84 | } 85 | 86 | #[derive(StructOpt)] 87 | #[structopt(rename_all = "kebab")] 88 | pub struct PathSpec { 89 | dirs: Vec, 90 | /// If set, remove duplicate path entries 91 | #[structopt(long)] 92 | dedup: bool, 93 | /// If set, don't add an entry that isn't a directory. 94 | /// This allow for speculatively adding a list of paths 95 | /// and not cluttering up the path list. 96 | #[structopt(long)] 97 | only_existing: bool, 98 | } 99 | 100 | enum PathOp { 101 | Append, 102 | Prepend, 103 | Remove, 104 | } 105 | 106 | impl PathSpec { 107 | fn apply(&self, env: &mut Environment, op: PathOp) -> anyhow::Result<()> { 108 | let mut set = HashSet::new(); 109 | let mut pathvec = Vec::new(); 110 | 111 | if let Some(path) = env.get("PATH") { 112 | for entry in std::env::split_paths(path) { 113 | if !self.dedup || !set.contains(&entry) { 114 | pathvec.push(entry.clone()); 115 | set.insert(entry); 116 | } 117 | } 118 | } 119 | 120 | for entry in &self.dirs { 121 | match op { 122 | PathOp::Append => { 123 | if !self.dedup || !set.contains(entry) { 124 | let entry = entry.to_path_buf(); 125 | set.insert(entry.clone()); 126 | if !self.only_existing || entry.is_dir() { 127 | pathvec.push(entry); 128 | } 129 | } 130 | } 131 | PathOp::Prepend => { 132 | // We want to ensure that we move this to the 133 | // front of the path, so take it out now 134 | if self.dedup && set.contains(entry) { 135 | set.remove(entry); 136 | pathvec.retain(|p| p != entry); 137 | } 138 | 139 | if !self.dedup || !set.contains(entry) { 140 | let entry = entry.to_path_buf(); 141 | set.insert(entry.clone()); 142 | if !self.only_existing || entry.is_dir() { 143 | pathvec.insert(0, entry); 144 | } 145 | } 146 | } 147 | PathOp::Remove => { 148 | pathvec.retain(|p| p != entry); 149 | } 150 | } 151 | } 152 | 153 | let new_path = std::env::join_paths(pathvec.into_iter())?; 154 | env.set("PATH", new_path); 155 | Ok(()) 156 | } 157 | } 158 | 159 | #[derive(StructOpt)] 160 | #[structopt(rename_all = "kebab")] 161 | /// Ever felt like a caveman when it comes to updating your PATH? 162 | /// This builtin command makes path manipulation a bit more civilized 163 | /// by offering append, prepend, removal and fixup operations to keep 164 | /// your path tidy 165 | pub enum PathCommand { 166 | /// Print the path elements 167 | Show, 168 | /// Add to the end of the path 169 | Add(PathSpec), 170 | /// Prepend to the path 171 | Prepend(PathSpec), 172 | /// Remove from the path 173 | Remove(PathSpec), 174 | /// Remove non-existent entries from the path and de-dup 175 | Fixup, 176 | } 177 | 178 | impl Builtin for PathCommand { 179 | fn name() -> &'static str { 180 | "path" 181 | } 182 | 183 | fn run( 184 | &mut self, 185 | env: &mut Environment, 186 | _current_directory: &mut PathBuf, 187 | io_env: &IoEnvironment, 188 | _cancel: Arc, 189 | _functions: &Arc, 190 | ) -> anyhow::Result { 191 | match self { 192 | PathCommand::Show => { 193 | if let Some(path) = env.get("PATH") { 194 | for entry in std::env::split_paths(path) { 195 | writeln!(io_env.stdout(), "{}", entry.display())?; 196 | } 197 | } else { 198 | writeln!(io_env.stderr(), "PATH environment is not set!")?; 199 | return Ok(Status::Complete(1.into()).into()); 200 | } 201 | } 202 | PathCommand::Add(spec) => spec.apply(env, PathOp::Append)?, 203 | PathCommand::Prepend(spec) => spec.apply(env, PathOp::Prepend)?, 204 | PathCommand::Remove(spec) => spec.apply(env, PathOp::Remove)?, 205 | PathCommand::Fixup => { 206 | let mut set = HashSet::new(); 207 | let mut pathvec = Vec::new(); 208 | 209 | if let Some(path) = env.get("PATH") { 210 | for entry in std::env::split_paths(path) { 211 | if !set.contains(&entry) { 212 | let entry = entry.to_path_buf(); 213 | set.insert(entry.clone()); 214 | if entry.is_dir() { 215 | pathvec.push(entry); 216 | } 217 | } 218 | } 219 | } 220 | let new_path = std::env::join_paths(pathvec.into_iter())?; 221 | env.set("PATH", new_path); 222 | } 223 | } 224 | Ok(Status::Complete(0.into()).into()) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/builtins/history.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use anyhow::Context; 4 | use cancel::Token; 5 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 6 | use sqlite::Value; 7 | use std::borrow::Cow; 8 | use std::convert::TryInto; 9 | use std::path::PathBuf; 10 | use std::sync::Arc; 11 | use structopt::*; 12 | use tabout::{tabulate_output, Alignment, Column}; 13 | use termwiz::lineedit::*; 14 | 15 | pub struct SqliteHistory { 16 | connection: sqlite::Connection, 17 | } 18 | 19 | pub struct HistoryEntry { 20 | pub idx: HistoryIndex, 21 | pub cmd: String, 22 | } 23 | 24 | impl SqliteHistory { 25 | pub fn new() -> Self { 26 | let path = dirs_next::home_dir() 27 | .expect("can't find HOME dir") 28 | .join(".wzsh-history.db"); 29 | let connection = sqlite::open(&path) 30 | .with_context(|| format!("initializing history file {}", path.display())) 31 | .unwrap(); 32 | connection 33 | .execute("CREATE TABLE if not exists history (cmd TEXT, ts INTEGER)") 34 | .unwrap(); 35 | Self { connection } 36 | } 37 | 38 | pub fn get_last_n_entries(&self, n: usize) -> Vec { 39 | let mut cursor = self 40 | .connection 41 | .prepare("select rowid, cmd from history order by rowid desc limit ?") 42 | .unwrap() 43 | .cursor(); 44 | cursor 45 | .bind(&[Value::Integer(n.try_into().unwrap())]) 46 | .unwrap(); 47 | 48 | let mut res = vec![]; 49 | while let Ok(Some(row)) = cursor.next() { 50 | res.push(HistoryEntry { 51 | idx: row[0].as_integer().unwrap().try_into().unwrap(), 52 | cmd: row[1].as_string().unwrap().to_string(), 53 | }); 54 | } 55 | res.reverse(); 56 | res 57 | } 58 | } 59 | 60 | impl History for SqliteHistory { 61 | fn get(&self, idx: HistoryIndex) -> Option> { 62 | let mut cursor = self 63 | .connection 64 | .prepare("select cmd from history where rowid=?") 65 | .unwrap() 66 | .cursor(); 67 | cursor 68 | .bind(&[Value::Integer(idx.try_into().unwrap())]) 69 | .unwrap(); 70 | if let Some(row) = cursor.next().unwrap() { 71 | Some(Cow::Owned(row[0].as_string().unwrap().to_string())) 72 | } else { 73 | None 74 | } 75 | } 76 | 77 | fn last(&self) -> Option { 78 | let mut cursor = self 79 | .connection 80 | .prepare("select rowid from history order by rowid desc limit 1") 81 | .unwrap() 82 | .cursor(); 83 | if let Some(row) = cursor.next().unwrap() { 84 | Some(row[0].as_integer().unwrap().try_into().unwrap()) 85 | } else { 86 | None 87 | } 88 | } 89 | 90 | fn add(&mut self, line: &str) { 91 | if let Some(last_idx) = self.last() { 92 | if let Some(last_line) = self.get(last_idx) { 93 | if last_line == line { 94 | // Ignore duplicates 95 | return; 96 | } 97 | } 98 | } 99 | 100 | let mut cursor = self 101 | .connection 102 | .prepare("insert into history values (?, strftime('%s','now'))") 103 | .unwrap() 104 | .cursor(); 105 | cursor.bind(&[Value::String(line.to_string())]).unwrap(); 106 | cursor.next().ok(); 107 | } 108 | 109 | fn search( 110 | &self, 111 | idx: HistoryIndex, 112 | style: SearchStyle, 113 | direction: SearchDirection, 114 | pattern: &str, 115 | ) -> Option { 116 | let query = match (style, direction) { 117 | (SearchStyle::Substring, SearchDirection::Backwards) => { 118 | "select rowid, cmd from history where rowid <= ? and cmd like ? order by rowid desc limit 1" 119 | } 120 | (SearchStyle::Substring, SearchDirection::Forwards) => { 121 | "select rowid, cmd from history where rowid >= ? and cmd like ? order by rowid limit 1" 122 | } 123 | }; 124 | 125 | let mut cursor = self.connection.prepare(query).unwrap().cursor(); 126 | let params = &[ 127 | Value::Integer(idx.try_into().unwrap()), 128 | Value::String(format!("%{}%", pattern)), 129 | ]; 130 | // print!("{} {:?}\r\n", query, params); 131 | 132 | cursor.bind(params).unwrap(); 133 | if let Some(Some(row)) = cursor.next().ok() { 134 | // print!("row: {:?}\r\n\r\n", row); 135 | let line = Cow::Owned(row[1].as_string().unwrap().to_string()); 136 | let idx = row[0].as_integer().unwrap(); 137 | if let Some(cursor) = style.match_against(pattern, &line) { 138 | Some(SearchResult { 139 | line, 140 | idx: idx.try_into().unwrap(), 141 | cursor, 142 | }) 143 | } else { 144 | None 145 | } 146 | } else { 147 | None 148 | } 149 | } 150 | } 151 | 152 | #[derive(StructOpt)] 153 | #[structopt(rename_all = "kebab")] 154 | pub enum HistoryCommand { 155 | Recent { 156 | #[structopt(default_value = "16")] 157 | num_entries: usize, 158 | }, 159 | } 160 | 161 | impl Builtin for HistoryCommand { 162 | fn name() -> &'static str { 163 | "history" 164 | } 165 | 166 | fn run( 167 | &mut self, 168 | _environment: &mut Environment, 169 | _current_directory: &mut PathBuf, 170 | io_env: &IoEnvironment, 171 | _cancel: Arc, 172 | _functions: &Arc, 173 | ) -> anyhow::Result { 174 | match self { 175 | Self::Recent { num_entries } => { 176 | let history = SqliteHistory::new(); 177 | let entries = history.get_last_n_entries(*num_entries); 178 | 179 | let columns = [ 180 | Column { 181 | name: "IDX".to_string(), 182 | alignment: Alignment::Right, 183 | }, 184 | Column { 185 | name: "CMD".to_string(), 186 | alignment: Alignment::Left, 187 | }, 188 | ]; 189 | 190 | let rows: Vec<_> = entries 191 | .into_iter() 192 | .map(|entry| vec![entry.idx.to_string(), entry.cmd]) 193 | .collect(); 194 | 195 | tabulate_output(&columns, &rows, &mut io_env.stdout())?; 196 | } 197 | } 198 | Ok(Status::Complete(0.into()).into()) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/builtins/jobcontrol.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::job::JOB_LIST; 3 | use crate::shellhost::FunctionRegistry; 4 | use anyhow::anyhow; 5 | use cancel::Token; 6 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 7 | use std::io::Write; 8 | use std::path::PathBuf; 9 | use std::sync::Arc; 10 | use structopt::*; 11 | 12 | #[derive(Debug, StructOpt)] 13 | /// Place a background job into the foreground 14 | pub struct FgCommand {} 15 | impl Builtin for FgCommand { 16 | fn name() -> &'static str { 17 | "fg" 18 | } 19 | 20 | fn run( 21 | &mut self, 22 | _environment: &mut Environment, 23 | _current_directory: &mut PathBuf, 24 | io_env: &IoEnvironment, 25 | _cancel: Arc, 26 | _functions: &Arc, 27 | ) -> anyhow::Result { 28 | let mut jobs = JOB_LIST.jobs(); 29 | if let Some(mut job) = jobs.pop() { 30 | writeln!( 31 | io_env.stderr(), 32 | "wzsh: putting [{}] {} into fg", 33 | job.process_group_id(), 34 | job 35 | )?; 36 | job.put_in_foreground()?; 37 | let status = job 38 | .wait() 39 | .ok_or_else(|| anyhow!("job.wait returned None?"))?; 40 | writeln!( 41 | io_env.stderr(), 42 | "wzsh: after fg, wait returned {:?}", 43 | status 44 | )?; 45 | Ok(Status::Complete(0.into()).into()) 46 | } else { 47 | writeln!(io_env.stderr(), "wzsh: fg: no jobs to put in foreground")?; 48 | Ok(Status::Complete(1.into()).into()) 49 | } 50 | } 51 | } 52 | 53 | #[derive(Debug, StructOpt, Default)] 54 | /// list known jobs 55 | pub struct JobsCommand {} 56 | impl Builtin for JobsCommand { 57 | fn name() -> &'static str { 58 | "jobs" 59 | } 60 | 61 | fn run( 62 | &mut self, 63 | _environment: &mut Environment, 64 | _current_directory: &mut PathBuf, 65 | io_env: &IoEnvironment, 66 | _cancel: Arc, 67 | _functions: &Arc, 68 | ) -> anyhow::Result { 69 | let mut jobs = JOB_LIST.jobs(); 70 | for job in &mut jobs { 71 | match job.poll() { 72 | Some(status) => writeln!( 73 | io_env.stdout(), 74 | "[{}] - {:?} {}", 75 | job.process_group_id(), 76 | status, 77 | job 78 | )?, 79 | None => writeln!( 80 | io_env.stdout(), 81 | "[{}] - {}", // TODO: be smarter about stopped status 82 | job.process_group_id(), 83 | job 84 | )?, 85 | } 86 | } 87 | Ok(Status::Complete(0.into()).into()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/builtins/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::shellhost::FunctionRegistry; 2 | use anyhow::anyhow; 3 | use cancel::Token; 4 | use lazy_static::lazy_static; 5 | use shell_vm::{Environment, IoEnvironment, Value, WaitableStatus}; 6 | use std::collections::HashMap; 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | use structopt::*; 10 | 11 | mod builtins; 12 | mod colon; 13 | mod echo; 14 | mod env; 15 | pub mod history; 16 | mod jobcontrol; 17 | mod truefalse; 18 | mod which; 19 | mod workingdir; 20 | 21 | /// The `Builtin` trait extends `StructOpt` by adding `name` and `run` 22 | /// methods that allow registering a command with the shell. 23 | pub trait Builtin: StructOpt { 24 | fn eval( 25 | argv: &[Value], 26 | environment: &mut Environment, 27 | current_directory: &mut PathBuf, 28 | io_env: &IoEnvironment, 29 | cancel: Arc, 30 | functions: &Arc, 31 | ) -> anyhow::Result 32 | where 33 | Self: Sized, 34 | { 35 | let app = Self::clap() 36 | .global_setting(structopt::clap::AppSettings::ColoredHelp) 37 | .global_setting(structopt::clap::AppSettings::DisableVersion) 38 | .name(Self::name()); 39 | let mut os_args = vec![]; 40 | for arg in argv { 41 | os_args.push( 42 | arg.as_os_str() 43 | .ok_or_else(|| anyhow!("argument is not representable as osstr"))?, 44 | ); 45 | } 46 | let mut args = Self::from_clap(&app.get_matches_from_safe(os_args.iter())?); 47 | args.run(environment, current_directory, io_env, cancel, functions) 48 | } 49 | 50 | fn name() -> &'static str; 51 | 52 | fn run( 53 | &mut self, 54 | environment: &mut Environment, 55 | current_directory: &mut PathBuf, 56 | io_env: &IoEnvironment, 57 | cancel: Arc, 58 | functions: &Arc, 59 | ) -> anyhow::Result; 60 | } 61 | 62 | pub type BuiltinFunc = fn( 63 | argv: &[Value], 64 | environment: &mut Environment, 65 | current_directory: &mut PathBuf, 66 | io_env: &IoEnvironment, 67 | cancel: Arc, 68 | functions: &Arc, 69 | ) -> anyhow::Result; 70 | 71 | pub fn lookup_builtin(name: &Value) -> Option { 72 | if let Some(s) = name.as_str() { 73 | BUILTINS.get(s).map(|f| *f) 74 | } else { 75 | None 76 | } 77 | } 78 | 79 | lazy_static! { 80 | static ref BUILTINS: HashMap<&'static str, BuiltinFunc> = { 81 | let mut builtins = HashMap::new(); 82 | // This identity helper effectively casts away the per-function 83 | // type information that would otherwise cause a type mismatch 84 | // when populating the hashmap 85 | fn identity(f: BuiltinFunc) -> BuiltinFunc { 86 | f 87 | } 88 | macro_rules! builtins { 89 | ($($CmdType:ty),* $(,)? ) => { 90 | $( 91 | builtins.insert(<$CmdType>::name(), identity(<$CmdType>::eval)); 92 | )* 93 | } 94 | } 95 | 96 | builtins!( 97 | builtins::BuiltinsCommand, 98 | colon::ColonCommand, 99 | echo::EchoCommand, 100 | env::ExportCommand, 101 | env::UnsetCommand, 102 | env::PathCommand, 103 | history::HistoryCommand, 104 | jobcontrol::FgCommand, 105 | jobcontrol::JobsCommand, 106 | truefalse::FalseCommand, 107 | truefalse::TrueCommand, 108 | which::WhichCommand, 109 | workingdir::CdCommand, 110 | workingdir::PwdCommand, 111 | ); 112 | 113 | builtins 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/builtins/truefalse.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use cancel::Token; 4 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 5 | use std::path::PathBuf; 6 | use std::sync::Arc; 7 | use structopt::*; 8 | 9 | #[derive(StructOpt)] 10 | /// The true utility shall return with exit code zero. 11 | pub struct TrueCommand {} 12 | 13 | impl Builtin for TrueCommand { 14 | fn name() -> &'static str { 15 | "true" 16 | } 17 | 18 | fn run( 19 | &mut self, 20 | _environment: &mut Environment, 21 | _current_directory: &mut PathBuf, 22 | _io_env: &IoEnvironment, 23 | _cancel: Arc, 24 | _functions: &Arc, 25 | ) -> anyhow::Result { 26 | Ok(Status::Complete(0.into()).into()) 27 | } 28 | } 29 | 30 | #[derive(StructOpt)] 31 | /// The false utility shall return with a non-zero exit code. 32 | pub struct FalseCommand {} 33 | 34 | impl Builtin for FalseCommand { 35 | fn name() -> &'static str { 36 | "false" 37 | } 38 | 39 | fn run( 40 | &mut self, 41 | _environment: &mut Environment, 42 | _current_directory: &mut PathBuf, 43 | _io_env: &IoEnvironment, 44 | _cancel: Arc, 45 | _functions: &Arc, 46 | ) -> anyhow::Result { 47 | Ok(Status::Complete(1.into()).into()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/builtins/which.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::{lookup_builtin, Builtin}; 2 | use crate::shellhost::FunctionRegistry; 3 | use cancel::Token; 4 | use pathsearch::PathSearcher; 5 | use shell_vm::{Environment, IoEnvironment, Status, Value, WaitableStatus}; 6 | use std::io::Write; 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | use structopt::*; 10 | 11 | #[derive(StructOpt)] 12 | /// Search the path for a command; if found, print out the path 13 | /// to that command. 14 | pub struct WhichCommand { 15 | /// Find all possible matches in the path, rather than stopping 16 | /// at the first one 17 | #[structopt(short = "a")] 18 | all: bool, 19 | 20 | /// Don't output anything; only indicate success/failure through 21 | /// the exit status. 22 | /// This is a non-standard extension for wzsh. 23 | #[structopt(short = "q")] 24 | quiet: bool, 25 | 26 | /// The command to find 27 | command: PathBuf, 28 | } 29 | 30 | impl Builtin for WhichCommand { 31 | fn name() -> &'static str { 32 | "which" 33 | } 34 | 35 | fn run( 36 | &mut self, 37 | environment: &mut Environment, 38 | _current_directory: &mut PathBuf, 39 | io_env: &IoEnvironment, 40 | cancel: Arc, 41 | functions: &Arc, 42 | ) -> anyhow::Result { 43 | let mut found = false; 44 | 45 | if let Some(name) = self.command.to_str() { 46 | if let Some(_) = functions.lookup_function(name) { 47 | found = true; 48 | if !self.quiet { 49 | writeln!(io_env.stdout(), "{}: shell function", name)?; 50 | } 51 | } 52 | } 53 | if let Some(_) = lookup_builtin(&Value::OsString(self.command.as_os_str().to_os_string())) { 54 | found = true; 55 | if !self.quiet { 56 | writeln!( 57 | io_env.stdout(), 58 | "{}: shell built-in command", 59 | self.command.display() 60 | )?; 61 | } 62 | } 63 | if !found || self.all { 64 | for path in PathSearcher::new( 65 | &self.command, 66 | environment.get("PATH"), 67 | environment.get("PATHEXT"), 68 | ) { 69 | cancel.check_cancel()?; 70 | found = true; 71 | if !self.quiet { 72 | writeln!(io_env.stdout(), "{}", path.display())?; 73 | } 74 | if !self.all { 75 | break; 76 | } 77 | } 78 | } 79 | Ok(Status::Complete(if found { 0 } else { 1 }.into()).into()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/builtins/workingdir.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::Builtin; 2 | use crate::shellhost::FunctionRegistry; 3 | use anyhow::anyhow; 4 | use cancel::Token; 5 | use shell_vm::{Environment, IoEnvironment, Status, WaitableStatus}; 6 | use std::io::Write; 7 | use std::path::{Component, Path, PathBuf}; 8 | use std::sync::Arc; 9 | use structopt::*; 10 | 11 | #[derive(Debug, StructOpt)] 12 | /// The pwd utility writes the absolute pathname of the current working directory to the standard output. 13 | pub struct PwdCommand { 14 | /// Display the logical current working directory. 15 | /// This is the default behavior. 16 | #[structopt(short = "L", overrides_with = "physical")] 17 | logical: bool, 18 | 19 | /// Display the physical current working directory (all symbolic links resolved). 20 | #[structopt(short = "P", overrides_with = "logical")] 21 | physical: bool, 22 | } 23 | 24 | impl Builtin for PwdCommand { 25 | fn name() -> &'static str { 26 | "pwd" 27 | } 28 | 29 | fn run( 30 | &mut self, 31 | _environment: &mut Environment, 32 | current_directory: &mut PathBuf, 33 | io_env: &IoEnvironment, 34 | _cancel: Arc, 35 | _functions: &Arc, 36 | ) -> anyhow::Result { 37 | let pwd = if self.physical { 38 | current_directory.canonicalize()? 39 | } else { 40 | current_directory.clone() 41 | }; 42 | writeln!(io_env.stdout(), "{}", pwd.display())?; 43 | Ok(Status::Complete(0.into()).into()) 44 | } 45 | } 46 | 47 | // https://pubs.opengroup.org/onlinepubs/009695399/utilities/cd.html 48 | 49 | #[derive(Debug, StructOpt)] 50 | /// The cd utility changes the working directory of the current shell environment. 51 | pub struct CdCommand { 52 | /// Handle the operand dot-dot logically; symbolic link components shall not be resolved before dot-dot components are processed 53 | #[structopt(short = "L", overrides_with = "physical")] 54 | logical: bool, 55 | 56 | /// Handle the operand dot-dot physically; symbolic link components shall be resolved before dot-dot components are processed 57 | #[structopt(short = "P", overrides_with = "logical")] 58 | physical: bool, 59 | 60 | /// The destination directory 61 | directory: Option, 62 | } 63 | 64 | /// Normalize the path so that redundant components such as `.` 65 | /// and `..` are appropriately removed. This normalization does 66 | /// not look at the filesystem; it is a logical rather than 67 | /// canonical normalization that is unaware of symlinks. 68 | fn normalize_path>(p: P) -> PathBuf { 69 | let mut normalized = PathBuf::new(); 70 | for component in p.as_ref().components() { 71 | match component { 72 | Component::RootDir => { 73 | normalized = PathBuf::new(); 74 | normalized.push("/"); 75 | } 76 | Component::Prefix(prefix) => { 77 | normalized = PathBuf::new(); 78 | normalized.push(prefix.as_os_str()); 79 | } 80 | Component::CurDir => {} 81 | Component::ParentDir => { 82 | normalized.pop(); 83 | } 84 | Component::Normal(s) => { 85 | normalized.push(s); 86 | } 87 | } 88 | } 89 | normalized 90 | } 91 | 92 | fn canonicalize_path>(p: P, physical: bool) -> anyhow::Result { 93 | if physical { 94 | p.as_ref().canonicalize().map_err(|e| e.into()) 95 | } else { 96 | Ok(normalize_path(p)) 97 | } 98 | } 99 | 100 | impl Builtin for CdCommand { 101 | fn name() -> &'static str { 102 | "cd" 103 | } 104 | 105 | fn run( 106 | &mut self, 107 | environment: &mut Environment, 108 | current_directory: &mut PathBuf, 109 | io_env: &IoEnvironment, 110 | _cancel: Arc, 111 | _functions: &Arc, 112 | ) -> anyhow::Result { 113 | let directory = match self.directory.take() { 114 | Some(dir) => dir, 115 | None => { 116 | if let Some(home) = environment.get("HOME") { 117 | PathBuf::from(home) 118 | } else { 119 | writeln!(io_env.stderr(), "$HOME is not set")?; 120 | return Ok(Status::Complete(1.into()).into()); 121 | } 122 | } 123 | }; 124 | 125 | let mut print = false; 126 | 127 | let curpath = if directory.is_absolute() { 128 | directory 129 | } else if directory == Path::new("-") { 130 | print = true; 131 | PathBuf::from( 132 | environment 133 | .get("OLDPWD") 134 | .ok_or_else(|| anyhow!("OLDPWD is not set"))?, 135 | ) 136 | } else { 137 | current_directory.join(directory) 138 | }; 139 | 140 | let cwd = canonicalize_path(curpath, self.physical)?; 141 | if !cwd.is_dir() { 142 | writeln!( 143 | io_env.stderr(), 144 | "wzsh: cd: {} is not a directory", 145 | cwd.display() 146 | )?; 147 | return Ok(Status::Complete(1.into()).into()); 148 | } 149 | 150 | *current_directory = cwd.clone(); 151 | if print { 152 | writeln!(io_env.stdout(), "{}", cwd.display())?; 153 | } 154 | return Ok(Status::Complete(0.into()).into()); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/errorprint.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use shell_lexer::{LexError, Span}; 3 | use shell_parser::ParseErrorKind; 4 | use std::io::Read; 5 | use std::path::Path; 6 | 7 | fn extract_error_range(e: &Error) -> Option { 8 | if let Some(lex_err) = e.downcast_ref::() { 9 | Some(lex_err.span) 10 | } else if let Some(parse_err) = e.downcast_ref::() { 11 | match parse_err { 12 | ParseErrorKind::UnexpectedToken(token, ..) => Some(token.span()), 13 | } 14 | } else { 15 | None 16 | } 17 | } 18 | 19 | pub fn print_error_path(e: &Error, path: &Path) { 20 | let mut file = match std::fs::File::open(path) { 21 | Ok(file) => file, 22 | Err(err) => { 23 | eprintln!("wzsh: {}: while opening: {}", path.display(), err); 24 | return; 25 | } 26 | }; 27 | let mut input = String::new(); 28 | if let Err(err) = file.read_to_string(&mut input) { 29 | eprintln!("wzsh: {}: while reading: {}", path.display(), err); 30 | return; 31 | } 32 | 33 | eprintln!("wzsh: {}: error:", path.display()); 34 | print_error(e, &input); 35 | } 36 | 37 | pub fn print_error(e: &Error, input: &str) { 38 | for item in e.chain() { 39 | eprintln!("wzsh: {}", item); 40 | } 41 | if let Some(span) = extract_error_range(e) { 42 | let lines: Vec<&str> = input.split('\n').collect(); 43 | 44 | let start_line = &lines[span.start.line]; 45 | let end_line = &lines[span.end.line]; 46 | 47 | let mut indicator = String::new(); 48 | let end_col = if span.start.line == span.end.line { 49 | span.end.col 50 | } else { 51 | start_line.len() 52 | }; 53 | 54 | for _ in 0..span.start.col { 55 | indicator.push(' '); 56 | } 57 | 58 | indicator.push_str("\x1b[1m"); 59 | for _ in span.start.col..=end_col { 60 | indicator.push('^'); 61 | } 62 | indicator.push_str("\x1b[0m"); 63 | 64 | eprintln!("{}", start_line); 65 | eprintln!("{}", indicator); 66 | 67 | if span.end.line != span.start.line { 68 | indicator.clear(); 69 | indicator.push_str("\x1b[1m"); 70 | for _ in 0..=span.end.col { 71 | indicator.push('^'); 72 | } 73 | indicator.push_str("\x1b[0m"); 74 | eprintln!("{}", end_line); 75 | eprintln!("{}", indicator); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/exitstatus.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | use filedescriptor::OwnedHandle; 3 | use shell_vm::{Status, WaitForStatus}; 4 | use std::borrow::Cow; 5 | #[cfg(windows)] 6 | use std::os::windows::io::AsRawHandle; 7 | use std::sync::{Arc, Mutex}; 8 | 9 | #[cfg(unix)] 10 | pub type Pid = libc::pid_t; 11 | #[cfg(windows)] 12 | pub type Pid = u32; 13 | 14 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 15 | pub enum ExitStatus { 16 | Running, 17 | Stopped, 18 | ExitCode(i32), 19 | Signalled(i32), 20 | } 21 | 22 | impl ExitStatus { 23 | pub fn terminated(&self) -> bool { 24 | match self { 25 | ExitStatus::ExitCode(_) | ExitStatus::Signalled(_) => true, 26 | ExitStatus::Stopped | ExitStatus::Running => false, 27 | } 28 | } 29 | } 30 | 31 | impl From for ExitStatus { 32 | fn from(status: std::process::ExitStatus) -> ExitStatus { 33 | if let Some(code) = status.code() { 34 | ExitStatus::ExitCode(code) 35 | } else if status.success() { 36 | ExitStatus::ExitCode(0) 37 | } else { 38 | ExitStatus::ExitCode(1) 39 | } 40 | } 41 | } 42 | 43 | impl From for Status { 44 | fn from(status: ExitStatus) -> Status { 45 | match status { 46 | ExitStatus::Running => Status::Running, 47 | ExitStatus::Stopped => Status::Stopped, 48 | ExitStatus::ExitCode(n) => Status::Complete((n as isize).into()), 49 | ExitStatus::Signalled(n) => Status::Complete((128 + n as isize).into()), 50 | } 51 | } 52 | } 53 | 54 | // libc doesn't provide an NSIG value, so we guess; this 55 | // value is likely larger than reality but it is safe because 56 | // we only use signal numbers produced from a process exit 57 | // status to index into the sys_signame and sys_siglist 58 | // globals, and those are therefore valid signal offsets. 59 | #[cfg(unix)] 60 | const NSIG: usize = 1024; 61 | #[cfg(unix)] 62 | extern "C" { 63 | static sys_signame: [*const i8; NSIG]; 64 | static sys_siglist: [*const i8; NSIG]; 65 | } 66 | 67 | fn signame(n: i32) -> Cow<'static, str> { 68 | #[cfg(unix)] 69 | unsafe { 70 | let c_str = sys_signame[n as usize]; 71 | std::ffi::CStr::from_ptr(c_str).to_string_lossy() 72 | } 73 | #[cfg(windows)] 74 | unreachable!(n); 75 | } 76 | 77 | fn sigdesc(n: i32) -> Cow<'static, str> { 78 | #[cfg(unix)] 79 | unsafe { 80 | let c_str = sys_siglist[n as usize]; 81 | std::ffi::CStr::from_ptr(c_str).to_string_lossy() 82 | } 83 | #[cfg(windows)] 84 | unreachable!(n); 85 | } 86 | 87 | impl std::fmt::Display for ExitStatus { 88 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 89 | match self { 90 | ExitStatus::Running => write!(fmt, "running"), 91 | ExitStatus::Stopped => write!(fmt, "stopped"), 92 | ExitStatus::ExitCode(n) => write!(fmt, "exit code {}", n), 93 | ExitStatus::Signalled(n) => { 94 | let name = signame(*n); 95 | let desc = sigdesc(*n); 96 | write!(fmt, "signal {} sig{}: {}", n, name, desc) 97 | } 98 | } 99 | } 100 | } 101 | 102 | #[derive(Debug)] 103 | struct ChildProcessInner { 104 | pid: Pid, 105 | last_status: ExitStatus, 106 | #[cfg(windows)] 107 | process: OwnedHandle, 108 | } 109 | 110 | impl ChildProcessInner { 111 | fn wait(&mut self, blocking: bool) -> Option { 112 | if self.last_status.terminated() { 113 | return Some(self.last_status); 114 | } 115 | #[cfg(unix)] 116 | unsafe { 117 | let mut status = 0i32; 118 | let res = libc::waitpid( 119 | self.pid, 120 | &mut status, 121 | libc::WUNTRACED | if blocking { 0 } else { libc::WNOHANG }, 122 | ); 123 | if res != self.pid { 124 | if !blocking { 125 | return Some(self.last_status); 126 | } 127 | let err = std::io::Error::last_os_error(); 128 | eprintln!("error waiting for child pid {} {}", self.pid, err); 129 | 130 | let status = ExitStatus::ExitCode(1); 131 | self.last_status = status; 132 | return Some(status); 133 | } 134 | 135 | let status = if libc::WIFSTOPPED(status) { 136 | ExitStatus::Stopped 137 | } else if libc::WIFSIGNALED(status) { 138 | ExitStatus::Signalled(libc::WTERMSIG(status)) 139 | } else if libc::WIFEXITED(status) { 140 | ExitStatus::ExitCode(libc::WEXITSTATUS(status)) 141 | } else { 142 | ExitStatus::Running 143 | }; 144 | 145 | self.last_status = status; 146 | Some(status) 147 | } 148 | #[cfg(windows)] 149 | { 150 | use winapi::shared::winerror::WAIT_TIMEOUT; 151 | use winapi::um::processthreadsapi::GetExitCodeProcess; 152 | use winapi::um::synchapi::*; 153 | use winapi::um::winbase::{INFINITE, WAIT_OBJECT_0}; 154 | if blocking { 155 | let res = unsafe { WaitForSingleObject(self.process.as_raw_handle(), INFINITE) }; 156 | match res { 157 | WAIT_OBJECT_0 => {} 158 | WAIT_TIMEOUT => return None, 159 | _ => { 160 | let err = std::io::Error::last_os_error(); 161 | eprintln!("error waiting for child pid {} {}", self.pid, err); 162 | let status = ExitStatus::ExitCode(1); 163 | self.last_status = status; 164 | return Some(status); 165 | } 166 | }; 167 | } 168 | 169 | let mut exit_code: winapi::shared::minwindef::DWORD = 0; 170 | let status = 171 | if unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &mut exit_code) } != 0 172 | { 173 | ExitStatus::ExitCode(exit_code as i32) 174 | } else { 175 | let err = std::io::Error::last_os_error(); 176 | eprintln!("error getting exit code for child pid {} {}", self.pid, err); 177 | ExitStatus::ExitCode(1) 178 | }; 179 | self.last_status = status; 180 | Some(status) 181 | } 182 | } 183 | } 184 | 185 | #[derive(Debug, Clone)] 186 | pub struct ChildProcess { 187 | inner: Arc>, 188 | } 189 | 190 | impl ChildProcess { 191 | pub fn pid(&self) -> Pid { 192 | self.inner.lock().unwrap().pid 193 | } 194 | 195 | pub fn new(child: std::process::Child) -> Self { 196 | let pid = child.id() as _; 197 | Self { 198 | inner: Arc::new(Mutex::new(ChildProcessInner { 199 | pid, 200 | last_status: ExitStatus::Running, 201 | #[cfg(windows)] 202 | process: OwnedHandle::new(child), 203 | })), 204 | } 205 | } 206 | 207 | fn wait(&self, blocking: bool) -> Option { 208 | self.inner.lock().unwrap().wait(blocking) 209 | } 210 | } 211 | 212 | impl WaitForStatus for ChildProcess { 213 | fn wait(&self) -> Option { 214 | ChildProcess::wait(self, true).map(Into::into) 215 | } 216 | fn poll(&self) -> Option { 217 | ChildProcess::wait(self, false).map(Into::into) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/job.rs: -------------------------------------------------------------------------------- 1 | use crate::exitstatus::{ChildProcess, Pid}; 2 | use lazy_static::lazy_static; 3 | use shell_vm::{Status, WaitForStatus}; 4 | use std::collections::HashMap; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | lazy_static! { 8 | pub static ref JOB_LIST: JobList = JobList::default(); 9 | } 10 | 11 | pub fn put_shell_in_foreground() { 12 | #[cfg(unix)] 13 | unsafe { 14 | let pgrp = libc::getpgid(libc::getpid()); 15 | libc::tcsetpgrp(0, pgrp); 16 | } 17 | } 18 | 19 | #[cfg(unix)] 20 | pub fn make_own_process_group(pid: i32) { 21 | unsafe { 22 | // Put the process into its own process group 23 | libc::setpgid(pid, pid); 24 | } 25 | } 26 | 27 | #[cfg(unix)] 28 | pub fn add_to_process_group(pid: i32, process_group_id: i32) { 29 | unsafe { 30 | libc::setpgid(pid, process_group_id); 31 | } 32 | } 33 | 34 | #[cfg(unix)] 35 | pub fn make_foreground_process_group(pid: i32) { 36 | make_own_process_group(pid); 37 | unsafe { 38 | // Grant that process group foreground control 39 | // over the terminal 40 | let pty_fd = 0; 41 | libc::tcsetpgrp(pty_fd, pid); 42 | } 43 | } 44 | 45 | #[cfg(unix)] 46 | fn send_cont(pid: libc::pid_t) -> anyhow::Result<()> { 47 | unsafe { 48 | use anyhow::Context; 49 | if libc::kill(pid, libc::SIGCONT) != 0 { 50 | let err = std::io::Error::last_os_error(); 51 | Err(err).with_context(|| format!("SIGCONT pid {}", pid)) 52 | } else { 53 | Ok(()) 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug)] 59 | struct Inner { 60 | processes: Vec, 61 | process_group_id: Pid, 62 | label: String, 63 | } 64 | 65 | #[derive(Clone, Debug)] 66 | pub struct Job { 67 | inner: Arc>, 68 | } 69 | 70 | #[derive(Default, Debug)] 71 | pub struct JobList { 72 | pub jobs: Mutex>, 73 | } 74 | 75 | impl std::fmt::Display for Job { 76 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 77 | let inner = self.inner.lock().unwrap(); 78 | write!(fmt, "{}", inner.label) 79 | } 80 | } 81 | 82 | impl Job { 83 | pub fn new_empty(label: String) -> Self { 84 | Self { 85 | inner: Arc::new(Mutex::new(Inner { 86 | processes: vec![], 87 | process_group_id: 0, 88 | label, 89 | })), 90 | } 91 | } 92 | 93 | pub fn add(&mut self, proc: ChildProcess) -> anyhow::Result<()> { 94 | let process_group_id = proc.pid(); 95 | 96 | let mut inner = self.inner.lock().unwrap(); 97 | if inner.process_group_id == 0 { 98 | inner.process_group_id = process_group_id; 99 | } 100 | 101 | inner.processes.push(proc); 102 | Ok(()) 103 | } 104 | 105 | #[allow(unused)] 106 | pub fn is_background(&self) -> bool { 107 | #[cfg(unix)] 108 | { 109 | let inner = self.inner.lock().unwrap(); 110 | let pgrp = unsafe { libc::tcgetpgrp(0) }; 111 | inner.process_group_id != pgrp 112 | } 113 | #[cfg(windows)] 114 | false 115 | } 116 | 117 | pub fn process_group_id(&self) -> i32 { 118 | #[cfg(unix)] 119 | { 120 | let inner = self.inner.lock().unwrap(); 121 | inner.process_group_id 122 | } 123 | #[cfg(windows)] 124 | 0 125 | } 126 | 127 | #[allow(unused)] 128 | pub fn put_in_background(&mut self) -> anyhow::Result<()> { 129 | #[cfg(unix)] 130 | { 131 | let inner = self.inner.lock().unwrap(); 132 | send_cont(-inner.process_group_id).ok(); 133 | } 134 | Ok(()) 135 | } 136 | 137 | pub fn put_in_foreground(&mut self) -> anyhow::Result<()> { 138 | #[cfg(unix)] 139 | { 140 | let inner = self.inner.lock().unwrap(); 141 | if inner.process_group_id == 0 { 142 | return Ok(()); 143 | } 144 | unsafe { 145 | let pty_fd = 0; 146 | libc::tcsetpgrp(pty_fd, inner.process_group_id) 147 | }; 148 | send_cont(-inner.process_group_id).ok(); 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | pub fn wait(&mut self) -> Option { 155 | let mut inner = self.inner.lock().unwrap(); 156 | inner.processes.last_mut().unwrap().wait() 157 | } 158 | pub fn poll(&mut self) -> Option { 159 | let mut inner = self.inner.lock().unwrap(); 160 | inner.processes.last_mut().unwrap().poll() 161 | } 162 | } 163 | 164 | impl JobList { 165 | pub fn add(&self, job: Job) -> Job { 166 | let id = job.process_group_id(); 167 | let mut jobs = self.jobs.lock().unwrap(); 168 | jobs.insert(id, job.clone()); 169 | job 170 | } 171 | 172 | pub fn jobs(&self) -> Vec { 173 | let jobs = self.jobs.lock().unwrap(); 174 | jobs.iter().map(|(_, v)| v.clone()).collect() 175 | } 176 | 177 | pub fn check_and_print_status(&self) { 178 | let mut jobs = self.jobs.lock().unwrap(); 179 | let mut terminated = vec![]; 180 | for (id, job) in jobs.iter_mut() { 181 | if let Some(Status::Complete(_status)) = job.poll() { 182 | /* FIXME: only print if it wasn't the most recent fg command 183 | if job.is_background() { 184 | eprintln!("[{}] - {} {}", id, status, job); 185 | } 186 | */ 187 | terminated.push(*id); 188 | } 189 | } 190 | 191 | for id in terminated { 192 | jobs.remove(&id); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::errorprint::{print_error, print_error_path}; 2 | use crate::shellhost::FunctionRegistry; 3 | use shell_vm::Environment; 4 | use std::io::Read; 5 | use std::path::PathBuf; 6 | use std::sync::Arc; 7 | use structopt::StructOpt; 8 | 9 | mod builtins; 10 | mod errorprint; 11 | mod exitstatus; 12 | mod job; 13 | mod repl; 14 | mod script; 15 | mod shellhost; 16 | 17 | #[derive(Debug, StructOpt)] 18 | #[structopt(about = "Wez's Shell\nhttp://github.com/wez/wzsh")] 19 | #[structopt(raw( 20 | global_setting = "structopt::clap::AppSettings::ColoredHelp", 21 | version = r#"env!("VERGEN_SEMVER_LIGHTWEIGHT")"#, 22 | ))] 23 | struct Opt { 24 | /// Skip loading startup.wzsh 25 | #[structopt(long = "no-startup")] 26 | skip_startup: bool, 27 | 28 | /// Instead of starting the interactive REPL, load script 29 | /// from file and execute it 30 | file: Option, 31 | } 32 | 33 | fn config_dir() -> PathBuf { 34 | dirs_next::home_dir() 35 | .expect("can't find HOME dir") 36 | .join(".config") 37 | .join("wzsh") 38 | } 39 | 40 | fn main() -> anyhow::Result<()> { 41 | let mut cwd = std::env::current_dir()?; 42 | let mut env = Environment::new(); 43 | let mut exe_dir = None; 44 | 45 | // We want to pick up our shell utility executables. 46 | // In the source tree they are emitted alongside the wzsh 47 | // executable. In a deployed package they will also be 48 | // located alongside the executable, so resolve the executable 49 | // path and add it to the current environment. 50 | if let Ok(exe) = std::env::current_exe() { 51 | if let Some(bindir) = exe.parent() { 52 | // Allow startup scripts to explicitly reference this 53 | // location so that they can set up aliases if they 54 | // prefer to use these utilities over others that 55 | // might be in their path 56 | env.set("WZSH_BIN_DIR", bindir); 57 | env.append_path("PATH", bindir)?; 58 | 59 | exe_dir.replace(bindir.to_path_buf()); 60 | } 61 | } 62 | 63 | let funcs = Arc::new(FunctionRegistry::new()); 64 | 65 | let opts = Opt::from_args(); 66 | if !opts.skip_startup { 67 | let startup_paths = &[ 68 | #[cfg(windows)] 69 | exe_dir.as_ref().unwrap().join(".wzshrc"), 70 | config_dir().join("startup.wzsh"), 71 | dirs_next::home_dir().unwrap().join(".wzshrc"), 72 | ]; 73 | 74 | for startup_script in startup_paths { 75 | if startup_script.exists() { 76 | if let Err(err) = 77 | script::compile_and_run_script_file(&startup_script, &mut cwd, &mut env, &funcs) 78 | { 79 | print_error_path(&err, &startup_script); 80 | eprintln!("wzsh: ignoring error during startup processing."); 81 | } 82 | break; 83 | } 84 | } 85 | } 86 | 87 | if let Some(file) = opts.file.as_ref() { 88 | if let Err(err) = script::compile_and_run_script_file(file, &mut cwd, &mut env, &funcs) { 89 | print_error_path(&err, file); 90 | std::process::exit(1); 91 | } 92 | Ok(()) 93 | } else if atty::isnt(atty::Stream::Stdin) { 94 | let mut stdin = String::new(); 95 | std::io::stdin().lock().read_to_string(&mut stdin)?; 96 | 97 | if let Err(err) = 98 | script::compile_and_run_script(stdin.as_bytes(), "stdin", &mut cwd, &mut env, &funcs) 99 | { 100 | print_error(&err, &stdin); 101 | std::process::exit(1); 102 | } 103 | Ok(()) 104 | } else { 105 | repl::repl(cwd, env, &funcs) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/repl.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::history::SqliteHistory; 2 | use crate::errorprint::print_error; 3 | use crate::job::{put_shell_in_foreground, Job, JOB_LIST}; 4 | use crate::shellhost::{FunctionRegistry, Host}; 5 | use anyhow::{anyhow, Context, Error}; 6 | use filenamegen::Glob; 7 | use shell_compiler::Compiler; 8 | use shell_lexer::{LexError, LexErrorKind}; 9 | use shell_parser::{ParseErrorKind, Parser}; 10 | use shell_vm::{Environment, Machine, Program, Status}; 11 | use std::path::PathBuf; 12 | use std::sync::Arc; 13 | use termwiz::cell::AttributeChange; 14 | use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; 15 | use termwiz::lineedit::*; 16 | 17 | /// Returns true if a given error might be resolved by allowing 18 | /// the user to continue typing more text on a subsequent line. 19 | /// Most lex errors fall into that category. 20 | fn is_recoverable_parse_error(e: &Error) -> bool { 21 | if let Some(lex_err) = e.downcast_ref::() { 22 | match lex_err.kind { 23 | LexErrorKind::EofDuringBackslash 24 | | LexErrorKind::EofDuringComment 25 | | LexErrorKind::EofDuringSingleQuotedString 26 | | LexErrorKind::EofDuringDoubleQuotedString 27 | | LexErrorKind::EofDuringAssignmentWord 28 | | LexErrorKind::EofDuringCommandSubstitution 29 | | LexErrorKind::EofDuringParameterExpansion => true, 30 | LexErrorKind::IoError => false, 31 | } 32 | } else if let Some(parse_err) = e.downcast_ref::() { 33 | match parse_err { 34 | ParseErrorKind::UnexpectedToken(..) => false, 35 | } 36 | } else { 37 | false 38 | } 39 | } 40 | 41 | #[cfg(unix)] 42 | fn init_job_control() -> anyhow::Result<()> { 43 | let pty_fd = 0; 44 | unsafe { 45 | // Loop until we are in the foreground. 46 | loop { 47 | let pgrp = libc::tcgetpgrp(pty_fd); 48 | let shell_pgid = libc::getpgrp(); 49 | if shell_pgid == pgrp { 50 | break; 51 | } 52 | libc::kill(-shell_pgid, libc::SIGTTIN); 53 | } 54 | 55 | // Ignore interactive and job control signals 56 | for s in &[ 57 | libc::SIGINT, 58 | libc::SIGQUIT, 59 | libc::SIGTSTP, 60 | libc::SIGTTIN, 61 | libc::SIGTTOU, 62 | // libc::SIGCHLD : we need to leave SIGCHLD alone, 63 | // otherwise waitpid returns ECHILD 64 | ] { 65 | libc::signal(*s, libc::SIG_IGN); 66 | } 67 | 68 | // Put ourselves in our own process group 69 | let shell_pgid = libc::getpid(); 70 | if libc::setpgid(shell_pgid, shell_pgid) != 0 { 71 | return Err(std::io::Error::last_os_error()) 72 | .context("unable to put shell into its own process group"); 73 | } 74 | 75 | // Grab control of the terminal 76 | libc::tcsetpgrp(pty_fd, shell_pgid); 77 | 78 | // TODO: tcgetattr to save terminal attributes 79 | } 80 | Ok(()) 81 | } 82 | 83 | struct EnvBits { 84 | cwd: PathBuf, 85 | env: Environment, 86 | funcs: Arc, 87 | } 88 | 89 | fn compile_and_run(prog: &str, env_bits: &mut EnvBits) -> anyhow::Result { 90 | let job = Job::new_empty(prog.to_owned()); 91 | let mut parser = Parser::new(prog.as_bytes()); 92 | let command = parser.parse()?; 93 | let mut compiler = Compiler::new(); 94 | compiler.compile_command(&command)?; 95 | let prog = compiler.finish()?; 96 | let mut machine = Machine::new( 97 | &Program::new(prog), 98 | Some(env_bits.env.clone()), 99 | &env_bits.cwd, 100 | )?; 101 | machine.set_host(Arc::new(Host::with_job_control(job, &env_bits.funcs))); 102 | let status = machine.run(); 103 | 104 | let (cwd, env) = machine.top_environment(); 105 | env_bits.cwd = cwd; 106 | env_bits.env = env; 107 | 108 | status 109 | } 110 | 111 | struct EditHost { 112 | history: SqliteHistory, 113 | cwd: PathBuf, 114 | is_continuation: bool, 115 | } 116 | 117 | impl EditHost { 118 | pub fn new() -> Self { 119 | Self { 120 | history: SqliteHistory::new(), 121 | cwd: Default::default(), 122 | is_continuation: false, 123 | } 124 | } 125 | } 126 | 127 | impl LineEditorHost for EditHost { 128 | fn render_prompt(&self, _prompt: &str) -> Vec { 129 | vec![ 130 | OutputElement::Attribute(AttributeChange::Foreground(AnsiColor::Purple.into())), 131 | OutputElement::Text(self.cwd.display().to_string()), 132 | OutputElement::Text("\r\n".into()), 133 | OutputElement::Attribute(AttributeChange::Foreground( 134 | ColorAttribute::TrueColorWithPaletteFallback( 135 | RgbColor::from_named("skyblue").unwrap(), 136 | AnsiColor::Navy.into(), 137 | ), 138 | )), 139 | OutputElement::Text(if self.is_continuation { 140 | "..> ".to_owned() 141 | } else { 142 | "$ ".to_owned() 143 | }), 144 | ] 145 | } 146 | 147 | fn history(&mut self) -> &mut dyn History { 148 | &mut self.history 149 | } 150 | 151 | fn complete(&self, line: &str, cursor_position: usize) -> Vec { 152 | let mut candidates = vec![]; 153 | if let Some((range, word)) = word_at_cursor(line, cursor_position) { 154 | if let Ok(glob) = Glob::new(&format!("{}*", word)) { 155 | for p in glob.walk(&self.cwd) { 156 | if let Some(text) = p.to_str() { 157 | candidates.push(CompletionCandidate { 158 | range: range.clone(), 159 | // It is convenient when tabbing to complet dirs 160 | // for the directory separator to be included in 161 | // the completion text, so we do that here. 162 | text: format!("{}{}", text, if p.is_dir() { "/" } else { "" }), 163 | }); 164 | } 165 | } 166 | } 167 | } 168 | candidates 169 | } 170 | } 171 | 172 | fn word_at_cursor(line: &str, cursor_position: usize) -> Option<(std::ops::Range, &str)> { 173 | let char_indices: Vec<(usize, char)> = line.char_indices().collect(); 174 | if char_indices.is_empty() { 175 | return None; 176 | } 177 | let char_position = char_indices 178 | .iter() 179 | .position(|(idx, _)| *idx == cursor_position) 180 | .unwrap_or(char_indices.len()); 181 | 182 | // Look back until we find whitespace 183 | let mut start_position = char_position; 184 | while start_position > 0 185 | && start_position <= char_indices.len() 186 | && !char_indices[start_position - 1].1.is_whitespace() 187 | { 188 | start_position -= 1; 189 | } 190 | 191 | // Look forwards until we find whitespace 192 | let mut end_position = char_position; 193 | while end_position < char_indices.len() && !char_indices[end_position].1.is_whitespace() { 194 | end_position += 1; 195 | } 196 | 197 | if end_position > start_position { 198 | let range = char_indices[start_position].0 199 | ..char_indices 200 | .get(end_position) 201 | .map(|c| c.0 + 1) 202 | .unwrap_or(line.len()); 203 | Some((range.clone(), &line[range])) 204 | } else { 205 | None 206 | } 207 | } 208 | 209 | pub fn repl(cwd: PathBuf, env: Environment, funcs: &Arc) -> anyhow::Result<()> { 210 | let mut env = EnvBits { 211 | cwd, 212 | env, 213 | funcs: Arc::clone(funcs), 214 | }; 215 | 216 | #[cfg(unix)] 217 | init_job_control()?; 218 | 219 | let mut terminal = line_editor_terminal()?; 220 | let mut editor = LineEditor::new(&mut terminal); 221 | let mut host = EditHost::new(); 222 | 223 | let mut input = String::new(); 224 | 225 | loop { 226 | // We handle all the prompt rendering in render_prompt. 227 | editor.set_prompt(""); 228 | 229 | JOB_LIST.check_and_print_status(); 230 | 231 | host.cwd = env.cwd.clone(); 232 | host.is_continuation = !input.is_empty(); 233 | 234 | match editor.read_line(&mut host) { 235 | Ok(Some(line)) => { 236 | host.history().add(&line); 237 | 238 | input.push_str(&line); 239 | 240 | let _status = match compile_and_run(&input, &mut env) { 241 | Err(e) => { 242 | if !is_recoverable_parse_error(&e) { 243 | print_error(&e, &input); 244 | input.clear(); 245 | } else { 246 | input.push('\n'); 247 | } 248 | continue; 249 | } 250 | Ok(command) => { 251 | input.clear(); 252 | command 253 | } 254 | }; 255 | 256 | put_shell_in_foreground(); 257 | } 258 | Ok(None) => { 259 | input.clear(); 260 | continue; 261 | } 262 | Err(err) => { 263 | print_error(&anyhow!("during readline: {}", err), ""); 264 | break; 265 | } 266 | } 267 | } 268 | 269 | Ok(()) 270 | } 271 | -------------------------------------------------------------------------------- /src/script.rs: -------------------------------------------------------------------------------- 1 | use crate::job::Job; 2 | use crate::shellhost::{FunctionRegistry, Host}; 3 | use shell_compiler::Compiler; 4 | use shell_parser::Parser; 5 | use shell_vm::{Environment, Machine, Program, Status}; 6 | use std::path::{Path, PathBuf}; 7 | use std::sync::Arc; 8 | 9 | pub fn compile_and_run_script( 10 | file: R, 11 | file_name: &str, 12 | cwd: &mut PathBuf, 13 | env: &mut Environment, 14 | funcs: &Arc, 15 | ) -> anyhow::Result { 16 | let job = Job::new_empty(file_name.to_string()); 17 | let mut parser = Parser::new(file); 18 | 19 | let command = parser.parse()?; 20 | let mut compiler = Compiler::new(); 21 | compiler.compile_command(&command)?; 22 | let prog = compiler.finish()?; 23 | 24 | let mut machine = Machine::new(&Program::new(prog), Some(env.clone()), &cwd)?; 25 | machine.set_host(Arc::new(Host::new(job, funcs))); 26 | let status = machine.run(); 27 | 28 | let (new_cwd, new_env) = machine.top_environment(); 29 | *cwd = new_cwd; 30 | *env = new_env; 31 | 32 | status 33 | } 34 | 35 | pub fn compile_and_run_script_file( 36 | path: &Path, 37 | cwd: &mut PathBuf, 38 | env: &mut Environment, 39 | funcs: &Arc, 40 | ) -> anyhow::Result { 41 | let file_name = path.to_string_lossy(); 42 | let file = std::fs::File::open(path)?; 43 | compile_and_run_script(file, &file_name, cwd, env, funcs) 44 | } 45 | -------------------------------------------------------------------------------- /src/shellhost.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::lookup_builtin; 2 | use crate::exitstatus::ChildProcess; 3 | #[cfg(unix)] 4 | use crate::job::{add_to_process_group, make_foreground_process_group}; 5 | use crate::job::{Job, JOB_LIST}; 6 | use anyhow::{anyhow, bail, Context}; 7 | use cancel::Token; 8 | use pathsearch::PathSearcher; 9 | use shell_vm::{ 10 | Environment, IoEnvironment, Machine, Program, ShellHost, Status, Value, WaitableStatus, 11 | }; 12 | use std::collections::HashMap; 13 | use std::ffi::OsString; 14 | use std::io::Write; 15 | use std::path::PathBuf; 16 | use std::sync::{Arc, Mutex}; 17 | 18 | #[derive(Debug)] 19 | pub struct FunctionRegistry { 20 | functions: Mutex>>, 21 | } 22 | 23 | impl FunctionRegistry { 24 | pub fn new() -> Self { 25 | Self { 26 | functions: Mutex::new(HashMap::new()), 27 | } 28 | } 29 | 30 | pub fn define_function(&self, name: &str, program: &Arc) { 31 | let mut funcs = self.functions.lock().unwrap(); 32 | funcs.insert(name.to_owned(), Arc::clone(program)); 33 | } 34 | 35 | pub fn lookup_function(&self, name: &str) -> Option> { 36 | let funcs = self.functions.lock().unwrap(); 37 | funcs.get(name).map(Arc::clone) 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct Host { 43 | job: Mutex, 44 | job_control_enabled: bool, 45 | funcs: Arc, 46 | } 47 | 48 | impl Host { 49 | pub fn new(job: Job, funcs: &Arc) -> Self { 50 | Self { 51 | job: Mutex::new(job), 52 | job_control_enabled: false, 53 | funcs: Arc::clone(funcs), 54 | } 55 | } 56 | 57 | pub fn with_job_control(job: Job, funcs: &Arc) -> Self { 58 | Self { 59 | job: Mutex::new(job), 60 | job_control_enabled: true, 61 | funcs: Arc::clone(funcs), 62 | } 63 | } 64 | } 65 | 66 | impl ShellHost for Host { 67 | fn lookup_homedir(&self, user: Option<&str>) -> anyhow::Result { 68 | if user.is_none() { 69 | if let Some(home) = dirs_next::home_dir() { 70 | if let Some(s) = home.to_str() { 71 | // Urgh for windows. 72 | Ok(s.replace("\\", "/").into()) 73 | } else { 74 | Ok(home.into()) 75 | } 76 | } else { 77 | bail!("failed to resolve own home dir"); 78 | } 79 | } else { 80 | bail!("lookup_homedir for specific user not implemented"); 81 | } 82 | } 83 | 84 | fn spawn_command( 85 | &self, 86 | argv: &Vec, 87 | environment: &mut Environment, 88 | current_directory: &mut PathBuf, 89 | io_env: &IoEnvironment, 90 | ) -> anyhow::Result { 91 | if argv.is_empty() { 92 | return Ok(Status::Complete(0.into()).into()); 93 | } 94 | 95 | let (search_builtin, search_path, argv) = if argv[0].as_str() == Some("command") { 96 | (false, true, &argv[1..]) 97 | } else if argv[0].as_str() == Some("builtin") { 98 | (true, false, &argv[1..]) 99 | } else { 100 | (true, true, &argv[..]) 101 | }; 102 | 103 | if let Some(name) = argv[0].as_str() { 104 | if let Some(prog) = self.funcs.lookup_function(name) { 105 | // Execute the function. 106 | let job = Job::new_empty(name.to_string()); 107 | let mut machine = 108 | Machine::new(&prog, Some(environment.clone()), ¤t_directory)?; 109 | machine.set_host(Arc::new(Host::with_job_control(job, &self.funcs))); 110 | 111 | machine.set_positional(argv.to_vec()); 112 | 113 | let status = machine.run(); 114 | 115 | let (new_cwd, new_env) = machine.top_environment(); 116 | *current_directory = new_cwd; 117 | *environment = new_env; 118 | 119 | return status.map(Into::into); 120 | } 121 | } 122 | 123 | if search_builtin { 124 | if let Some(builtin) = lookup_builtin(&argv[0]) { 125 | // Create a token for cancellation. 126 | // This is currently useless; I just wanted to establish this in 127 | // the builtins interface. 128 | // This needs to be connected to CTRL-C from the REPL or from a 129 | // signal handler. 130 | let token = Arc::new(Token::new()); 131 | return builtin( 132 | &argv[..], 133 | environment, 134 | current_directory, 135 | io_env, 136 | token, 137 | &self.funcs, 138 | ); 139 | } 140 | } 141 | 142 | if search_path { 143 | if let Some(exe) = PathSearcher::new( 144 | argv[0] 145 | .as_os_str() 146 | .ok_or_else(|| anyhow!("argv0 is not convertible to OsStr"))?, 147 | environment.get("PATH"), 148 | environment.get("PATHEXT"), 149 | ) 150 | .next() 151 | { 152 | let mut child_cmd = std::process::Command::new(&exe); 153 | for (i, arg) in argv.iter().enumerate().skip(1) { 154 | child_cmd.arg( 155 | arg.as_os_str() 156 | .ok_or_else(|| anyhow!("argv{} is not convertible to OsStr", i))?, 157 | ); 158 | } 159 | child_cmd.env_clear(); 160 | child_cmd.envs(environment.iter()); 161 | child_cmd.current_dir(¤t_directory); 162 | 163 | child_cmd.stdin(io_env.fd_as_stdio(0)?); 164 | child_cmd.stdout(io_env.fd_as_stdio(1)?); 165 | child_cmd.stderr(io_env.fd_as_stdio(2)?); 166 | 167 | let process_group_id = self.job.lock().unwrap().process_group_id(); 168 | 169 | #[cfg(unix)] 170 | unsafe { 171 | use std::os::unix::process::CommandExt; 172 | let job_control = self.job_control_enabled; 173 | child_cmd.pre_exec(move || { 174 | let pid = libc::getpid(); 175 | if job_control { 176 | if process_group_id == 0 { 177 | /* 178 | if asynchronous { 179 | make_own_process_group(pid); 180 | } else {} 181 | */ 182 | make_foreground_process_group(pid); 183 | } else { 184 | add_to_process_group(pid, process_group_id); 185 | } 186 | } 187 | for s in &[ 188 | libc::SIGINT, 189 | libc::SIGQUIT, 190 | libc::SIGTSTP, 191 | libc::SIGTTIN, 192 | libc::SIGTTOU, 193 | libc::SIGCHLD, 194 | ] { 195 | libc::signal(*s, libc::SIG_DFL); 196 | } 197 | 198 | Ok(()) 199 | }); 200 | } 201 | 202 | let child = child_cmd 203 | .spawn() 204 | .with_context(|| format!("spawning {:?}", argv))?; 205 | let child = ChildProcess::new(child); 206 | 207 | if self.job_control_enabled { 208 | // To avoid a race condition with starting up the child, we 209 | // need to also munge the process group assignment here in 210 | // the parent. Note that the loser of the race will experience 211 | // errors in attempting this block, so we willfully ignore 212 | // the return values here: we cannot do anything about them. 213 | #[cfg(unix)] 214 | { 215 | let pid = child.pid(); 216 | if process_group_id == 0 { 217 | /* 218 | if asynchronous { 219 | make_own_process_group(pid); 220 | } else {} 221 | */ 222 | make_foreground_process_group(pid); 223 | } else { 224 | add_to_process_group(pid, process_group_id); 225 | } 226 | } 227 | } 228 | 229 | self.job.lock().unwrap().add(child.clone())?; 230 | 231 | if process_group_id == 0 { 232 | JOB_LIST.add(self.job.lock().unwrap().clone()); 233 | } 234 | 235 | return Ok(WaitableStatus::new(Arc::new(child))); 236 | } 237 | } 238 | 239 | if let Some(s) = argv[0].as_str() { 240 | writeln!(io_env.stderr(), "wzsh: {} not found", s)?; 241 | } else { 242 | writeln!(io_env.stderr(), "wzsh: {:?} not found", &argv[0])?; 243 | } 244 | Ok(Status::Complete(127.into()).into()) 245 | } 246 | 247 | fn define_function(&self, name: &str, program: &Arc) -> anyhow::Result<()> { 248 | self.funcs.define_function(name, program); 249 | Ok(()) 250 | } 251 | } 252 | --------------------------------------------------------------------------------