├── .cargo └── config.toml ├── .cliffignore ├── .config └── nextest.toml ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── install-rust-tools.sh ├── .github ├── dependabot.yml └── workflows │ ├── cd.yaml │ ├── ci-reports.yaml │ ├── ci.yaml │ ├── devcontainer.yaml │ ├── docs.yaml │ ├── spelling.yaml │ └── workflow-checks.yaml ├── .gitignore ├── .lycheeignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── brush-core ├── Cargo.toml ├── LICENSE ├── benches │ └── shell.rs └── src │ ├── arithmetic.rs │ ├── builtins.rs │ ├── builtins │ ├── alias.rs │ ├── bg.rs │ ├── bind.rs │ ├── break_.rs │ ├── brushinfo.rs │ ├── builtin_.rs │ ├── cd.rs │ ├── colon.rs │ ├── command.rs │ ├── complete.rs │ ├── continue_.rs │ ├── declare.rs │ ├── dirs.rs │ ├── dot.rs │ ├── echo.rs │ ├── enable.rs │ ├── eval.rs │ ├── exec.rs │ ├── exit.rs │ ├── export.rs │ ├── factory.rs │ ├── false_.rs │ ├── fg.rs │ ├── getopts.rs │ ├── hash.rs │ ├── help.rs │ ├── jobs.rs │ ├── kill.rs │ ├── let_.rs │ ├── mapfile.rs │ ├── popd.rs │ ├── printf.rs │ ├── pushd.rs │ ├── pwd.rs │ ├── read.rs │ ├── return_.rs │ ├── set.rs │ ├── shift.rs │ ├── shopt.rs │ ├── suspend.rs │ ├── test.rs │ ├── times.rs │ ├── trap.rs │ ├── true_.rs │ ├── type_.rs │ ├── ulimit.rs │ ├── umask.rs │ ├── unalias.rs │ ├── unimp.rs │ ├── unset.rs │ └── wait.rs │ ├── commands.rs │ ├── completion.rs │ ├── env.rs │ ├── error.rs │ ├── escape.rs │ ├── expansion.rs │ ├── extendedtests.rs │ ├── functions.rs │ ├── interfaces.rs │ ├── interfaces │ └── keybindings.rs │ ├── interp.rs │ ├── jobs.rs │ ├── keywords.rs │ ├── lib.rs │ ├── namedoptions.rs │ ├── openfiles.rs │ ├── options.rs │ ├── pathcache.rs │ ├── patterns.rs │ ├── processes.rs │ ├── prompt.rs │ ├── regex.rs │ ├── shell.rs │ ├── sys.rs │ ├── sys │ ├── fs.rs │ ├── hostname.rs │ ├── os_pipe.rs │ ├── stubs.rs │ ├── stubs │ │ ├── fs.rs │ │ ├── input.rs │ │ ├── network.rs │ │ ├── pipes.rs │ │ ├── process.rs │ │ ├── resource.rs │ │ ├── signal.rs │ │ ├── terminal.rs │ │ └── users.rs │ ├── tokio_process.rs │ ├── unix.rs │ ├── unix │ │ ├── fs.rs │ │ ├── input.rs │ │ ├── network.rs │ │ ├── resource.rs │ │ ├── signal.rs │ │ ├── terminal.rs │ │ └── users.rs │ ├── wasm.rs │ ├── windows.rs │ └── windows │ │ ├── network.rs │ │ └── users.rs │ ├── terminal.rs │ ├── tests.rs │ ├── timing.rs │ ├── trace_categories.rs │ ├── traps.rs │ └── variables.rs ├── brush-interactive ├── Cargo.toml ├── LICENSE └── src │ ├── basic │ ├── basic_shell.rs │ ├── mod.rs │ ├── raw_mode.rs │ └── term_line_reader.rs │ ├── completion.rs │ ├── error.rs │ ├── interactive_shell.rs │ ├── lib.rs │ ├── minimal │ ├── minimal_shell.rs │ └── mod.rs │ ├── options.rs │ ├── reedline │ ├── completer.rs │ ├── edit_mode.rs │ ├── highlighter.rs │ ├── mod.rs │ ├── prompt.rs │ ├── reedline_shell.rs │ ├── refs.rs │ └── validator.rs │ └── trace_categories.rs ├── brush-parser ├── Cargo.toml ├── LICENSE ├── benches │ └── parser.rs └── src │ ├── arithmetic.rs │ ├── ast.rs │ ├── error.rs │ ├── lib.rs │ ├── parser.rs │ ├── pattern.rs │ ├── prompt.rs │ ├── readline_binding.rs │ ├── test_command.rs │ ├── tokenizer.rs │ └── word.rs ├── brush-shell ├── Cargo.toml ├── LICENSE ├── src │ ├── args.rs │ ├── brushctl.rs │ ├── events.rs │ ├── lib.rs │ ├── main.rs │ ├── productinfo.rs │ └── shell_factory.rs └── tests │ ├── cases │ ├── and_or.yaml │ ├── arguments.yaml │ ├── arithmetic.yaml │ ├── arrays.yaml │ ├── assignments.yaml │ ├── basic.yaml │ ├── builtins │ │ ├── alias.yaml │ │ ├── builtin.yaml │ │ ├── cd.yaml │ │ ├── colon.yaml │ │ ├── command.yaml │ │ ├── common.yaml │ │ ├── compgen.yaml │ │ ├── complete.yaml │ │ ├── declare.yaml │ │ ├── dot.yaml │ │ ├── echo.yaml │ │ ├── enable.yaml │ │ ├── eval.yaml │ │ ├── exec.yaml │ │ ├── exit.yaml │ │ ├── export.yaml │ │ ├── getopts.yaml │ │ ├── hash.yaml │ │ ├── help.yaml │ │ ├── jobs.yaml │ │ ├── kill.yaml │ │ ├── let.yaml │ │ ├── local.yaml │ │ ├── mapfile.yaml │ │ ├── printf.yaml │ │ ├── ps4.yaml │ │ ├── pushd_popd_dirs.yaml │ │ ├── pwd.yaml │ │ ├── read.yaml │ │ ├── readonly.yaml │ │ ├── return.yaml │ │ ├── set.yaml │ │ ├── shopt.yaml │ │ ├── test.yaml │ │ ├── times.yaml │ │ ├── trap.yaml │ │ ├── type.yaml │ │ ├── typeset.yaml │ │ ├── ulimit.yaml │ │ ├── unalias.yaml │ │ └── unset.yaml │ ├── command_substitution.yaml │ ├── complete_commands.yaml │ ├── compound_cmds │ │ ├── arithmetic.yaml │ │ ├── arithmetic_for.yaml │ │ ├── brace.yaml │ │ ├── case.yaml │ │ ├── for.yaml │ │ ├── if.yaml │ │ ├── subshell.yaml │ │ ├── until.yaml │ │ └── while.yaml │ ├── extended_tests.yaml │ ├── functions.yaml │ ├── here.yaml │ ├── interactive.yaml │ ├── list.yaml │ ├── options.yaml │ ├── patterns │ │ ├── filename_expansion.yaml │ │ └── patterns.yaml │ ├── pipeline.yaml │ ├── process.yaml │ ├── prompt.yaml │ ├── redirection.yaml │ ├── simple_commands.yaml │ ├── special_parameters.yaml │ ├── status.yaml │ ├── variables.yaml │ ├── well_known_vars.yaml │ └── word_expansion.yaml │ ├── compat_tests.rs │ ├── completion_tests.rs │ ├── integration_tests.rs │ ├── interactive_tests.rs │ └── utils │ └── process-helpers.sh ├── cliff.toml ├── deny.toml ├── docs ├── README.md ├── demos │ └── demo.tape ├── extras │ └── brush-screenshot.png ├── how-to │ ├── README.md │ ├── build.md │ ├── release.md │ ├── run-benchmarks.md │ └── run-tests.md ├── reference │ ├── README.md │ └── integration-testing.md └── tutorials │ └── README.md ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── fuzz_arithmetic.rs │ └── fuzz_parse.rs ├── release-plz.toml ├── rustfmt.toml ├── scripts ├── compare-benchmark-results.py ├── run-bash-tests.py ├── start-dev-container.py ├── summarize-pytest-results.py └── test-code-coverage.sh ├── xtask ├── Cargo.toml └── src │ └── main.rs └── zizmor.yml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | 4 | [build] 5 | rustflags = ["-C", "target-cpu=native"] 6 | 7 | [target.wasm32-unknown-unknown] 8 | # Select a getrandom backend that will work for this target 9 | rustflags = ["--cfg", 'getrandom_backend="wasm_js"'] 10 | 11 | [target.wasm32-wasip2] 12 | # Uninteresting flag to ensure this overrides the flags in [build] 13 | rustflags = ["--cfg", "x"] 14 | -------------------------------------------------------------------------------- /.cliffignore: -------------------------------------------------------------------------------- 1 | # Commits not to include in changelog 2 | 3 | a8ba7982d9ca3746cd6f6e3c26783e9d1d765466 4 | 5 | 448e60c51cb721bd6991c00bc39e926c7a1ffdac 6 | 6db7ffa0d3ccfbf1e757d5a4eb096fd51d65cc19 7 | 5400c8267db15ed56b5758a6fc35667678a5c856 8 | 87ad64f9ae2044d1aa70effd50244491a765c33e 9 | 6567ede8f0a9be622176dc5e840e5b93344aeb15 10 | -------------------------------------------------------------------------------- /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | retries = 3 3 | status-level = "slow" 4 | slow-timeout = { period = "5s", terminate-after = 3, grace-period = "2s" } 5 | fail-fast = false 6 | 7 | [profile.default.junit] 8 | path = "test-results.xml" 9 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ARG USERNAME=devel 4 | ARG USER_UID=1000 5 | ARG USER_GID=$USER_UID 6 | 7 | # Install basic utilities and prerequisites. 8 | # bash-completion - for completion testing 9 | # bsdmainutils - provides hexdump, used by integration tests 10 | # neovim - for convenience and modern editing 11 | RUN apt-get update -y && \ 12 | apt-get install -y \ 13 | bash \ 14 | bash-completion \ 15 | bsdmainutils \ 16 | build-essential \ 17 | curl \ 18 | git \ 19 | iputils-ping \ 20 | language-pack-en \ 21 | neovim \ 22 | sed \ 23 | sudo \ 24 | wget 25 | 26 | # Install gh cli 27 | # Reference: https://github.com/cli/cli/blob/trunk/docs/install_linux.md 28 | RUN mkdir -p -m 755 /etc/apt/keyrings && \ 29 | wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && \ 30 | chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && \ 31 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \ 32 | apt-get update -y && \ 33 | apt-get install -y gh 34 | 35 | # Add a non-root user that we'll do our best to use for development. 36 | RUN userdel ubuntu && \ 37 | groupadd --gid $USER_GID $USERNAME && \ 38 | useradd --uid $USER_UID --gid $USER_GID -m $USERNAME && \ 39 | echo "devel ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers 40 | 41 | # Switch to user. 42 | USER devel 43 | 44 | # Set up path to include rust components. 45 | ENV PATH="${PATH}:/home/devel/.cargo/bin" 46 | 47 | # Copy scripts to temp dir. 48 | WORKDIR /tmp 49 | 50 | # Install rust toolchain and cargo tools. 51 | COPY install-rust-tools.sh . 52 | RUN ./install-rust-tools.sh 53 | 54 | # Switch back to home dir for normal usage. 55 | WORKDIR /home/devel 56 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": "." 5 | }, 6 | "customizations": { 7 | "vscode": { 8 | "extensions": [ 9 | "rust-lang.rust-analyzer" 10 | ] 11 | } 12 | }, 13 | "features": {}, 14 | "remoteUser": "devel", 15 | "updateRemoteUserUID": true 16 | } -------------------------------------------------------------------------------- /.devcontainer/install-rust-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Install rustup and needed components 5 | # Reference: https://rustup.rs/ 6 | curl https://sh.rustup.rs -sSf | sh -s -- -y 7 | rustup component add llvm-tools-preview 8 | 9 | # Install cargo binstall 10 | # Reference: https://github.com/cargo-bins/cargo-binstall 11 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 12 | 13 | # Install cargo tools using binstall 14 | cargo binstall --no-confirm cargo-audit 15 | cargo binstall --no-confirm cargo-deny 16 | # cargo binstall --no-confirm cargo-flamegraph 17 | cargo binstall --no-confirm cargo-llvm-cov 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | 12 | - package-ecosystem: "cargo" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | groups: 17 | cargo: 18 | patterns: 19 | - "*" 20 | -------------------------------------------------------------------------------- /.github/workflows/devcontainer.yaml: -------------------------------------------------------------------------------- 1 | name: "Devcontainer" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - ".devcontainer/**" 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | build: 14 | name: "Build devcontainer" 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | contents: read 18 | packages: read 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | 25 | - name: Login to GitHub Container Registry 26 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.repository_owner }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Pre-build dev container image 33 | uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 34 | with: 35 | imageName: ghcr.io/reubeno/brush/devcontainer 36 | imageTag: latest 37 | cacheFrom: ghcr.io/reubeno/brush/devcontainer 38 | push: never 39 | 40 | build_and_publish: 41 | name: "Build and publish devcontainer" 42 | runs-on: ubuntu-24.04 43 | permissions: 44 | contents: read 45 | packages: write 46 | steps: 47 | - name: Checkout sources 48 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | with: 50 | persist-credentials: false 51 | 52 | - name: Login to GitHub Container Registry 53 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.repository_owner }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Pre-build dev container image 60 | uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 61 | with: 62 | imageName: ghcr.io/reubeno/brush/devcontainer 63 | imageTag: latest 64 | cacheFrom: ghcr.io/reubeno/brush/devcontainer 65 | push: filter 66 | refFilterForPush: refs/heads/main 67 | eventFilterForPush: push 68 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: "Docs" 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | RUST_BACKTRACE: 1 11 | CARGO_TERM_COLOR: always 12 | CLICOLOR: 1 13 | CLICOLOR_FORCE: 1 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | # Check links in docs 20 | check-links: 21 | name: "Check links" 22 | runs-on: ubuntu-24.04 23 | steps: 24 | - name: Checkout sources 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Check repo-local links 30 | uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 31 | with: 32 | args: ". --offline --verbose --no-progress" 33 | fail: true 34 | 35 | # Generate docs content 36 | generate-docs: 37 | name: "Generate usage docs" 38 | runs-on: ubuntu-24.04 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 42 | with: 43 | persist-credentials: false 44 | 45 | - name: Set up rust toolchain 46 | uses: dtolnay/rust-toolchain@stable 47 | with: 48 | toolchain: stable 49 | 50 | - name: Enable cargo cache 51 | uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 52 | 53 | - name: Create dirs 54 | run: mkdir -p ./md ./man 55 | 56 | - name: Build markdown info 57 | run: cargo xtask generate-markdown --out ./md/brush.md 58 | 59 | - name: Upload markdown docs 60 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 61 | with: 62 | name: docs-markdown 63 | path: md 64 | 65 | - name: Build man pages 66 | run: cargo xtask generate-man --output-dir ./man 67 | 68 | - name: Upload man pages 69 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 70 | with: 71 | name: docs-man 72 | path: man 73 | -------------------------------------------------------------------------------- /.github/workflows/spelling.yaml: -------------------------------------------------------------------------------- 1 | name: Spelling 2 | on: [pull_request] 3 | 4 | env: 5 | RUST_BACKTRACE: 1 6 | CARGO_TERM_COLOR: always 7 | CLICOLOR: 1 8 | CLICOLOR_FORCE: 1 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | spelling: 15 | name: spell-check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout brush 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | 23 | - name: Spell check repo 24 | uses: crate-ci/typos@0f0ccba9ed1df83948f0c15026e4f5ccfce46109 # v1.32.0 25 | -------------------------------------------------------------------------------- /.github/workflows/workflow-checks.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # This workflow was based on https://docs.zizmor.sh/usage/#use-in-github-actions 3 | # 4 | 5 | name: GitHub Actions Security Analysis with zizmor 🌈 6 | 7 | on: 8 | push: 9 | branches: ["main"] 10 | pull_request: 11 | branches: ["**"] 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | zizmor: 17 | name: zizmor latest via PyPI 18 | runs-on: ubuntu-latest 19 | permissions: 20 | security-events: write # needed for SARIF uploads 21 | contents: read # only needed for private repos 22 | actions: read # only needed for private repos 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Install the latest version of uv 30 | uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 31 | 32 | - name: Run zizmor 🌈 33 | run: uvx zizmor --format=sarif . > results.sarif 34 | env: 35 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Upload SARIF file 38 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 39 | with: 40 | sarif_file: results.sarif 41 | category: zizmor 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | mutants.out*/ 3 | -------------------------------------------------------------------------------- /.lycheeignore: -------------------------------------------------------------------------------- 1 | https://github.com/reubeno/brush/* 2 | target/* 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions to recommend for users of this workspace. 3 | // See https://go.microsoft.com/fwlink/?LinkId=827846 for more details. 4 | "recommendations": [ 5 | "rust-lang.rust-analyzer", 6 | ], 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'brush'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=brush", 15 | "--package=brush-shell" 16 | ], 17 | "filter": { 18 | "name": "brush", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [ 23 | "--enable-highlighting" 24 | ], 25 | "cwd": "${workspaceFolder}" 26 | }, 27 | { 28 | "type": "lldb", 29 | "request": "launch", 30 | "name": "Debug integration test 'brush-compat-tests'", 31 | "cargo": { 32 | "args": [ 33 | "test", 34 | "--no-run", 35 | "--test=brush-compat-tests", 36 | "--package=brush-shell" 37 | ], 38 | "filter": { 39 | "name": "brush-compat-tests", 40 | "kind": "test" 41 | } 42 | }, 43 | "args": [], 44 | "cwd": "${workspaceFolder}" 45 | }, 46 | { 47 | "type": "lldb", 48 | "request": "launch", 49 | "name": "Debug executable 'xtask'", 50 | "cargo": { 51 | "args": [ 52 | "build", 53 | "--bin=xtask", 54 | "--package=xtask" 55 | ], 56 | "filter": { 57 | "name": "xtask", 58 | "kind": "bin" 59 | } 60 | }, 61 | "args": [], 62 | "cwd": "${workspaceFolder}" 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.exclude": { 4 | "target/": true 5 | }, 6 | "terminal.integrated.defaultProfile.linux": "bash", 7 | "terminal.integrated.profiles.linux": { 8 | "bash": { 9 | "path": "/bin/bash" 10 | } 11 | }, 12 | "rust-analyzer.check.command": "clippy", 13 | "rewrap.wrappingColumn": 90, 14 | "rust-analyzer.debug.openDebugPane": true, 15 | "rust-analyzer.testExplorer": true, 16 | "rust-analyzer.check.features": "all" 17 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "brush-shell", 5 | "brush-parser", 6 | "brush-core", 7 | "brush-interactive", 8 | "fuzz", 9 | "xtask", 10 | ] 11 | default-members = ["brush-shell"] 12 | 13 | [workspace.package] 14 | authors = ["reuben olinsky"] 15 | categories = ["command-line-utilities", "development-tools"] 16 | edition = "2021" 17 | keywords = ["cli", "shell", "sh", "bash", "script"] 18 | license = "MIT" 19 | readme = "README.md" 20 | repository = "https://github.com/reubeno/brush" 21 | rust-version = "1.75.0" 22 | 23 | [workspace.lints.rust] 24 | warnings = { level = "deny" } 25 | rust_2018_idioms = { level = "deny", priority = -1 } 26 | unnameable_types = "deny" 27 | unsafe_op_in_unsafe_fn = "deny" 28 | unused_lifetimes = "deny" 29 | unused_macro_rules = "deny" 30 | # For now we allow unknown lints so we can opt into warnings that don't exist on the 31 | # oldest toolchain we need to support. 32 | unknown_lints = { level = "allow", priority = -100 } 33 | 34 | [workspace.lints.clippy] 35 | all = { level = "deny", priority = -1 } 36 | pedantic = { level = "deny", priority = -1 } 37 | cargo = { level = "deny", priority = -1 } 38 | perf = { level = "deny", priority = -1 } 39 | expect_used = "deny" 40 | format_push_string = "deny" 41 | panic = "deny" 42 | panic_in_result_fn = "deny" 43 | todo = "deny" 44 | unwrap_in_result = "deny" 45 | bool_to_int_with_if = "allow" 46 | collapsible_else_if = "allow" 47 | collapsible_if = "allow" 48 | if_not_else = "allow" 49 | if_same_then_else = "allow" 50 | match_same_arms = "allow" 51 | missing_errors_doc = "allow" 52 | missing_panics_doc = "allow" 53 | multiple_crate_versions = "allow" 54 | must_use_candidate = "allow" 55 | redundant_closure_for_method_calls = "allow" 56 | redundant_else = "allow" 57 | result_large_err = "allow" 58 | similar_names = "allow" 59 | struct_excessive_bools = "allow" 60 | 61 | [workspace.metadata.typos] 62 | files.extend-exclude = [ 63 | # Exclude the changelog because it's dynamically generated. 64 | "/CHANGELOG.md", 65 | # Remove this once impending mapfile updates are merged. 66 | "/brush-core/src/builtins/mapfile.rs", 67 | ] 68 | 69 | [workspace.metadata.typos.default] 70 | extend-ignore-re = [ 71 | # -ot is a valid binary operator 72 | "-ot", 73 | # Ignore 2-letter string literals, which show up in testing a fair bit. 74 | '"[a-zA-Z]{2}"', 75 | ] 76 | 77 | [workspace.metadata.typos.default.extend-words] 78 | # These show up in tests. 79 | "abd" = "abd" 80 | "hel" = "hel" 81 | "ba" = "ba" 82 | 83 | [profile.release] 84 | strip = true 85 | lto = "fat" 86 | codegen-units = 1 87 | panic = "abort" 88 | 89 | [profile.bench] 90 | inherits = "release" 91 | strip = "debuginfo" 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 reuben olinsky 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 | -------------------------------------------------------------------------------- /brush-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brush-core" 3 | description = "Reusable core of a POSIX/bash shell (used by brush-shell)" 4 | version = "0.3.1" 5 | categories.workspace = true 6 | edition.workspace = true 7 | keywords.workspace = true 8 | license.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | rust-version.workspace = true 12 | 13 | [lib] 14 | bench = false 15 | 16 | [lints] 17 | workspace = true 18 | 19 | [dependencies] 20 | async-recursion = "1.1.1" 21 | async-trait = "0.1.88" 22 | brush-parser = { version = "^0.2.16", path = "../brush-parser" } 23 | cached = "0.55.1" 24 | cfg-if = "1.0.0" 25 | chrono = "0.4.41" 26 | clap = { version = "4.5.37", features = ["derive", "wrap_help"] } 27 | fancy-regex = "0.14.0" 28 | futures = "0.3.31" 29 | indexmap = "2.9.0" 30 | itertools = "0.14.0" 31 | lazy_static = "1.5.0" 32 | normalize-path = "0.2.1" 33 | rand = "0.9.1" 34 | strum = "0.27.1" 35 | strum_macros = "0.27.1" 36 | thiserror = "2.0.12" 37 | tracing = "0.1.41" 38 | 39 | [target.'cfg(target_family = "wasm")'.dependencies] 40 | tokio = { version = "1.45.1", features = ["io-util", "macros", "rt"] } 41 | 42 | [target.'cfg(any(windows, unix))'.dependencies] 43 | hostname = "0.4.1" 44 | os_pipe = { version = "1.2.2", features = ["io_safety"] } 45 | tokio = { version = "1.45.1", features = [ 46 | "io-util", 47 | "macros", 48 | "process", 49 | "rt", 50 | "rt-multi-thread", 51 | "signal", 52 | "sync", 53 | ] } 54 | 55 | [target.'cfg(windows)'.dependencies] 56 | homedir = "0.3.4" 57 | whoami = "1.6.0" 58 | 59 | [target.'cfg(unix)'.dependencies] 60 | command-fds = "=0.3.0" 61 | nix = { version = "0.30.1", features = [ 62 | "fs", 63 | "process", 64 | "resource", 65 | "signal", 66 | "term", 67 | "user", 68 | ] } 69 | rlimit = "0.10.2" 70 | terminfo = "0.9.0" 71 | uzers = "0.12.1" 72 | 73 | [target.'cfg(target_os = "linux")'.dependencies] 74 | procfs = "0.17.0" 75 | 76 | [target.wasm32-unknown-unknown.dependencies] 77 | getrandom = { version = "0.3.3", features = ["wasm_js"] } 78 | uuid = { version = "1.17.0", features = ["js"] } 79 | 80 | [dev-dependencies] 81 | anyhow = "1.0.98" 82 | criterion = { version = "0.5.1", features = ["async_tokio", "html_reports"] } 83 | 84 | [target.'cfg(unix)'.dev-dependencies] 85 | pprof = { version = "0.15.0", features = ["criterion", "flamegraph"] } 86 | 87 | [[bench]] 88 | name = "shell" 89 | harness = false 90 | -------------------------------------------------------------------------------- /brush-core/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /brush-core/src/builtins/alias.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Manage aliases within the shell. 7 | #[derive(Parser)] 8 | pub(crate) struct AliasCommand { 9 | /// Print all defined aliases in a reusable format. 10 | #[arg(short = 'p')] 11 | print: bool, 12 | 13 | /// List of aliases to display or update. 14 | #[arg(name = "name[=value]")] 15 | aliases: Vec, 16 | } 17 | 18 | impl builtins::Command for AliasCommand { 19 | async fn execute( 20 | &self, 21 | context: commands::ExecutionContext<'_>, 22 | ) -> Result { 23 | let mut exit_code = builtins::ExitCode::Success; 24 | 25 | if self.print || self.aliases.is_empty() { 26 | for (name, value) in &context.shell.aliases { 27 | writeln!(context.stdout(), "alias {name}='{value}'")?; 28 | } 29 | } else { 30 | for alias in &self.aliases { 31 | if let Some((name, unexpanded_value)) = alias.split_once('=') { 32 | context 33 | .shell 34 | .aliases 35 | .insert(name.to_owned(), unexpanded_value.to_owned()); 36 | } else if let Some(value) = context.shell.aliases.get(alias) { 37 | writeln!(context.stdout(), "alias {alias}='{value}'")?; 38 | } else { 39 | writeln!( 40 | context.stderr(), 41 | "{}: {alias}: not found", 42 | context.command_name 43 | )?; 44 | exit_code = builtins::ExitCode::Custom(1); 45 | } 46 | } 47 | } 48 | 49 | Ok(exit_code) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /brush-core/src/builtins/bg.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Moves a job to run in the background. 7 | #[derive(Parser)] 8 | pub(crate) struct BgCommand { 9 | /// List of job specs to move to background. 10 | job_specs: Vec, 11 | } 12 | 13 | impl builtins::Command for BgCommand { 14 | async fn execute( 15 | &self, 16 | context: commands::ExecutionContext<'_>, 17 | ) -> Result { 18 | let mut exit_code = builtins::ExitCode::Success; 19 | 20 | if !self.job_specs.is_empty() { 21 | for job_spec in &self.job_specs { 22 | if let Some(job) = context.shell.jobs.resolve_job_spec(job_spec) { 23 | job.move_to_background()?; 24 | } else { 25 | writeln!( 26 | context.stderr(), 27 | "{}: {}: no such job", 28 | context.command_name, 29 | job_spec 30 | )?; 31 | exit_code = builtins::ExitCode::Custom(1); 32 | } 33 | } 34 | } else { 35 | if let Some(job) = context.shell.jobs.current_job_mut() { 36 | job.move_to_background()?; 37 | } else { 38 | writeln!(context.stderr(), "{}: no current job", context.command_name)?; 39 | exit_code = builtins::ExitCode::Custom(1); 40 | } 41 | } 42 | 43 | Ok(exit_code) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /brush-core/src/builtins/break_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Breaks out of a control-flow loop. 6 | #[derive(Parser)] 7 | pub(crate) struct BreakCommand { 8 | /// If specified, indicates which nested loop to break out of. 9 | #[clap(default_value_t = 1)] 10 | which_loop: i8, 11 | } 12 | 13 | impl builtins::Command for BreakCommand { 14 | async fn execute( 15 | &self, 16 | _context: commands::ExecutionContext<'_>, 17 | ) -> Result { 18 | // If specified, which_loop needs to be positive. 19 | if self.which_loop <= 0 { 20 | return Ok(builtins::ExitCode::InvalidUsage); 21 | } 22 | 23 | #[allow(clippy::cast_sign_loss)] 24 | Ok(builtins::ExitCode::BreakLoop((self.which_loop - 1) as u8)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /brush-core/src/builtins/builtin_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Directly invokes a built-in, without going through typical search order. 7 | #[derive(Default, Parser)] 8 | pub(crate) struct BuiltinCommand { 9 | #[clap(skip)] 10 | args: Vec, 11 | } 12 | 13 | impl builtins::DeclarationCommand for BuiltinCommand { 14 | fn set_declarations(&mut self, args: Vec) { 15 | self.args = args; 16 | } 17 | } 18 | 19 | impl builtins::Command for BuiltinCommand { 20 | async fn execute( 21 | &self, 22 | mut context: commands::ExecutionContext<'_>, 23 | ) -> Result { 24 | if self.args.is_empty() { 25 | return Ok(builtins::ExitCode::Success); 26 | } 27 | 28 | let args: Vec<_> = self.args.iter().skip(1).cloned().collect(); 29 | if args.is_empty() { 30 | return Ok(builtins::ExitCode::Success); 31 | } 32 | 33 | let builtin_name = args[0].to_string(); 34 | 35 | if let Some(builtin) = context.shell.builtins.get(&builtin_name) { 36 | context.command_name = builtin_name; 37 | 38 | (builtin.execute_func)(context, args) 39 | .await 40 | .map(|res: builtins::BuiltinResult| res.exit_code) 41 | } else { 42 | writeln!(context.stderr(), "{builtin_name}: command not found")?; 43 | Ok(builtins::ExitCode::Custom(1)) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /brush-core/src/builtins/colon.rs: -------------------------------------------------------------------------------- 1 | use crate::{builtins, commands, error}; 2 | 3 | /// No-op command. 4 | pub(crate) struct ColonCommand {} 5 | 6 | impl builtins::SimpleCommand for ColonCommand { 7 | fn get_content( 8 | _name: &str, 9 | content_type: builtins::ContentType, 10 | ) -> Result { 11 | match content_type { 12 | builtins::ContentType::DetailedHelp => { 13 | Ok("Null command; always returns success.".into()) 14 | } 15 | builtins::ContentType::ShortUsage => Ok(":: :".into()), 16 | builtins::ContentType::ShortDescription => Ok(": - Null command".into()), 17 | builtins::ContentType::ManPage => error::unimp("man page not yet implemented"), 18 | } 19 | } 20 | 21 | fn execute, S: AsRef>( 22 | _context: commands::ExecutionContext<'_>, 23 | _args: I, 24 | ) -> Result { 25 | Ok(builtins::BuiltinResult { 26 | exit_code: builtins::ExitCode::Success, 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /brush-core/src/builtins/continue_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Continue to the next iteration of a control-flow loop. 6 | #[derive(Parser)] 7 | pub(crate) struct ContinueCommand { 8 | /// If specified, indicates which nested loop to continue to the next iteration of. 9 | #[clap(default_value_t = 1)] 10 | which_loop: i8, 11 | } 12 | 13 | impl builtins::Command for ContinueCommand { 14 | async fn execute( 15 | &self, 16 | _context: commands::ExecutionContext<'_>, 17 | ) -> Result { 18 | // If specified, which_loop needs to be positive. 19 | if self.which_loop <= 0 { 20 | return Ok(builtins::ExitCode::InvalidUsage); 21 | } 22 | 23 | #[allow(clippy::cast_sign_loss)] 24 | Ok(builtins::ExitCode::ContinueLoop( 25 | (self.which_loop - 1) as u8, 26 | )) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /brush-core/src/builtins/dirs.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Manage the current directory stack. 7 | #[derive(Default, Parser)] 8 | pub(crate) struct DirsCommand { 9 | /// Clear the directory stack. 10 | #[arg(short = 'c')] 11 | clear: bool, 12 | 13 | /// Don't tilde-shorten paths. 14 | #[arg(short = 'l')] 15 | tilde_long: bool, 16 | 17 | /// Print one directory per line instead of all on one line. 18 | #[arg(short = 'p')] 19 | print_one_per_line: bool, 20 | 21 | /// Print one directory per line with its index. 22 | #[arg(short = 'v')] 23 | print_one_per_line_with_index: bool, 24 | // 25 | // TODO: implement +N and -N 26 | } 27 | 28 | impl builtins::Command for DirsCommand { 29 | async fn execute( 30 | &self, 31 | context: commands::ExecutionContext<'_>, 32 | ) -> Result { 33 | if self.clear { 34 | context.shell.directory_stack.clear(); 35 | } else { 36 | let dirs = vec![&context.shell.working_dir] 37 | .into_iter() 38 | .chain(context.shell.directory_stack.iter().rev()) 39 | .collect::>(); 40 | 41 | let one_per_line = self.print_one_per_line || self.print_one_per_line_with_index; 42 | 43 | for (i, dir) in dirs.iter().enumerate() { 44 | if !one_per_line && i > 0 { 45 | write!(context.stdout(), " ")?; 46 | } 47 | 48 | if self.print_one_per_line_with_index { 49 | write!(context.stdout(), "{i:2} ")?; 50 | } 51 | 52 | let mut dir_str = dir.to_string_lossy().to_string(); 53 | 54 | if !self.tilde_long { 55 | dir_str = context.shell.tilde_shorten(dir_str); 56 | } 57 | 58 | write!(context.stdout(), "{dir_str}")?; 59 | 60 | if one_per_line || i == dirs.len() - 1 { 61 | writeln!(context.stdout())?; 62 | } 63 | } 64 | 65 | return Ok(builtins::ExitCode::Success); 66 | } 67 | 68 | Ok(builtins::ExitCode::Success) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /brush-core/src/builtins/dot.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use clap::Parser; 4 | 5 | use crate::{builtins, commands}; 6 | 7 | /// Evaluate the provided script in the current shell environment. 8 | #[derive(Parser)] 9 | pub(crate) struct DotCommand { 10 | /// Path to the script to evaluate. 11 | script_path: String, 12 | 13 | /// Any arguments to be passed as positional parameters to the script. 14 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 15 | script_args: Vec, 16 | } 17 | 18 | impl builtins::Command for DotCommand { 19 | async fn execute( 20 | &self, 21 | context: commands::ExecutionContext<'_>, 22 | ) -> Result { 23 | // TODO: Handle trap inheritance. 24 | let params = context.params.clone(); 25 | let result = context 26 | .shell 27 | .source_script( 28 | Path::new(&self.script_path), 29 | self.script_args.iter(), 30 | ¶ms, 31 | ) 32 | .await?; 33 | 34 | if result.exit_code != 0 { 35 | return Ok(builtins::ExitCode::Custom(result.exit_code)); 36 | } 37 | 38 | Ok(builtins::ExitCode::Success) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /brush-core/src/builtins/echo.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, escape}; 5 | 6 | /// Echo text to standard output. 7 | #[derive(Parser)] 8 | #[clap(disable_help_flag = true, disable_version_flag = true)] 9 | pub(crate) struct EchoCommand { 10 | /// Suppress the trailing newline from the output. 11 | #[arg(short = 'n')] 12 | no_trailing_newline: bool, 13 | 14 | /// Interpret backslash escapes in the provided text. 15 | #[arg(short = 'e')] 16 | interpret_backslash_escapes: bool, 17 | 18 | /// Do not interpret backslash escapes in the provided text. 19 | #[arg(short = 'E')] 20 | no_interpret_backslash_escapes: bool, 21 | 22 | /// Tokens to echo to standard output. 23 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 24 | args: Vec, 25 | } 26 | 27 | impl builtins::Command for EchoCommand { 28 | /// Override the default [`builtins::Command::new`] function to handle clap's limitation related 29 | /// to `--`. See [`builtins::parse_known`] for more information 30 | /// TODO: we can safely remove this after the issue is resolved 31 | fn new(args: I) -> Result 32 | where 33 | I: IntoIterator, 34 | { 35 | let (mut this, rest_args) = crate::builtins::try_parse_known::(args)?; 36 | if let Some(args) = rest_args { 37 | this.args.extend(args); 38 | } 39 | Ok(this) 40 | } 41 | 42 | async fn execute( 43 | &self, 44 | context: commands::ExecutionContext<'_>, 45 | ) -> Result { 46 | let mut trailing_newline = !self.no_trailing_newline; 47 | let mut s; 48 | if self.interpret_backslash_escapes { 49 | s = String::new(); 50 | for (i, arg) in self.args.iter().enumerate() { 51 | if i > 0 { 52 | s.push(' '); 53 | } 54 | 55 | let (expanded_arg, keep_going) = escape::expand_backslash_escapes( 56 | arg.as_str(), 57 | escape::EscapeExpansionMode::EchoBuiltin, 58 | )?; 59 | s.push_str(&String::from_utf8_lossy(expanded_arg.as_slice())); 60 | 61 | if !keep_going { 62 | trailing_newline = false; 63 | break; 64 | } 65 | } 66 | } else { 67 | s = self.args.join(" "); 68 | } 69 | 70 | if trailing_newline { 71 | s.push('\n'); 72 | } 73 | 74 | write!(context.stdout(), "{s}")?; 75 | context.stdout().flush()?; 76 | 77 | Ok(builtins::ExitCode::Success) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /brush-core/src/builtins/enable.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use itertools::Itertools; 3 | use std::io::Write; 4 | 5 | use crate::builtins; 6 | use crate::commands; 7 | use crate::error; 8 | 9 | /// Enable, disable, or display built-in commands. 10 | #[derive(Parser)] 11 | pub(crate) struct EnableCommand { 12 | /// Print a list of built-in commands. 13 | #[arg(short = 'a')] 14 | print_list: bool, 15 | 16 | /// Disables the specified built-in commands. 17 | #[arg(short = 'n')] 18 | disable: bool, 19 | 20 | /// Print a list of built-in commands with reusable output. 21 | #[arg(short = 'p')] 22 | print_reusably: bool, 23 | 24 | /// Only operate on special built-in commands. 25 | #[arg(short = 's')] 26 | special_only: bool, 27 | 28 | /// Path to a shared object from which built-in commands will be loaded. 29 | #[arg(short = 'f', value_name = "PATH")] 30 | shared_object_path: Option, 31 | 32 | /// Remove the built-in commands loaded from the indicated object path. 33 | #[arg(short = 'd')] 34 | remove_loaded_builtin: bool, 35 | 36 | /// Names of built-in commands to operate on. 37 | names: Vec, 38 | } 39 | 40 | impl builtins::Command for EnableCommand { 41 | async fn execute( 42 | &self, 43 | context: commands::ExecutionContext<'_>, 44 | ) -> Result { 45 | let mut result = builtins::ExitCode::Success; 46 | 47 | if self.shared_object_path.is_some() { 48 | return error::unimp("enable -f"); 49 | } 50 | if self.remove_loaded_builtin { 51 | return error::unimp("enable -d"); 52 | } 53 | 54 | if !self.names.is_empty() { 55 | for name in &self.names { 56 | if let Some(builtin) = context.shell.builtins.get_mut(name) { 57 | builtin.disabled = self.disable; 58 | } else { 59 | writeln!(context.stderr(), "{name}: not a shell builtin")?; 60 | result = builtins::ExitCode::Custom(1); 61 | } 62 | } 63 | } else { 64 | let builtins: Vec<_> = context 65 | .shell 66 | .builtins 67 | .iter() 68 | .sorted_by_key(|(name, _reg)| *name) 69 | .collect(); 70 | 71 | for (builtin_name, builtin) in builtins { 72 | if self.disable { 73 | if !builtin.disabled { 74 | continue; 75 | } 76 | } else if self.print_list { 77 | if builtin.disabled { 78 | continue; 79 | } 80 | } 81 | 82 | if self.special_only && !builtin.special_builtin { 83 | continue; 84 | } 85 | 86 | let prefix = if builtin.disabled { "-n " } else { "" }; 87 | 88 | writeln!(context.stdout(), "enable {prefix}{builtin_name}")?; 89 | } 90 | } 91 | 92 | Ok(result) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /brush-core/src/builtins/eval.rs: -------------------------------------------------------------------------------- 1 | use crate::{builtins, commands}; 2 | use clap::Parser; 3 | 4 | /// Evaluate the given string as script. 5 | #[derive(Parser)] 6 | pub(crate) struct EvalCommand { 7 | /// The script to evaluate. 8 | #[clap(allow_hyphen_values = true)] 9 | args: Vec, 10 | } 11 | 12 | impl builtins::Command for EvalCommand { 13 | async fn execute( 14 | &self, 15 | context: commands::ExecutionContext<'_>, 16 | ) -> Result { 17 | if !self.args.is_empty() { 18 | let args_concatenated = self.args.join(" "); 19 | 20 | tracing::debug!("Applying eval to: {:?}", args_concatenated); 21 | 22 | let params = context.params.clone(); 23 | let exec_result = context.shell.run_string(args_concatenated, ¶ms).await?; 24 | 25 | Ok(builtins::ExitCode::Custom(exec_result.exit_code)) 26 | } else { 27 | Ok(builtins::ExitCode::Success) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /brush-core/src/builtins/exec.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::{borrow::Cow, os::unix::process::CommandExt}; 3 | 4 | use crate::{builtins, commands, error}; 5 | 6 | /// Exec the provided command. 7 | #[derive(Parser)] 8 | pub(crate) struct ExecCommand { 9 | /// Pass given name as zeroth argument to command. 10 | #[arg(short = 'a', value_name = "NAME")] 11 | name_for_argv0: Option, 12 | 13 | /// Exec command with an empty environment. 14 | #[arg(short = 'c')] 15 | empty_environment: bool, 16 | 17 | /// Exec command as a login shell. 18 | #[arg(short = 'l')] 19 | exec_as_login: bool, 20 | 21 | /// Command and args. 22 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 23 | args: Vec, 24 | } 25 | 26 | impl builtins::Command for ExecCommand { 27 | async fn execute( 28 | &self, 29 | context: commands::ExecutionContext<'_>, 30 | ) -> Result { 31 | if self.args.is_empty() { 32 | // When no arguments are present, then there's nothing for us to execute -- but we need 33 | // to ensure that any redirections setup for this builtin get applied to the calling 34 | // shell instance. 35 | context.shell.replace_open_files(context.params.open_files); 36 | return Ok(builtins::ExitCode::Success); 37 | } 38 | 39 | let mut argv0 = Cow::Borrowed(self.name_for_argv0.as_ref().unwrap_or(&self.args[0])); 40 | 41 | if self.exec_as_login { 42 | argv0 = Cow::Owned(std::format!("-{argv0}")); 43 | } 44 | 45 | let mut cmd = commands::compose_std_command( 46 | context.shell, 47 | &self.args[0], 48 | argv0.as_str(), 49 | &self.args[1..], 50 | context.params.open_files.clone(), 51 | self.empty_environment, 52 | )?; 53 | 54 | let exec_error = cmd.exec(); 55 | 56 | if exec_error.kind() == std::io::ErrorKind::NotFound { 57 | Ok(builtins::ExitCode::Custom(127)) 58 | } else { 59 | Err(error::Error::from(exec_error)) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /brush-core/src/builtins/exit.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Exit the shell. 6 | #[derive(Parser)] 7 | pub(crate) struct ExitCommand { 8 | /// The exit code to return. 9 | code: Option, 10 | } 11 | 12 | impl builtins::Command for ExitCommand { 13 | async fn execute( 14 | &self, 15 | context: commands::ExecutionContext<'_>, 16 | ) -> Result { 17 | let code_8bit: u8; 18 | 19 | #[allow(clippy::cast_sign_loss)] 20 | if let Some(code_32bit) = &self.code { 21 | code_8bit = (code_32bit & 0xFF) as u8; 22 | } else { 23 | code_8bit = context.shell.last_exit_status; 24 | } 25 | 26 | Ok(builtins::ExitCode::ExitShell(code_8bit)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /brush-core/src/builtins/false_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Return a non-zero exit code. 6 | #[derive(Parser)] 7 | pub(crate) struct FalseCommand {} 8 | 9 | impl builtins::Command for FalseCommand { 10 | async fn execute( 11 | &self, 12 | _context: commands::ExecutionContext<'_>, 13 | ) -> Result { 14 | Ok(builtins::ExitCode::Custom(1)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /brush-core/src/builtins/fg.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, jobs, sys}; 5 | 6 | /// Move a specified job to the foreground. 7 | #[derive(Parser)] 8 | pub(crate) struct FgCommand { 9 | /// Job spec for the job to move to the foreground; if not specified, the current job is moved. 10 | job_spec: Option, 11 | } 12 | 13 | impl builtins::Command for FgCommand { 14 | async fn execute( 15 | &self, 16 | context: commands::ExecutionContext<'_>, 17 | ) -> Result { 18 | let mut stderr = context.stdout(); 19 | 20 | if let Some(job_spec) = &self.job_spec { 21 | if let Some(job) = context.shell.jobs.resolve_job_spec(job_spec) { 22 | job.move_to_foreground()?; 23 | writeln!(stderr, "{}", job.command_line)?; 24 | 25 | let result = job.wait().await?; 26 | if context.shell.options.interactive { 27 | sys::terminal::move_self_to_foreground()?; 28 | } 29 | 30 | if matches!(job.state, jobs::JobState::Stopped) { 31 | // N.B. We use the '\r' to overwrite any ^Z output. 32 | let formatted = job.to_string(); 33 | writeln!(context.stderr(), "\r{formatted}")?; 34 | } 35 | 36 | Ok(builtins::ExitCode::from(result)) 37 | } else { 38 | writeln!( 39 | stderr, 40 | "{}: {}: no such job", 41 | job_spec, context.command_name 42 | )?; 43 | Ok(builtins::ExitCode::Custom(1)) 44 | } 45 | } else { 46 | if let Some(job) = context.shell.jobs.current_job_mut() { 47 | job.move_to_foreground()?; 48 | writeln!(stderr, "{}", job.command_line)?; 49 | 50 | let result = job.wait().await?; 51 | if context.shell.options.interactive { 52 | sys::terminal::move_self_to_foreground()?; 53 | } 54 | 55 | if matches!(job.state, jobs::JobState::Stopped) { 56 | // N.B. We use the '\r' to overwrite any ^Z output. 57 | let formatted = job.to_string(); 58 | writeln!(context.stderr(), "\r{formatted}")?; 59 | } 60 | 61 | Ok(builtins::ExitCode::from(result)) 62 | } else { 63 | writeln!(stderr, "{}: no current job", context.command_name)?; 64 | Ok(builtins::ExitCode::Custom(1)) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /brush-core/src/builtins/jobs.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, error, jobs}; 5 | 6 | /// Manage jobs. 7 | #[derive(Parser)] 8 | pub(crate) struct JobsCommand { 9 | /// Also show process IDs. 10 | #[arg(short = 'l')] 11 | also_show_pids: bool, 12 | 13 | /// List only jobs that have changed status since the last notification. 14 | #[arg(short = 'n')] 15 | list_changed_only: bool, 16 | 17 | /// Show only process IDs. 18 | #[arg(short = 'p')] 19 | show_pids_only: bool, 20 | 21 | /// Show only running jobs. 22 | #[arg(short = 'r')] 23 | running_jobs_only: bool, 24 | 25 | /// Show only stopped jobs. 26 | #[arg(short = 's')] 27 | stopped_jobs_only: bool, 28 | 29 | /// Job specs to list. 30 | // TODO: Add -x option 31 | job_specs: Vec, 32 | } 33 | 34 | impl builtins::Command for JobsCommand { 35 | async fn execute( 36 | &self, 37 | context: commands::ExecutionContext<'_>, 38 | ) -> Result { 39 | if self.also_show_pids { 40 | return error::unimp("jobs -l"); 41 | } 42 | if self.list_changed_only { 43 | return error::unimp("jobs -n"); 44 | } 45 | 46 | if self.job_specs.is_empty() { 47 | for job in &context.shell.jobs.jobs { 48 | self.display_job(&context, job)?; 49 | } 50 | } else { 51 | return error::unimp("jobs with job specs"); 52 | } 53 | 54 | Ok(builtins::ExitCode::Success) 55 | } 56 | } 57 | 58 | impl JobsCommand { 59 | fn display_job( 60 | &self, 61 | context: &commands::ExecutionContext<'_>, 62 | job: &jobs::Job, 63 | ) -> Result<(), crate::error::Error> { 64 | if self.running_jobs_only && !matches!(job.state, jobs::JobState::Running) { 65 | return Ok(()); 66 | } 67 | if self.stopped_jobs_only && !matches!(job.state, jobs::JobState::Stopped) { 68 | return Ok(()); 69 | } 70 | 71 | if self.show_pids_only { 72 | if let Some(pid) = job.get_representative_pid() { 73 | writeln!(context.stdout(), "{pid}")?; 74 | } 75 | } else { 76 | writeln!(context.stdout(), "{job}")?; 77 | } 78 | 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /brush-core/src/builtins/let_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{arithmetic::Evaluatable, builtins, commands}; 5 | 6 | /// Evaluate arithmetic expressions. 7 | #[derive(Parser)] 8 | pub(crate) struct LetCommand { 9 | /// Arithmetic expressions to evaluate. 10 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 11 | exprs: Vec, 12 | } 13 | 14 | impl builtins::Command for LetCommand { 15 | async fn execute( 16 | &self, 17 | context: commands::ExecutionContext<'_>, 18 | ) -> Result { 19 | let mut exit_code = builtins::ExitCode::InvalidUsage; 20 | 21 | if self.exprs.is_empty() { 22 | writeln!(context.stderr(), "missing expression")?; 23 | return Ok(exit_code); 24 | } 25 | 26 | for expr in &self.exprs { 27 | let parsed = brush_parser::arithmetic::parse(expr.as_str())?; 28 | let evaluated = parsed.eval(context.shell)?; 29 | 30 | if evaluated == 0 { 31 | exit_code = builtins::ExitCode::Custom(1); 32 | } else { 33 | exit_code = builtins::ExitCode::Custom(0); 34 | } 35 | } 36 | 37 | Ok(exit_code) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /brush-core/src/builtins/popd.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Pop a path from the current directory stack. 7 | #[derive(Parser)] 8 | pub(crate) struct PopdCommand { 9 | /// Pop the path without changing the current working directory. 10 | #[clap(short = 'n')] 11 | no_directory_change: bool, 12 | // 13 | // TODO: implement +N and -N 14 | } 15 | 16 | impl builtins::Command for PopdCommand { 17 | async fn execute( 18 | &self, 19 | context: commands::ExecutionContext<'_>, 20 | ) -> Result { 21 | if let Some(popped) = context.shell.directory_stack.pop() { 22 | if !self.no_directory_change { 23 | context.shell.set_working_dir(&popped)?; 24 | } 25 | 26 | // Display dirs. 27 | let dirs_cmd = crate::builtins::dirs::DirsCommand::default(); 28 | dirs_cmd.execute(context).await?; 29 | } else { 30 | writeln!(context.stderr(), "popd: directory stack empty")?; 31 | return Ok(builtins::ExitCode::Custom(1)); 32 | } 33 | 34 | Ok(builtins::ExitCode::Success) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /brush-core/src/builtins/pushd.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Push a path onto the current directory stack. 6 | #[derive(Parser)] 7 | pub(crate) struct PushdCommand { 8 | /// Push the path without changing the current working directory. 9 | #[clap(short = 'n')] 10 | no_directory_change: bool, 11 | 12 | /// Directory to push on the directory stack. 13 | dir: String, 14 | // 15 | // TODO: implement +N and -N 16 | } 17 | 18 | impl builtins::Command for PushdCommand { 19 | async fn execute( 20 | &self, 21 | context: commands::ExecutionContext<'_>, 22 | ) -> Result { 23 | if self.no_directory_change { 24 | context 25 | .shell 26 | .directory_stack 27 | .push(std::path::PathBuf::from(&self.dir)); 28 | } else { 29 | let prev_working_dir = context.shell.working_dir.clone(); 30 | 31 | let dir = std::path::Path::new(&self.dir); 32 | context.shell.set_working_dir(dir)?; 33 | 34 | context.shell.directory_stack.push(prev_working_dir); 35 | } 36 | 37 | // Display dirs. 38 | let dirs_cmd = crate::builtins::dirs::DirsCommand::default(); 39 | dirs_cmd.execute(context).await?; 40 | 41 | Ok(builtins::ExitCode::Success) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /brush-core/src/builtins/pwd.rs: -------------------------------------------------------------------------------- 1 | use crate::{builtins, commands}; 2 | use clap::Parser; 3 | use std::{borrow::Cow, io::Write, path::Path}; 4 | 5 | /// Display the current working directory. 6 | #[derive(Parser)] 7 | pub(crate) struct PwdCommand { 8 | /// Print the physical directory without any symlinks. 9 | #[arg(short = 'P', overrides_with = "allow_symlinks")] 10 | physical: bool, 11 | 12 | /// Print $PWD if it names the current working directory. 13 | #[arg(short = 'L', overrides_with = "physical")] 14 | allow_symlinks: bool, 15 | } 16 | 17 | impl builtins::Command for PwdCommand { 18 | async fn execute( 19 | &self, 20 | context: commands::ExecutionContext<'_>, 21 | ) -> Result { 22 | let mut cwd: Cow<'_, Path> = context.shell.working_dir.as_path().into(); 23 | 24 | let should_canonicalize = self.physical 25 | || context 26 | .shell 27 | .options 28 | .do_not_resolve_symlinks_when_changing_dir; 29 | 30 | if should_canonicalize { 31 | cwd = cwd.canonicalize()?.into(); 32 | } 33 | 34 | writeln!(context.stdout(), "{}", cwd.to_string_lossy())?; 35 | 36 | Ok(builtins::ExitCode::Success) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /brush-core/src/builtins/return_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Return from the current function. 7 | #[derive(Parser)] 8 | pub(crate) struct ReturnCommand { 9 | /// The exit code to return. 10 | code: Option, 11 | } 12 | 13 | impl builtins::Command for ReturnCommand { 14 | async fn execute( 15 | &self, 16 | context: commands::ExecutionContext<'_>, 17 | ) -> Result { 18 | let code_8bit: u8; 19 | #[allow(clippy::cast_sign_loss)] 20 | if let Some(code_32bit) = &self.code { 21 | code_8bit = (code_32bit & 0xFF) as u8; 22 | } else { 23 | code_8bit = context.shell.last_exit_status; 24 | } 25 | 26 | if context.shell.in_function() || context.shell.in_sourced_script() { 27 | Ok(builtins::ExitCode::ReturnFromFunctionOrScript(code_8bit)) 28 | } else { 29 | writeln!( 30 | context.shell.stderr(), 31 | "return: can only be used in a function or sourced script" 32 | )?; 33 | Ok(builtins::ExitCode::InvalidUsage) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /brush-core/src/builtins/shift.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Shift positional arguments. 6 | #[derive(Parser)] 7 | pub(crate) struct ShiftCommand { 8 | /// Number of positions to shift the arguments by (defaults to 1). 9 | n: Option, 10 | } 11 | 12 | impl builtins::Command for ShiftCommand { 13 | async fn execute( 14 | &self, 15 | context: commands::ExecutionContext<'_>, 16 | ) -> Result { 17 | let n = self.n.unwrap_or(1); 18 | 19 | if n < 0 { 20 | return Ok(builtins::ExitCode::InvalidUsage); 21 | } 22 | 23 | #[allow(clippy::cast_sign_loss)] 24 | let n = n as usize; 25 | 26 | if n > context.shell.positional_parameters.len() { 27 | return Ok(builtins::ExitCode::InvalidUsage); 28 | } 29 | 30 | context.shell.positional_parameters.drain(0..n); 31 | 32 | Ok(builtins::ExitCode::Success) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /brush-core/src/builtins/suspend.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, error}; 5 | 6 | /// Suspend the shell. 7 | #[derive(Parser)] 8 | pub(crate) struct SuspendCommand { 9 | /// Force suspend login shells. 10 | #[arg(short = 'f')] 11 | force: bool, 12 | } 13 | 14 | impl builtins::Command for SuspendCommand { 15 | async fn execute( 16 | &self, 17 | context: commands::ExecutionContext<'_>, 18 | ) -> Result { 19 | if context.shell.options.login_shell && !self.force { 20 | writeln!(context.stderr(), "login shell cannot be suspended")?; 21 | return Ok(builtins::ExitCode::InvalidUsage); 22 | } 23 | 24 | #[allow(clippy::cast_possible_truncation)] 25 | #[allow(clippy::cast_possible_wrap)] 26 | crate::sys::signal::kill_process( 27 | std::process::id() as i32, 28 | crate::traps::TrapSignal::Signal(nix::sys::signal::SIGSTOP), 29 | )?; 30 | 31 | Ok(builtins::ExitCode::Success) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /brush-core/src/builtins/test.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, error, tests, ExecutionParameters, Shell}; 5 | 6 | /// Evaluate test expression. 7 | #[derive(Parser)] 8 | #[clap(disable_help_flag = true, disable_version_flag = true)] 9 | pub(crate) struct TestCommand { 10 | #[clap(allow_hyphen_values = true)] 11 | args: Vec, 12 | } 13 | 14 | impl builtins::Command for TestCommand { 15 | async fn execute( 16 | &self, 17 | context: commands::ExecutionContext<'_>, 18 | ) -> Result { 19 | let mut args = self.args.as_slice(); 20 | 21 | if context.command_name == "[" { 22 | match args.last() { 23 | Some(s) if s == "]" => (), 24 | None | Some(_) => { 25 | writeln!(context.stderr(), "[: missing ']'")?; 26 | return Ok(builtins::ExitCode::InvalidUsage); 27 | } 28 | } 29 | 30 | args = &args[0..args.len() - 1]; 31 | } 32 | 33 | if execute_test(context.shell, &context.params, args)? { 34 | Ok(builtins::ExitCode::Success) 35 | } else { 36 | Ok(builtins::ExitCode::Custom(1)) 37 | } 38 | } 39 | } 40 | 41 | fn execute_test( 42 | shell: &mut Shell, 43 | params: &ExecutionParameters, 44 | args: &[String], 45 | ) -> Result { 46 | let test_command = 47 | brush_parser::test_command::parse(args).map_err(error::Error::TestCommandParseError)?; 48 | tests::eval_test_expr(&test_command, shell, params) 49 | } 50 | -------------------------------------------------------------------------------- /brush-core/src/builtins/times.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, error, timing}; 5 | 6 | /// Report on usage time. 7 | #[derive(Parser)] 8 | pub(crate) struct TimesCommand {} 9 | 10 | impl builtins::Command for TimesCommand { 11 | async fn execute( 12 | &self, 13 | context: commands::ExecutionContext<'_>, 14 | ) -> Result { 15 | let (self_user, self_system) = crate::sys::resource::get_self_user_and_system_time()?; 16 | writeln!( 17 | context.stdout(), 18 | "{} {}", 19 | timing::format_duration_non_posixly(&self_user), 20 | timing::format_duration_non_posixly(&self_system), 21 | )?; 22 | 23 | let (children_user, children_system) = 24 | crate::sys::resource::get_children_user_and_system_time()?; 25 | writeln!( 26 | context.stdout(), 27 | "{} {}", 28 | timing::format_duration_non_posixly(&children_user), 29 | timing::format_duration_non_posixly(&children_system), 30 | )?; 31 | 32 | Ok(builtins::ExitCode::Success) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /brush-core/src/builtins/true_.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use crate::{builtins, commands}; 4 | 5 | /// Return 0. 6 | #[derive(Parser)] 7 | pub(crate) struct TrueCommand {} 8 | 9 | impl builtins::Command for TrueCommand { 10 | async fn execute( 11 | &self, 12 | _context: commands::ExecutionContext<'_>, 13 | ) -> Result { 14 | Ok(builtins::ExitCode::Success) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /brush-core/src/builtins/unalias.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands}; 5 | 6 | /// Unset a shell alias. 7 | #[derive(Parser)] 8 | pub(crate) struct UnaliasCommand { 9 | /// Remove all aliases. 10 | #[arg(short = 'a')] 11 | remove_all: bool, 12 | 13 | /// Names of aliases to operate on. 14 | aliases: Vec, 15 | } 16 | 17 | impl builtins::Command for UnaliasCommand { 18 | async fn execute( 19 | &self, 20 | context: commands::ExecutionContext<'_>, 21 | ) -> Result { 22 | let mut exit_code = builtins::ExitCode::Success; 23 | 24 | if self.remove_all { 25 | context.shell.aliases.clear(); 26 | } else { 27 | for alias in &self.aliases { 28 | if context.shell.aliases.remove(alias).is_none() { 29 | writeln!( 30 | context.stderr(), 31 | "{}: {}: not found", 32 | context.command_name, 33 | alias 34 | )?; 35 | exit_code = builtins::ExitCode::Custom(1); 36 | } 37 | } 38 | } 39 | 40 | Ok(exit_code) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /brush-core/src/builtins/unimp.rs: -------------------------------------------------------------------------------- 1 | use crate::{builtins, commands, trace_categories}; 2 | 3 | use clap::Parser; 4 | 5 | /// (UNIMPLEMENTED COMMAND) 6 | #[derive(Parser)] 7 | pub(crate) struct UnimplementedCommand { 8 | #[clap(allow_hyphen_values = true)] 9 | args: Vec, 10 | 11 | #[clap(skip)] 12 | declarations: Vec, 13 | } 14 | 15 | impl builtins::Command for UnimplementedCommand { 16 | async fn execute( 17 | &self, 18 | context: commands::ExecutionContext<'_>, 19 | ) -> Result { 20 | tracing::warn!(target: trace_categories::UNIMPLEMENTED, 21 | "unimplemented built-in: {} {}", 22 | context.command_name, 23 | self.args.join(" ") 24 | ); 25 | Ok(builtins::ExitCode::Unimplemented) 26 | } 27 | } 28 | 29 | impl builtins::DeclarationCommand for UnimplementedCommand { 30 | fn set_declarations(&mut self, declarations: Vec) { 31 | self.declarations = declarations; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /brush-core/src/builtins/wait.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::Write; 3 | 4 | use crate::{builtins, commands, error}; 5 | 6 | /// Wait for jobs to terminate. 7 | #[derive(Parser)] 8 | pub(crate) struct WaitCommand { 9 | /// Wait for specified job to terminate (instead of change status). 10 | #[arg(short = 'f')] 11 | wait_for_terminate: bool, 12 | 13 | /// Wait for a single job to change status; if jobs are specified, waits for 14 | /// the first to change status, and otherwise waits for the next change. 15 | #[arg(short = 'n')] 16 | wait_for_first_or_next: bool, 17 | 18 | /// Name of variable to receive the job ID of the job whose status is indicated. 19 | #[arg(short = 'p', value_name = "VAR_NAME")] 20 | variable_to_receive_id: Option, 21 | 22 | /// Specs of jobs to wait for. 23 | job_specs: Vec, 24 | } 25 | 26 | impl builtins::Command for WaitCommand { 27 | async fn execute( 28 | &self, 29 | context: commands::ExecutionContext<'_>, 30 | ) -> Result { 31 | if self.wait_for_terminate { 32 | return error::unimp("wait -f"); 33 | } 34 | if self.wait_for_first_or_next { 35 | return error::unimp("wait -n"); 36 | } 37 | if self.variable_to_receive_id.is_some() { 38 | return error::unimp("wait -p"); 39 | } 40 | if !self.job_specs.is_empty() { 41 | return error::unimp("wait with job specs"); 42 | } 43 | 44 | let jobs = context.shell.jobs.wait_all().await?; 45 | 46 | if context.shell.options.enable_job_control { 47 | for job in jobs { 48 | writeln!(context.stdout(), "{job}")?; 49 | } 50 | } 51 | 52 | Ok(builtins::ExitCode::Success) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /brush-core/src/functions.rs: -------------------------------------------------------------------------------- 1 | //! Structures for managing function registrations. 2 | 3 | use std::{collections::HashMap, sync::Arc}; 4 | 5 | /// An environment for defined, named functions. 6 | #[derive(Clone, Default)] 7 | pub struct FunctionEnv { 8 | functions: HashMap, 9 | } 10 | 11 | impl FunctionEnv { 12 | /// Tries to retrieve the registration for a function by name. 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `name` - The name of the function to retrieve. 17 | pub fn get(&self, name: &str) -> Option<&FunctionRegistration> { 18 | self.functions.get(name) 19 | } 20 | 21 | /// Tries to retrieve a mutable reference to the registration for a 22 | /// function by name. 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `name` - The name of the function to retrieve. 27 | pub fn get_mut(&mut self, name: &str) -> Option<&mut FunctionRegistration> { 28 | self.functions.get_mut(name) 29 | } 30 | 31 | /// Unregisters a function from the environment. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `name` - The name of the function to remove. 36 | pub fn remove(&mut self, name: &str) -> Option { 37 | self.functions.remove(name) 38 | } 39 | 40 | /// Updates a function registration in this environment. 41 | /// 42 | /// # Arguments 43 | /// 44 | /// * `name` - The name of the function to update. 45 | /// * `registration` - The new registration for the function. 46 | pub fn update(&mut self, name: String, registration: FunctionRegistration) { 47 | self.functions.insert(name, registration); 48 | } 49 | 50 | /// Returns an iterator over the functions registered in this environment. 51 | pub fn iter(&self) -> impl Iterator { 52 | self.functions.iter() 53 | } 54 | } 55 | 56 | /// Encapsulates a registration for a defined function. 57 | #[derive(Clone)] 58 | pub struct FunctionRegistration { 59 | /// The definition of the function. 60 | pub(crate) definition: Arc, 61 | /// Whether or not this function definition should be exported to children. 62 | exported: bool, 63 | } 64 | 65 | impl From for FunctionRegistration { 66 | fn from(definition: brush_parser::ast::FunctionDefinition) -> Self { 67 | FunctionRegistration { 68 | definition: Arc::new(definition), 69 | exported: false, 70 | } 71 | } 72 | } 73 | 74 | impl FunctionRegistration { 75 | /// Marks the function for export. 76 | pub fn export(&mut self) { 77 | self.exported = true; 78 | } 79 | 80 | /// Unmarks the function for export. 81 | pub fn unexport(&mut self) { 82 | self.exported = false; 83 | } 84 | 85 | /// Returns whether this function is exported. 86 | pub fn is_exported(&self) -> bool { 87 | self.exported 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /brush-core/src/interfaces.rs: -------------------------------------------------------------------------------- 1 | //! Exports traits for shell interfaces implemented by callers. 2 | 3 | mod keybindings; 4 | 5 | pub use keybindings::{InputFunction, Key, KeyAction, KeyBindings, KeySequence, KeyStroke}; 6 | -------------------------------------------------------------------------------- /brush-core/src/keywords.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::Shell; 4 | 5 | fn get_keywords(sh_mode_only: bool) -> HashSet { 6 | let mut keywords = HashSet::new(); 7 | keywords.insert(String::from("!")); 8 | keywords.insert(String::from("{")); 9 | keywords.insert(String::from("}")); 10 | keywords.insert(String::from("case")); 11 | keywords.insert(String::from("do")); 12 | keywords.insert(String::from("done")); 13 | keywords.insert(String::from("elif")); 14 | keywords.insert(String::from("else")); 15 | keywords.insert(String::from("esac")); 16 | keywords.insert(String::from("fi")); 17 | keywords.insert(String::from("for")); 18 | keywords.insert(String::from("if")); 19 | keywords.insert(String::from("in")); 20 | keywords.insert(String::from("then")); 21 | keywords.insert(String::from("until")); 22 | keywords.insert(String::from("while")); 23 | 24 | if !sh_mode_only { 25 | keywords.insert(String::from("[[")); 26 | keywords.insert(String::from("]]")); 27 | keywords.insert(String::from("coproc")); 28 | keywords.insert(String::from("function")); 29 | keywords.insert(String::from("select")); 30 | keywords.insert(String::from("time")); 31 | } 32 | 33 | keywords 34 | } 35 | 36 | lazy_static::lazy_static! { 37 | pub(crate) static ref SH_MODE_KEYWORDS: HashSet = get_keywords(true); 38 | pub(crate) static ref KEYWORDS: HashSet = get_keywords(false); 39 | } 40 | 41 | pub fn is_keyword(shell: &Shell, name: &str) -> bool { 42 | if shell.options.sh_mode { 43 | SH_MODE_KEYWORDS.contains(name) 44 | } else { 45 | KEYWORDS.contains(name) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /brush-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Core implementation of the brush shell. Implements the shell's abstraction, its interpreter, and 2 | //! various facilities used internally by the shell. 3 | 4 | #![deny(missing_docs)] 5 | 6 | pub mod completion; 7 | 8 | mod arithmetic; 9 | pub mod builtins; 10 | mod commands; 11 | pub mod env; 12 | mod error; 13 | mod escape; 14 | mod expansion; 15 | mod extendedtests; 16 | pub mod functions; 17 | pub mod interfaces; 18 | mod interp; 19 | mod jobs; 20 | mod keywords; 21 | mod namedoptions; 22 | mod openfiles; 23 | pub mod options; 24 | mod pathcache; 25 | mod patterns; 26 | mod processes; 27 | mod prompt; 28 | mod regex; 29 | mod shell; 30 | mod sys; 31 | mod terminal; 32 | mod tests; 33 | mod timing; 34 | mod trace_categories; 35 | pub mod traps; 36 | pub mod variables; 37 | 38 | pub use arithmetic::EvalError; 39 | pub use commands::{CommandArg, ExecutionContext}; 40 | pub use error::Error; 41 | pub use interp::{ExecutionParameters, ExecutionResult, ProcessGroupPolicy}; 42 | pub use shell::{CreateOptions, Shell}; 43 | pub use terminal::TerminalControl; 44 | pub use variables::{ShellValue, ShellVariable}; 45 | -------------------------------------------------------------------------------- /brush-core/src/pathcache.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, variables}; 2 | use std::path::PathBuf; 3 | 4 | /// A cache of paths associated with names. 5 | #[derive(Clone, Default)] 6 | pub struct PathCache { 7 | /// The cache itself. 8 | cache: std::collections::HashMap, 9 | } 10 | 11 | impl PathCache { 12 | /// Clears all elements from the cache. 13 | pub fn reset(&mut self) { 14 | self.cache.clear(); 15 | } 16 | 17 | /// Returns the path associated with the given name. 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `name` - The name to lookup. 22 | pub fn get>(&self, name: S) -> Option { 23 | self.cache.get(name.as_ref()).cloned() 24 | } 25 | 26 | /// Sets the path associated with the given name. 27 | /// 28 | /// # Arguments 29 | /// 30 | /// * `name` - The name to set. 31 | pub fn set>(&mut self, name: S, path: PathBuf) { 32 | self.cache.insert(name.as_ref().to_string(), path); 33 | } 34 | 35 | /// Projects the cache into a shell value. 36 | pub fn to_value(&self) -> Result { 37 | let pairs = self 38 | .cache 39 | .iter() 40 | .map(|(k, v)| (Some(k.to_owned()), v.to_string_lossy().to_string())) 41 | .collect::>(); 42 | 43 | variables::ShellValue::associative_array_from_literals(variables::ArrayLiteral(pairs)) 44 | } 45 | 46 | /// Removes the path associated with the given name, if there is one. 47 | /// Returns whether or not an entry was removed. 48 | /// 49 | /// # Arguments 50 | /// 51 | /// * `name` - The name to remove. 52 | pub fn unset>(&mut self, name: S) -> bool { 53 | self.cache.remove(name.as_ref()).is_some() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /brush-core/src/processes.rs: -------------------------------------------------------------------------------- 1 | use futures::FutureExt; 2 | 3 | use crate::{error, sys}; 4 | 5 | /// A waitable future that will yield the results of a child process's execution. 6 | pub(crate) type WaitableChildProcess = std::pin::Pin< 7 | Box> + Send + Sync>, 8 | >; 9 | 10 | /// Tracks a child process being awaited. 11 | pub(crate) struct ChildProcess { 12 | /// If available, the process ID of the child. 13 | pid: Option, 14 | /// A waitable future that will yield the results of a child process's execution. 15 | exec_future: WaitableChildProcess, 16 | } 17 | 18 | impl ChildProcess { 19 | /// Wraps a child process and its future. 20 | pub fn new(pid: Option, child: sys::process::Child) -> Self { 21 | Self { 22 | pid, 23 | exec_future: Box::pin(child.wait_with_output()), 24 | } 25 | } 26 | 27 | pub fn pid(&self) -> Option { 28 | self.pid 29 | } 30 | 31 | pub async fn wait(&mut self) -> Result { 32 | #[allow(unused_mut)] 33 | let mut sigtstp = sys::signal::tstp_signal_listener()?; 34 | #[allow(unused_mut)] 35 | let mut sigchld = sys::signal::chld_signal_listener()?; 36 | 37 | loop { 38 | tokio::select! { 39 | output = &mut self.exec_future => { 40 | break Ok(ProcessWaitResult::Completed(output?)) 41 | }, 42 | _ = sigtstp.recv() => { 43 | break Ok(ProcessWaitResult::Stopped) 44 | }, 45 | _ = sigchld.recv() => { 46 | if sys::signal::poll_for_stopped_children()? { 47 | break Ok(ProcessWaitResult::Stopped); 48 | } 49 | }, 50 | _ = sys::signal::await_ctrl_c() => { 51 | // SIGINT got thrown. Handle it and continue looping. The child should 52 | // have received it as well, and either handled it or ended up getting 53 | // terminated (in which case we'll see the child exit). 54 | }, 55 | } 56 | } 57 | } 58 | 59 | pub(crate) fn poll(&mut self) -> Option> { 60 | let checkable_future = &mut self.exec_future; 61 | checkable_future 62 | .now_or_never() 63 | .map(|result| result.map_err(Into::into)) 64 | } 65 | } 66 | 67 | /// Represents the result of waiting for an executing process. 68 | pub(crate) enum ProcessWaitResult { 69 | /// The process completed. 70 | Completed(std::process::Output), 71 | /// The process stopped and has not yet completed. 72 | Stopped, 73 | } 74 | -------------------------------------------------------------------------------- /brush-core/src/sys.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | pub(crate) mod unix; 3 | #[cfg(unix)] 4 | pub(crate) use unix as platform; 5 | 6 | #[cfg(windows)] 7 | pub(crate) mod windows; 8 | #[cfg(windows)] 9 | pub(crate) use windows as platform; 10 | 11 | #[cfg(target_family = "wasm")] 12 | pub(crate) mod wasm; 13 | #[cfg(target_family = "wasm")] 14 | pub(crate) use wasm as platform; 15 | 16 | #[cfg(not(unix))] 17 | pub(crate) mod stubs; 18 | 19 | #[cfg(any(unix, windows))] 20 | pub(crate) mod hostname; 21 | #[cfg(any(unix, windows))] 22 | pub(crate) mod os_pipe; 23 | #[cfg(any(unix, windows))] 24 | pub(crate) mod tokio_process; 25 | 26 | pub(crate) mod fs; 27 | 28 | pub(crate) use platform::input; 29 | pub(crate) use platform::network; 30 | pub(crate) use platform::pipes; 31 | pub(crate) use platform::process; 32 | pub(crate) use platform::resource; 33 | pub(crate) use platform::signal; 34 | pub(crate) use platform::terminal; 35 | pub(crate) use platform::users; 36 | -------------------------------------------------------------------------------- /brush-core/src/sys/fs.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | pub(crate) use super::platform::fs::*; 3 | 4 | #[cfg(unix)] 5 | pub(crate) use std::os::unix::fs::MetadataExt; 6 | #[cfg(not(unix))] 7 | pub(crate) use StubMetadataExt as MetadataExt; 8 | 9 | pub(crate) trait PathExt { 10 | fn readable(&self) -> bool; 11 | fn writable(&self) -> bool; 12 | fn executable(&self) -> bool; 13 | 14 | fn exists_and_is_block_device(&self) -> bool; 15 | fn exists_and_is_char_device(&self) -> bool; 16 | fn exists_and_is_fifo(&self) -> bool; 17 | fn exists_and_is_socket(&self) -> bool; 18 | fn exists_and_is_setgid(&self) -> bool; 19 | fn exists_and_is_setuid(&self) -> bool; 20 | fn exists_and_is_sticky_bit(&self) -> bool; 21 | 22 | fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error>; 23 | } 24 | -------------------------------------------------------------------------------- /brush-core/src/sys/hostname.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn get() -> std::io::Result { 2 | hostname::get() 3 | } 4 | -------------------------------------------------------------------------------- /brush-core/src/sys/os_pipe.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use os_pipe::{pipe, PipeReader, PipeWriter}; 2 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(clippy::needless_pass_by_value)] 3 | #![allow(clippy::unused_async)] 4 | #![allow(clippy::unused_self)] 5 | #![allow(clippy::unnecessary_wraps)] 6 | 7 | pub(crate) mod fs; 8 | pub(crate) mod input; 9 | pub(crate) mod network; 10 | pub(crate) mod pipes; 11 | pub(crate) mod process; 12 | pub(crate) mod resource; 13 | pub(crate) mod signal; 14 | pub(crate) mod terminal; 15 | pub(crate) mod users; 16 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/fs.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(unix))] 2 | impl crate::sys::fs::PathExt for std::path::Path { 3 | fn readable(&self) -> bool { 4 | true 5 | } 6 | 7 | fn writable(&self) -> bool { 8 | true 9 | } 10 | 11 | fn executable(&self) -> bool { 12 | true 13 | } 14 | 15 | fn exists_and_is_block_device(&self) -> bool { 16 | false 17 | } 18 | 19 | fn exists_and_is_char_device(&self) -> bool { 20 | false 21 | } 22 | 23 | fn exists_and_is_fifo(&self) -> bool { 24 | false 25 | } 26 | 27 | fn exists_and_is_socket(&self) -> bool { 28 | false 29 | } 30 | 31 | fn exists_and_is_setgid(&self) -> bool { 32 | false 33 | } 34 | 35 | fn exists_and_is_setuid(&self) -> bool { 36 | false 37 | } 38 | 39 | fn exists_and_is_sticky_bit(&self) -> bool { 40 | false 41 | } 42 | 43 | fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> { 44 | Ok((0, 0)) 45 | } 46 | } 47 | 48 | pub(crate) trait StubMetadataExt { 49 | fn gid(&self) -> u32 { 50 | 0 51 | } 52 | 53 | fn uid(&self) -> u32 { 54 | 0 55 | } 56 | } 57 | 58 | impl StubMetadataExt for std::fs::Metadata {} 59 | 60 | pub(crate) fn get_default_executable_search_paths() -> Vec { 61 | vec![] 62 | } 63 | 64 | pub(crate) fn get_default_standard_utils_paths() -> Vec { 65 | vec![] 66 | } 67 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/input.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, interfaces}; 2 | 3 | pub(crate) fn get_key_from_key_code(_key_code: &[u8]) -> Result { 4 | error::unimp("get_key_from_key_code") 5 | } 6 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/network.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn get_hostname() -> std::io::Result { 2 | Ok("".into()) 3 | } 4 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/pipes.rs: -------------------------------------------------------------------------------- 1 | /// Stub implementation of a pipe reader. 2 | #[derive(Clone)] 3 | pub(crate) struct PipeReader {} 4 | 5 | impl PipeReader { 6 | /// Tries to clone the reader. 7 | pub fn try_clone(&self) -> std::io::Result { 8 | Ok((*self).clone()) 9 | } 10 | } 11 | 12 | impl From for std::process::Stdio { 13 | fn from(_reader: PipeReader) -> Self { 14 | std::process::Stdio::null() 15 | } 16 | } 17 | 18 | impl std::io::Read for PipeReader { 19 | fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { 20 | // TODO: implement 21 | Ok(0) 22 | } 23 | } 24 | 25 | /// Stub implementation o a pipe writer. 26 | #[derive(Clone)] 27 | pub(crate) struct PipeWriter {} 28 | 29 | impl PipeWriter { 30 | /// Tries to clone the writer. 31 | pub fn try_clone(&self) -> std::io::Result { 32 | Ok((*self).clone()) 33 | } 34 | } 35 | 36 | impl From for std::process::Stdio { 37 | fn from(_writer: PipeWriter) -> Self { 38 | std::process::Stdio::null() 39 | } 40 | } 41 | 42 | impl std::io::Write for PipeWriter { 43 | fn write(&mut self, _buf: &[u8]) -> std::io::Result { 44 | // TODO: implement 45 | Ok(0) 46 | } 47 | 48 | fn flush(&mut self) -> std::io::Result<()> { 49 | Ok(()) 50 | } 51 | } 52 | 53 | pub(crate) fn pipe() -> std::io::Result<(PipeReader, PipeWriter)> { 54 | Ok((PipeReader {}, PipeWriter {})) 55 | } 56 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/process.rs: -------------------------------------------------------------------------------- 1 | pub(crate) type ProcessId = i32; 2 | 3 | pub(crate) struct Child { 4 | inner: std::process::Child, 5 | } 6 | 7 | pub(crate) use std::process::ExitStatus; 8 | pub(crate) use std::process::Output; 9 | 10 | impl Child { 11 | pub fn id(&self) -> Option { 12 | None 13 | } 14 | 15 | pub async fn wait(&mut self) -> std::io::Result { 16 | self.inner.wait() 17 | } 18 | 19 | pub async fn wait_with_output(self) -> std::io::Result { 20 | self.inner.wait_with_output() 21 | } 22 | } 23 | 24 | pub(crate) fn spawn(mut command: std::process::Command) -> std::io::Result { 25 | let child = command.spawn()?; 26 | Ok(Child { inner: child }) 27 | } 28 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | 3 | #[allow(clippy::unnecessary_wraps)] 4 | pub(crate) fn get_self_user_and_system_time( 5 | ) -> Result<(std::time::Duration, std::time::Duration), error::Error> { 6 | Ok((std::time::Duration::ZERO, std::time::Duration::ZERO)) 7 | } 8 | 9 | #[allow(clippy::unnecessary_wraps)] 10 | pub(crate) fn get_children_user_and_system_time( 11 | ) -> Result<(std::time::Duration, std::time::Duration), error::Error> { 12 | Ok((std::time::Duration::ZERO, std::time::Duration::ZERO)) 13 | } 14 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/signal.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, sys, traps}; 2 | 3 | pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> { 4 | error::unimp("continue process") 5 | } 6 | 7 | pub(crate) fn kill_process( 8 | _pid: sys::process::ProcessId, 9 | _signal: traps::TrapSignal, 10 | ) -> Result<(), error::Error> { 11 | error::unimp("kill process") 12 | } 13 | 14 | pub(crate) fn lead_new_process_group() -> Result<(), error::Error> { 15 | Ok(()) 16 | } 17 | 18 | pub(crate) struct FakeSignal {} 19 | 20 | impl FakeSignal { 21 | fn new() -> Self { 22 | Self {} 23 | } 24 | 25 | pub async fn recv(&self) { 26 | futures::future::pending::<()>().await; 27 | } 28 | } 29 | 30 | pub(crate) fn tstp_signal_listener() -> Result { 31 | Ok(FakeSignal::new()) 32 | } 33 | 34 | pub(crate) fn chld_signal_listener() -> Result { 35 | Ok(FakeSignal::new()) 36 | } 37 | 38 | pub(crate) async fn await_ctrl_c() -> std::io::Result<()> { 39 | FakeSignal::new().recv().await; 40 | Ok(()) 41 | } 42 | 43 | pub(crate) fn mask_sigttou() -> Result<(), error::Error> { 44 | Ok(()) 45 | } 46 | 47 | pub(crate) fn poll_for_stopped_children() -> Result { 48 | Ok(false) 49 | } 50 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/terminal.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, sys}; 2 | 3 | #[derive(Clone)] 4 | pub(crate) struct TerminalSettings {} 5 | 6 | impl TerminalSettings { 7 | pub fn set_canonical(&mut self, _value: bool) {} 8 | pub fn set_echo(&mut self, _value: bool) {} 9 | pub fn set_int_signal(&mut self, _value: bool) {} 10 | } 11 | 12 | pub(crate) fn get_term_attr(_fd: Fd) -> Result { 13 | Ok(TerminalSettings {}) 14 | } 15 | 16 | pub(crate) fn set_term_attr_now( 17 | _fd: Fd, 18 | _settings: &TerminalSettings, 19 | ) -> Result<(), error::Error> { 20 | Ok(()) 21 | } 22 | 23 | pub(crate) fn get_parent_process_id() -> Option { 24 | None 25 | } 26 | 27 | pub(crate) fn get_process_group_id() -> Option { 28 | None 29 | } 30 | 31 | pub(crate) fn get_foreground_pid() -> Option { 32 | None 33 | } 34 | 35 | pub(crate) fn move_to_foreground(_pid: sys::process::ProcessId) -> Result<(), error::Error> { 36 | Ok(()) 37 | } 38 | 39 | pub(crate) fn move_self_to_foreground() -> Result<(), error::Error> { 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /brush-core/src/sys/stubs/users.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | use std::path::PathBuf; 3 | 4 | pub(crate) fn get_user_home_dir(_username: &str) -> Option { 5 | None 6 | } 7 | 8 | pub(crate) fn get_current_user_home_dir() -> Option { 9 | None 10 | } 11 | 12 | pub(crate) fn is_root() -> bool { 13 | false 14 | } 15 | 16 | pub(crate) fn get_effective_uid() -> Result { 17 | error::unimp("get effective uid") 18 | } 19 | 20 | pub(crate) fn get_effective_gid() -> Result { 21 | error::unimp("get effective gid") 22 | } 23 | 24 | pub(crate) fn get_current_username() -> Result { 25 | error::unimp("get current username") 26 | } 27 | 28 | pub(crate) fn get_user_group_ids() -> Result, error::Error> { 29 | Ok(vec![]) 30 | } 31 | 32 | pub(crate) fn get_all_users() -> Result, error::Error> { 33 | Ok(vec![]) 34 | } 35 | 36 | pub(crate) fn get_all_groups() -> Result, error::Error> { 37 | Ok(vec![]) 38 | } 39 | -------------------------------------------------------------------------------- /brush-core/src/sys/tokio_process.rs: -------------------------------------------------------------------------------- 1 | pub(crate) type ProcessId = i32; 2 | pub(crate) use tokio::process::Child; 3 | 4 | pub(crate) fn spawn(command: std::process::Command) -> std::io::Result { 5 | let mut command = tokio::process::Command::from(command); 6 | command.spawn() 7 | } 8 | -------------------------------------------------------------------------------- /brush-core/src/sys/unix.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use crate::sys::os_pipe as pipes; 2 | pub(crate) mod fs; 3 | pub(crate) mod input; 4 | pub(crate) mod network; 5 | pub(crate) use crate::sys::tokio_process as process; 6 | pub(crate) mod resource; 7 | pub(crate) mod signal; 8 | pub(crate) mod terminal; 9 | pub(crate) mod users; 10 | -------------------------------------------------------------------------------- /brush-core/src/sys/unix/network.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn get_hostname() -> std::io::Result { 2 | crate::sys::hostname::get() 3 | } 4 | -------------------------------------------------------------------------------- /brush-core/src/sys/unix/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | 3 | #[allow(clippy::unnecessary_wraps)] 4 | pub(crate) fn get_self_user_and_system_time( 5 | ) -> Result<(std::time::Duration, std::time::Duration), error::Error> { 6 | let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF)?; 7 | Ok(( 8 | convert_rusage_time(usage.user_time()), 9 | convert_rusage_time(usage.system_time()), 10 | )) 11 | } 12 | 13 | #[allow(clippy::unnecessary_wraps)] 14 | pub(crate) fn get_children_user_and_system_time( 15 | ) -> Result<(std::time::Duration, std::time::Duration), error::Error> { 16 | let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_CHILDREN)?; 17 | Ok(( 18 | convert_rusage_time(usage.user_time()), 19 | convert_rusage_time(usage.system_time()), 20 | )) 21 | } 22 | 23 | fn convert_rusage_time(time: nix::sys::time::TimeVal) -> std::time::Duration { 24 | #[allow(clippy::cast_sign_loss)] 25 | #[allow(clippy::cast_possible_truncation)] 26 | std::time::Duration::new(time.tv_sec() as u64, time.tv_usec() as u32 * 1000) 27 | } 28 | -------------------------------------------------------------------------------- /brush-core/src/sys/unix/terminal.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, sys}; 2 | use std::{io::IsTerminal, os::fd::AsFd}; 3 | 4 | #[derive(Clone)] 5 | pub(crate) struct TerminalSettings { 6 | termios: nix::sys::termios::Termios, 7 | } 8 | 9 | impl TerminalSettings { 10 | pub fn set_canonical(&mut self, value: bool) { 11 | self.set_local_flag(nix::sys::termios::LocalFlags::ICANON, value); 12 | } 13 | 14 | pub fn set_echo(&mut self, value: bool) { 15 | self.set_local_flag(nix::sys::termios::LocalFlags::ICANON, value); 16 | } 17 | 18 | pub fn set_int_signal(&mut self, value: bool) { 19 | self.set_local_flag(nix::sys::termios::LocalFlags::ISIG, value); 20 | } 21 | 22 | fn set_local_flag(&mut self, flag: nix::sys::termios::LocalFlags, value: bool) { 23 | if value { 24 | self.termios.local_flags.insert(flag); 25 | } else { 26 | self.termios.local_flags.remove(flag); 27 | } 28 | } 29 | } 30 | 31 | pub(crate) fn get_term_attr(fd: Fd) -> Result { 32 | Ok(TerminalSettings { 33 | termios: nix::sys::termios::tcgetattr(fd)?, 34 | }) 35 | } 36 | 37 | pub(crate) fn set_term_attr_now( 38 | fd: Fd, 39 | settings: &TerminalSettings, 40 | ) -> Result<(), error::Error> { 41 | nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &settings.termios)?; 42 | Ok(()) 43 | } 44 | 45 | #[allow(clippy::unnecessary_wraps)] 46 | pub(crate) fn get_parent_process_id() -> Option { 47 | Some(nix::unistd::getppid().as_raw()) 48 | } 49 | 50 | #[allow(clippy::unnecessary_wraps)] 51 | pub(crate) fn get_process_group_id() -> Option { 52 | Some(nix::unistd::getpgrp().as_raw()) 53 | } 54 | 55 | pub(crate) fn get_foreground_pid() -> Option { 56 | nix::unistd::tcgetpgrp(std::io::stdin()) 57 | .ok() 58 | .map(|pgid| pgid.as_raw()) 59 | } 60 | 61 | pub(crate) fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> { 62 | nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?; 63 | Ok(()) 64 | } 65 | 66 | pub(crate) fn move_self_to_foreground() -> Result<(), error::Error> { 67 | if std::io::stdin().is_terminal() { 68 | let pgid = nix::unistd::getpgid(None)?; 69 | 70 | // TODO: jobs: This sometimes fails with ENOTTY even though we checked that stdin is a 71 | // terminal. We should investigate why this is happening. 72 | let _ = nix::unistd::tcsetpgrp(std::io::stdin(), pgid); 73 | } 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /brush-core/src/sys/unix/users.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, trace_categories}; 2 | use std::path::PathBuf; 3 | 4 | use uzers::os::unix::UserExt; 5 | 6 | pub(crate) fn is_root() -> bool { 7 | uzers::get_current_uid() == 0 8 | } 9 | 10 | pub(crate) fn get_user_home_dir(username: &str) -> Option { 11 | if let Some(user_info) = uzers::get_user_by_name(username) { 12 | return Some(user_info.home_dir().to_path_buf()); 13 | } 14 | 15 | None 16 | } 17 | 18 | pub(crate) fn get_current_user_home_dir() -> Option { 19 | if let Some(username) = uzers::get_current_username() { 20 | if let Some(user_info) = uzers::get_user_by_name(&username) { 21 | return Some(user_info.home_dir().to_path_buf()); 22 | } 23 | } 24 | 25 | None 26 | } 27 | 28 | #[allow(clippy::unnecessary_wraps)] 29 | pub(crate) fn get_effective_uid() -> Result { 30 | Ok(uzers::get_effective_uid()) 31 | } 32 | 33 | #[allow(clippy::unnecessary_wraps)] 34 | pub(crate) fn get_effective_gid() -> Result { 35 | Ok(uzers::get_effective_gid()) 36 | } 37 | 38 | pub(crate) fn get_current_username() -> Result { 39 | let username = uzers::get_current_username().ok_or_else(|| error::Error::NoCurrentUser)?; 40 | Ok(username.to_string_lossy().to_string()) 41 | } 42 | 43 | pub(crate) fn get_user_group_ids() -> Result, error::Error> { 44 | let username = uzers::get_current_username().ok_or_else(|| error::Error::NoCurrentUser)?; 45 | let gid = uzers::get_current_gid(); 46 | let groups = uzers::get_user_groups(&username, gid).unwrap_or_default(); 47 | Ok(groups.into_iter().map(|g| g.gid()).collect()) 48 | } 49 | 50 | #[allow(clippy::unnecessary_wraps)] 51 | pub(crate) fn get_all_users() -> Result, error::Error> { 52 | // TODO(#475): uzers::all_users() is available but unsafe 53 | tracing::debug!(target: trace_categories::UNIMPLEMENTED, "get_all_users"); 54 | Ok(vec![]) 55 | } 56 | 57 | #[allow(clippy::unnecessary_wraps)] 58 | pub(crate) fn get_all_groups() -> Result, error::Error> { 59 | // TODO(#475): uzers::all_groups() is available but unsafe 60 | tracing::debug!(target: trace_categories::UNIMPLEMENTED, "get_all_groups"); 61 | Ok(vec![]) 62 | } 63 | -------------------------------------------------------------------------------- /brush-core/src/sys/wasm.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use crate::sys::stubs::fs; 2 | pub(crate) use crate::sys::stubs::input; 3 | pub(crate) use crate::sys::stubs::network; 4 | pub(crate) use crate::sys::stubs::pipes; 5 | pub(crate) use crate::sys::stubs::process; 6 | pub(crate) use crate::sys::stubs::resource; 7 | pub(crate) use crate::sys::stubs::signal; 8 | pub(crate) use crate::sys::stubs::terminal; 9 | pub(crate) use crate::sys::stubs::users; 10 | -------------------------------------------------------------------------------- /brush-core/src/sys/windows.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use crate::sys::os_pipe as pipes; 2 | pub(crate) use crate::sys::stubs::fs; 3 | pub(crate) use crate::sys::stubs::input; 4 | pub(crate) mod network; 5 | pub(crate) use crate::sys::stubs::resource; 6 | 7 | pub(crate) mod signal { 8 | pub(crate) use crate::sys::stubs::signal::*; 9 | pub(crate) use tokio::signal::ctrl_c as await_ctrl_c; 10 | } 11 | 12 | pub(crate) use crate::sys::stubs::terminal; 13 | pub(crate) use crate::sys::tokio_process as process; 14 | pub(crate) mod users; 15 | -------------------------------------------------------------------------------- /brush-core/src/sys/windows/network.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn get_hostname() -> std::io::Result { 2 | crate::sys::hostname::get() 3 | } 4 | -------------------------------------------------------------------------------- /brush-core/src/sys/windows/users.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | use std::path::PathBuf; 3 | 4 | // 5 | // Non-Unix implementation 6 | // 7 | 8 | pub(crate) fn get_user_home_dir(username: &str) -> Option { 9 | homedir::home(username).unwrap_or_default() 10 | } 11 | 12 | pub(crate) fn get_current_user_home_dir() -> Option { 13 | homedir::my_home().unwrap_or_default() 14 | } 15 | 16 | pub(crate) fn is_root() -> bool { 17 | // TODO: implement some version of this for Windows 18 | false 19 | } 20 | 21 | pub(crate) fn get_effective_uid() -> Result { 22 | error::unimp("get effective uid") 23 | } 24 | 25 | pub(crate) fn get_effective_gid() -> Result { 26 | error::unimp("get effective gid") 27 | } 28 | 29 | pub(crate) fn get_current_username() -> Result { 30 | let username = whoami::fallible::username()?; 31 | Ok(username) 32 | } 33 | 34 | pub(crate) fn get_user_group_ids() -> Result, error::Error> { 35 | // TODO: implement some version of this for Windows 36 | Ok(vec![]) 37 | } 38 | 39 | #[allow(clippy::unnecessary_wraps)] 40 | pub(crate) fn get_all_users() -> Result, error::Error> { 41 | // TODO: implement some version of this for Windows 42 | Ok(vec![]) 43 | } 44 | 45 | #[allow(clippy::unnecessary_wraps)] 46 | pub(crate) fn get_all_groups() -> Result, error::Error> { 47 | // TODO: implement some version of this for Windows 48 | Ok(vec![]) 49 | } 50 | -------------------------------------------------------------------------------- /brush-core/src/terminal.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, sys}; 2 | 3 | /// Encapsulates the state of a controlled terminal. 4 | #[allow(clippy::module_name_repetitions)] 5 | pub struct TerminalControl { 6 | prev_fg_pid: Option, 7 | } 8 | 9 | impl TerminalControl { 10 | /// Acquire the terminal for the shell. 11 | pub fn acquire() -> Result { 12 | let prev_fg_pid = sys::terminal::get_foreground_pid(); 13 | 14 | // Break out into new process group. 15 | // TODO: jobs: Investigate why this sometimes fails with EPERM. 16 | let _ = sys::signal::lead_new_process_group(); 17 | 18 | // Take ownership. 19 | sys::terminal::move_self_to_foreground()?; 20 | 21 | // Mask out SIGTTOU. 22 | sys::signal::mask_sigttou()?; 23 | 24 | Ok(Self { prev_fg_pid }) 25 | } 26 | 27 | fn try_release(&mut self) { 28 | // Restore the previous foreground process group. 29 | if let Some(pid) = self.prev_fg_pid { 30 | if sys::terminal::move_to_foreground(pid).is_ok() { 31 | self.prev_fg_pid = None; 32 | } 33 | } 34 | } 35 | } 36 | 37 | impl Drop for TerminalControl { 38 | fn drop(&mut self) { 39 | self.try_release(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /brush-core/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, extendedtests, ExecutionParameters, Shell}; 2 | 3 | pub(crate) fn eval_test_expr( 4 | expr: &brush_parser::ast::TestExpr, 5 | shell: &mut Shell, 6 | params: &ExecutionParameters, 7 | ) -> Result { 8 | match expr { 9 | brush_parser::ast::TestExpr::False => Ok(false), 10 | brush_parser::ast::TestExpr::Literal(s) => Ok(!s.is_empty()), 11 | brush_parser::ast::TestExpr::And(left, right) => { 12 | Ok(eval_test_expr(left, shell, params)? && eval_test_expr(right, shell, params)?) 13 | } 14 | brush_parser::ast::TestExpr::Or(left, right) => { 15 | Ok(eval_test_expr(left, shell, params)? || eval_test_expr(right, shell, params)?) 16 | } 17 | brush_parser::ast::TestExpr::Not(expr) => Ok(!eval_test_expr(expr, shell, params)?), 18 | brush_parser::ast::TestExpr::Parenthesized(expr) => eval_test_expr(expr, shell, params), 19 | brush_parser::ast::TestExpr::UnaryTest(op, operand) => { 20 | extendedtests::apply_unary_predicate_to_str(op, operand, shell, params) 21 | } 22 | brush_parser::ast::TestExpr::BinaryTest(op, left, right) => { 23 | extendedtests::apply_binary_predicate_to_strs(op, left.as_str(), right.as_str(), shell) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /brush-core/src/trace_categories.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const COMMANDS: &str = "commands"; 2 | pub(crate) const COMPLETION: &str = "completion"; 3 | pub(crate) const EXPANSION: &str = "expansion"; 4 | pub(crate) const FUNCTIONS: &str = "functions"; 5 | pub(crate) const INPUT: &str = "input"; 6 | pub(crate) const JOBS: &str = "jobs"; 7 | pub(crate) const PARSE: &str = "parse"; 8 | pub(crate) const PATTERN: &str = "pattern"; 9 | pub(crate) const UNIMPLEMENTED: &str = "unimplemented"; 10 | -------------------------------------------------------------------------------- /brush-interactive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brush-interactive" 3 | description = "Interactive layer of brush-shell" 4 | version = "0.2.18" 5 | authors.workspace = true 6 | categories.workspace = true 7 | edition.workspace = true 8 | keywords.workspace = true 9 | license.workspace = true 10 | readme.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | 14 | [lib] 15 | bench = false 16 | 17 | [features] 18 | default = [] 19 | basic = ["dep:crossterm"] 20 | minimal = [] 21 | reedline = ["dep:reedline", "dep:nu-ansi-term"] 22 | 23 | [lints] 24 | workspace = true 25 | 26 | [dependencies] 27 | async-trait = "0.1.88" 28 | brush-parser = { version = "^0.2.16", path = "../brush-parser" } 29 | brush-core = { version = "^0.3.1", path = "../brush-core" } 30 | crossterm = { version = "0.29.0", features = ["serde"], optional = true } 31 | indexmap = "2.9.0" 32 | nu-ansi-term = { version = "0.50.1", optional = true } 33 | reedline = { version = "0.40.0", optional = true } 34 | thiserror = "2.0.12" 35 | tracing = "0.1.41" 36 | 37 | [target.'cfg(any(windows, unix))'.dependencies] 38 | tokio = { version = "1.45.1", features = ["macros", "signal"] } 39 | 40 | [target.wasm32-unknown-unknown.dependencies] 41 | getrandom = { version = "0.3.3", features = ["wasm_js"] } 42 | -------------------------------------------------------------------------------- /brush-interactive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /brush-interactive/src/basic/mod.rs: -------------------------------------------------------------------------------- 1 | mod basic_shell; 2 | mod raw_mode; 3 | mod term_line_reader; 4 | 5 | #[allow(clippy::module_name_repetitions)] 6 | pub use basic_shell::BasicShell; 7 | -------------------------------------------------------------------------------- /brush-interactive/src/basic/raw_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::ShellError; 2 | 3 | pub(crate) struct RawModeToggle { 4 | initial: bool, 5 | } 6 | 7 | impl RawModeToggle { 8 | pub fn new() -> Result { 9 | let initial = crossterm::terminal::is_raw_mode_enabled()?; 10 | Ok(Self { initial }) 11 | } 12 | 13 | #[allow(clippy::unused_self)] 14 | pub fn enable(&self) -> Result<(), ShellError> { 15 | crossterm::terminal::enable_raw_mode()?; 16 | Ok(()) 17 | } 18 | 19 | #[allow(clippy::unused_self)] 20 | pub fn disable(&self) -> Result<(), ShellError> { 21 | crossterm::terminal::disable_raw_mode()?; 22 | Ok(()) 23 | } 24 | } 25 | 26 | impl Drop for RawModeToggle { 27 | fn drop(&mut self) { 28 | let _ = if self.initial { 29 | crossterm::terminal::enable_raw_mode() 30 | } else { 31 | crossterm::terminal::disable_raw_mode() 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /brush-interactive/src/completion.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use indexmap::IndexSet; 4 | 5 | use crate::trace_categories; 6 | 7 | #[allow(dead_code)] 8 | pub(crate) async fn complete_async( 9 | shell: &mut brush_core::Shell, 10 | line: &str, 11 | pos: usize, 12 | ) -> brush_core::completion::Completions { 13 | let working_dir = shell.working_dir.clone(); 14 | 15 | // Intentionally ignore any errors that arise. 16 | let completion_future = shell.get_completions(line, pos); 17 | tokio::pin!(completion_future); 18 | 19 | // Wait for the completions to come back or interruption, whichever happens first. 20 | let result = tokio::select! { 21 | result = &mut completion_future => { 22 | result 23 | } 24 | _ = tokio::signal::ctrl_c() => { 25 | Err(brush_core::Error::Interrupted) 26 | }, 27 | }; 28 | 29 | let mut completions = result.unwrap_or_else(|_| brush_core::completion::Completions { 30 | insertion_index: pos, 31 | delete_count: 0, 32 | candidates: IndexSet::new(), 33 | options: brush_core::completion::ProcessingOptions::default(), 34 | }); 35 | 36 | // TODO: Consider optimizing this out when not needed? 37 | let completing_end_of_line = pos == line.len(); 38 | completions.candidates = completions 39 | .candidates 40 | .into_iter() 41 | .map(|candidate| { 42 | postprocess_completion_candidate( 43 | candidate, 44 | &completions.options, 45 | working_dir.as_ref(), 46 | completing_end_of_line, 47 | ) 48 | }) 49 | .collect(); 50 | 51 | completions 52 | } 53 | 54 | #[allow(dead_code)] 55 | fn postprocess_completion_candidate( 56 | mut candidate: String, 57 | options: &brush_core::completion::ProcessingOptions, 58 | working_dir: &Path, 59 | completing_end_of_line: bool, 60 | ) -> String { 61 | if options.treat_as_filenames { 62 | // Check if it's a directory. 63 | if !candidate.ends_with(std::path::MAIN_SEPARATOR) { 64 | let candidate_path = Path::new(&candidate); 65 | let abs_candidate_path = if candidate_path.is_absolute() { 66 | PathBuf::from(candidate_path) 67 | } else { 68 | working_dir.join(candidate_path) 69 | }; 70 | 71 | if abs_candidate_path.is_dir() { 72 | candidate.push(std::path::MAIN_SEPARATOR); 73 | } 74 | } 75 | } 76 | if options.no_autoquote_filenames { 77 | tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: don't autoquote filenames"); 78 | } 79 | if completing_end_of_line && !options.no_trailing_space_at_end_of_line { 80 | if !options.treat_as_filenames || !candidate.ends_with(std::path::MAIN_SEPARATOR) { 81 | candidate.push(' '); 82 | } 83 | } 84 | 85 | candidate 86 | } 87 | -------------------------------------------------------------------------------- /brush-interactive/src/error.rs: -------------------------------------------------------------------------------- 1 | /// Represents an error encountered while running or otherwise managing an interactive shell. 2 | #[allow(clippy::module_name_repetitions)] 3 | #[allow(clippy::enum_variant_names)] 4 | #[derive(thiserror::Error, Debug)] 5 | pub enum ShellError { 6 | /// An error occurred with the embedded shell. 7 | #[error("{0}")] 8 | ShellError(#[from] brush_core::Error), 9 | 10 | /// A generic I/O error occurred. 11 | #[error("I/O error: {0}")] 12 | IoError(#[from] std::io::Error), 13 | 14 | /// An error occurred while reading input. 15 | #[error("input error occurred")] 16 | InputError, 17 | 18 | /// The requested input backend type is not supported. 19 | #[error("requested input backend type not supported")] 20 | InputBackendNotSupported, 21 | } 22 | -------------------------------------------------------------------------------- /brush-interactive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Library implementing interactive command input and completion for the brush shell. 2 | 3 | #![deny(missing_docs)] 4 | 5 | mod error; 6 | pub use error::ShellError; 7 | 8 | mod interactive_shell; 9 | pub use interactive_shell::{ 10 | InteractiveExecutionResult, InteractivePrompt, InteractiveShell, ReadResult, 11 | }; 12 | 13 | mod options; 14 | pub use options::Options; 15 | 16 | #[cfg(any(windows, unix))] 17 | mod completion; 18 | 19 | // Reedline-based shell 20 | #[cfg(feature = "reedline")] 21 | mod reedline; 22 | #[cfg(feature = "reedline")] 23 | pub use reedline::ReedlineShell; 24 | 25 | // Basic shell 26 | #[cfg(feature = "basic")] 27 | mod basic; 28 | #[cfg(feature = "basic")] 29 | pub use basic::BasicShell; 30 | 31 | // Minimal shell 32 | #[cfg(feature = "minimal")] 33 | mod minimal; 34 | #[cfg(feature = "minimal")] 35 | pub use minimal::MinimalShell; 36 | 37 | mod trace_categories; 38 | -------------------------------------------------------------------------------- /brush-interactive/src/minimal/mod.rs: -------------------------------------------------------------------------------- 1 | mod minimal_shell; 2 | 3 | #[allow(clippy::module_name_repetitions)] 4 | pub use minimal_shell::MinimalShell; 5 | -------------------------------------------------------------------------------- /brush-interactive/src/options.rs: -------------------------------------------------------------------------------- 1 | /// Options for creating an interactive shell. 2 | pub struct Options { 3 | /// Lower-level options for creating the shell. 4 | pub shell: brush_core::CreateOptions, 5 | /// Whether to disable bracketed paste mode. 6 | pub disable_bracketed_paste: bool, 7 | /// Whether to disable color. 8 | pub disable_color: bool, 9 | /// Whether to disable syntax highlighting. 10 | pub disable_highlighting: bool, 11 | } 12 | -------------------------------------------------------------------------------- /brush-interactive/src/reedline/completer.rs: -------------------------------------------------------------------------------- 1 | use nu_ansi_term::{Color, Style}; 2 | use std::borrow::BorrowMut; 3 | 4 | use super::refs; 5 | use crate::completion; 6 | 7 | pub(crate) struct ReedlineCompleter { 8 | pub shell: refs::ShellRef, 9 | } 10 | 11 | impl reedline::Completer for ReedlineCompleter { 12 | fn complete(&mut self, line: &str, pos: usize) -> Vec { 13 | tokio::task::block_in_place(|| { 14 | tokio::runtime::Handle::current().block_on(self.complete_async(line, pos)) 15 | }) 16 | } 17 | } 18 | 19 | impl ReedlineCompleter { 20 | async fn complete_async(&self, line: &str, pos: usize) -> Vec { 21 | let mut shell_guard = self.shell.lock().await; 22 | let shell = shell_guard.borrow_mut().as_mut(); 23 | 24 | let completions = completion::complete_async(shell, line, pos).await; 25 | let insertion_index = completions.insertion_index; 26 | let delete_count = completions.delete_count; 27 | let options = completions.options; 28 | 29 | completions 30 | .candidates 31 | .into_iter() 32 | .map(|candidate| { 33 | Self::to_suggestion(line, candidate, insertion_index, delete_count, &options) 34 | }) 35 | .collect() 36 | } 37 | 38 | fn to_suggestion( 39 | line: &str, 40 | mut candidate: String, 41 | mut insertion_index: usize, 42 | mut delete_count: usize, 43 | options: &brush_core::completion::ProcessingOptions, 44 | ) -> reedline::Suggestion { 45 | let mut style = Style::new(); 46 | 47 | // Special handling for filename completions. 48 | if options.treat_as_filenames { 49 | if candidate.ends_with(std::path::MAIN_SEPARATOR) { 50 | style = style.fg(Color::Green); 51 | } 52 | 53 | if insertion_index + delete_count <= line.len() { 54 | let removed = &line[insertion_index..insertion_index + delete_count]; 55 | if let Some(last_sep_index) = removed.rfind(std::path::MAIN_SEPARATOR) { 56 | if candidate.starts_with(removed) { 57 | candidate = candidate.split_off(last_sep_index + 1); 58 | insertion_index += last_sep_index + 1; 59 | delete_count -= last_sep_index + 1; 60 | } 61 | } 62 | } 63 | } 64 | 65 | // See if there's whitespace at the end. 66 | let append_whitespace = candidate.ends_with(' '); 67 | if append_whitespace { 68 | candidate.pop(); 69 | } 70 | 71 | reedline::Suggestion { 72 | value: candidate, 73 | description: None, 74 | style: Some(style), 75 | extra: None, 76 | span: reedline::Span { 77 | start: insertion_index, 78 | end: insertion_index + delete_count, 79 | }, 80 | append_whitespace, 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /brush-interactive/src/reedline/mod.rs: -------------------------------------------------------------------------------- 1 | mod completer; 2 | mod edit_mode; 3 | mod highlighter; 4 | mod prompt; 5 | mod reedline_shell; 6 | mod refs; 7 | mod validator; 8 | 9 | #[allow(clippy::module_name_repetitions)] 10 | pub use reedline_shell::ReedlineShell; 11 | -------------------------------------------------------------------------------- /brush-interactive/src/reedline/prompt.rs: -------------------------------------------------------------------------------- 1 | use crate::interactive_shell::InteractivePrompt; 2 | 3 | impl reedline::Prompt for InteractivePrompt { 4 | fn render_prompt_left(&self) -> std::borrow::Cow<'_, str> { 5 | // [Workaround: see https://github.com/nushell/reedline/issues/707] 6 | // If the prompt starts with a newline character, then there's a chance 7 | // that it won't be rendered correctly. For this specific case, insert 8 | // an extra space character before the newline. 9 | if self.prompt.starts_with('\n') { 10 | std::format!(" {}", self.prompt).into() 11 | } else { 12 | self.prompt.as_str().into() 13 | } 14 | } 15 | 16 | fn render_prompt_right(&self) -> std::borrow::Cow<'_, str> { 17 | self.alt_side_prompt.as_str().into() 18 | } 19 | 20 | // N.B. For now, we don't support prompt indicators. 21 | fn render_prompt_indicator( 22 | &self, 23 | _prompt_mode: reedline::PromptEditMode, 24 | ) -> std::borrow::Cow<'_, str> { 25 | "".into() 26 | } 27 | 28 | fn render_prompt_multiline_indicator(&self) -> std::borrow::Cow<'_, str> { 29 | self.continuation_prompt.as_str().into() 30 | } 31 | 32 | fn render_prompt_history_search_indicator( 33 | &self, 34 | history_search: reedline::PromptHistorySearch, 35 | ) -> std::borrow::Cow<'_, str> { 36 | match history_search.status { 37 | reedline::PromptHistorySearchStatus::Passing => { 38 | if history_search.term.is_empty() { 39 | "(rev search) ".into() 40 | } else { 41 | std::format!("(rev search: {}) ", history_search.term).into() 42 | } 43 | } 44 | reedline::PromptHistorySearchStatus::Failing => { 45 | std::format!("(failing rev search: {}) ", history_search.term).into() 46 | } 47 | } 48 | } 49 | 50 | fn get_prompt_color(&self) -> reedline::Color { 51 | reedline::Color::Reset 52 | } 53 | 54 | fn get_prompt_multiline_color(&self) -> nu_ansi_term::Color { 55 | nu_ansi_term::Color::LightBlue 56 | } 57 | 58 | fn get_indicator_color(&self) -> reedline::Color { 59 | reedline::Color::Cyan 60 | } 61 | 62 | fn get_prompt_right_color(&self) -> reedline::Color { 63 | reedline::Color::AnsiValue(5) 64 | } 65 | 66 | fn right_prompt_on_last_line(&self) -> bool { 67 | false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /brush-interactive/src/reedline/refs.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::{Borrow, BorrowMut}, 3 | sync::Arc, 4 | }; 5 | 6 | use tokio::sync::{Mutex, MutexGuard}; 7 | 8 | pub(crate) type ShellRef = Arc>; 9 | 10 | pub(crate) struct ReedlineShellReader<'a> { 11 | pub shell: MutexGuard<'a, brush_core::Shell>, 12 | } 13 | 14 | impl AsRef for ReedlineShellReader<'_> { 15 | fn as_ref(&self) -> &brush_core::Shell { 16 | self.shell.borrow() 17 | } 18 | } 19 | 20 | pub(crate) struct ReedlineShellWriter<'a> { 21 | pub shell: MutexGuard<'a, brush_core::Shell>, 22 | } 23 | 24 | impl AsMut for ReedlineShellWriter<'_> { 25 | fn as_mut(&mut self) -> &mut brush_core::Shell { 26 | self.shell.borrow_mut() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /brush-interactive/src/reedline/validator.rs: -------------------------------------------------------------------------------- 1 | use super::refs; 2 | 3 | pub(crate) struct ReedlineValidator { 4 | pub shell: refs::ShellRef, 5 | } 6 | 7 | impl reedline::Validator for ReedlineValidator { 8 | fn validate(&self, line: &str) -> reedline::ValidationResult { 9 | let shell = tokio::task::block_in_place(|| { 10 | tokio::runtime::Handle::current().block_on(self.shell.lock()) 11 | }); 12 | 13 | match shell.parse_string(line.to_owned()) { 14 | Err(brush_parser::ParseError::Tokenizing { inner, position: _ }) 15 | if inner.is_incomplete() => 16 | { 17 | reedline::ValidationResult::Incomplete 18 | } 19 | Err(brush_parser::ParseError::ParsingAtEndOfInput) => { 20 | reedline::ValidationResult::Incomplete 21 | } 22 | _ => reedline::ValidationResult::Complete, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /brush-interactive/src/trace_categories.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub(crate) const COMPLETION: &str = "completion"; 4 | -------------------------------------------------------------------------------- /brush-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brush-parser" 3 | description = "POSIX/bash shell tokenizer and parsers (used by brush-shell)" 4 | version = "0.2.16" 5 | authors.workspace = true 6 | categories.workspace = true 7 | edition.workspace = true 8 | keywords.workspace = true 9 | license.workspace = true 10 | readme.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [lib] 18 | bench = false 19 | 20 | [features] 21 | fuzz-testing = ["dep:arbitrary"] 22 | debug-tracing = ["peg/trace"] 23 | 24 | [dependencies] 25 | arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } 26 | cached = "0.55.1" 27 | indenter = "0.3.3" 28 | peg = "0.8.5" 29 | thiserror = "2.0.12" 30 | tracing = "0.1.41" 31 | utf8-chars = "3.0.5" 32 | 33 | [dev-dependencies] 34 | anyhow = "1.0.98" 35 | assert_matches = "1.5.0" 36 | criterion = { version = "0.5.1", features = ["html_reports"] } 37 | pretty_assertions = { version = "1.4.1", features = ["unstable"] } 38 | 39 | [target.'cfg(unix)'.dev-dependencies] 40 | pprof = { version = "0.15.0", features = ["criterion", "flamegraph"] } 41 | 42 | [[bench]] 43 | name = "parser" 44 | harness = false 45 | -------------------------------------------------------------------------------- /brush-parser/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /brush-parser/benches/parser.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | mod unix { 3 | use brush_parser::{parse_tokens, Token}; 4 | use criterion::Criterion; 5 | 6 | fn uncached_tokenize(content: &str) -> Vec { 7 | brush_parser::uncached_tokenize_str(content, &brush_parser::TokenizerOptions::default()) 8 | .unwrap() 9 | } 10 | 11 | fn cacheable_tokenize(content: &str) -> Vec { 12 | brush_parser::tokenize_str_with_options(content, &brush_parser::TokenizerOptions::default()) 13 | .unwrap() 14 | } 15 | 16 | fn parse(tokens: &Vec) -> brush_parser::ast::Program { 17 | parse_tokens( 18 | tokens, 19 | &brush_parser::ParserOptions::default(), 20 | &brush_parser::SourceInfo::default(), 21 | ) 22 | .unwrap() 23 | } 24 | 25 | const SAMPLE_SCRIPT: &str = r#" 26 | for f in A B C; do 27 | echo "${f@L}" >&2 28 | done 29 | "#; 30 | 31 | fn benchmark_parsing_script_using_caches(c: &mut Criterion, script_path: &std::path::Path) { 32 | let contents = std::fs::read_to_string(script_path).unwrap(); 33 | 34 | c.bench_function( 35 | std::format!( 36 | "parse_{}", 37 | script_path.file_name().unwrap().to_string_lossy() 38 | ) 39 | .as_str(), 40 | |b| b.iter(|| parse(&cacheable_tokenize(contents.as_str()))), 41 | ); 42 | } 43 | 44 | pub(crate) fn criterion_benchmark(c: &mut Criterion) { 45 | const POSSIBLE_BASH_COMPLETION_SCRIPT_PATH: &str = 46 | "/usr/share/bash-completion/bash_completion"; 47 | 48 | c.bench_function("tokenize_sample_script", |b| { 49 | b.iter(|| uncached_tokenize(SAMPLE_SCRIPT)); 50 | }); 51 | 52 | let tokens = uncached_tokenize(SAMPLE_SCRIPT); 53 | c.bench_function("parse_sample_script", |b| b.iter(|| parse(&tokens))); 54 | 55 | let well_known_complicated_script = 56 | std::path::PathBuf::from(POSSIBLE_BASH_COMPLETION_SCRIPT_PATH); 57 | 58 | if well_known_complicated_script.exists() { 59 | benchmark_parsing_script_using_caches(c, &well_known_complicated_script); 60 | } 61 | } 62 | } 63 | 64 | #[cfg(unix)] 65 | criterion::criterion_group! { 66 | name = benches; 67 | config = criterion::Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(100, pprof::criterion::Output::Flamegraph(None))); 68 | targets = unix::criterion_benchmark 69 | } 70 | #[cfg(unix)] 71 | criterion::criterion_main!(benches); 72 | 73 | #[cfg(not(unix))] 74 | fn main() -> () {} 75 | -------------------------------------------------------------------------------- /brush-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Implements a tokenizer and parsers for POSIX / bash shell syntax. 2 | 3 | #![deny(missing_docs)] 4 | 5 | pub mod arithmetic; 6 | pub mod ast; 7 | pub mod pattern; 8 | pub mod prompt; 9 | pub mod readline_binding; 10 | pub mod test_command; 11 | pub mod word; 12 | 13 | mod error; 14 | mod parser; 15 | mod tokenizer; 16 | 17 | pub use error::{BindingParseError, ParseError, TestCommandParseError, WordParseError}; 18 | pub use parser::{parse_tokens, Parser, ParserOptions, SourceInfo}; 19 | pub use tokenizer::{ 20 | tokenize_str, tokenize_str_with_options, uncached_tokenize_str, unquote_str, SourcePosition, 21 | Token, TokenLocation, TokenizerError, TokenizerOptions, 22 | }; 23 | -------------------------------------------------------------------------------- /brush-shell/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brush-shell" 3 | description = "Rust-implemented shell focused on POSIX and bash compatibility" 4 | version = "0.2.18" 5 | authors.workspace = true 6 | categories.workspace = true 7 | edition.workspace = true 8 | keywords.workspace = true 9 | license.workspace = true 10 | readme.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | 14 | [package.metadata.binstall] 15 | pkg-url = "{ repo }/releases/download/{ name }-v{ version }/brush-{ target }{ archive-suffix }" 16 | 17 | [[bin]] 18 | name = "brush" 19 | path = "src/main.rs" 20 | bench = false 21 | 22 | [[test]] 23 | name = "brush-compat-tests" 24 | path = "tests/compat_tests.rs" 25 | harness = false 26 | 27 | [[test]] 28 | name = "brush-interactive-tests" 29 | path = "tests/interactive_tests.rs" 30 | 31 | [[test]] 32 | name = "brush-completion-tests" 33 | path = "tests/completion_tests.rs" 34 | 35 | [features] 36 | default = ["basic", "reedline", "minimal"] 37 | basic = ["brush-interactive/basic"] 38 | minimal = ["brush-interactive/minimal"] 39 | reedline = ["brush-interactive/reedline"] 40 | 41 | [lints] 42 | workspace = true 43 | 44 | [dependencies] 45 | async-trait = "0.1.88" 46 | brush-parser = { version = "^0.2.16", path = "../brush-parser" } 47 | brush-core = { version = "^0.3.1", path = "../brush-core" } 48 | cfg-if = "1.0.0" 49 | clap = { version = "4.5.37", features = ["derive", "env", "wrap_help"] } 50 | const_format = "0.2.34" 51 | git-version = "0.3.9" 52 | lazy_static = "1.5.0" 53 | tracing = "0.1.41" 54 | tracing-subscriber = "0.3.19" 55 | human-panic = "2.0.2" 56 | 57 | [target.'cfg(not(any(windows, unix)))'.dependencies] 58 | brush-interactive = { version = "^0.2.18", path = "../brush-interactive", features = [ 59 | "minimal", 60 | ] } 61 | tokio = { version = "1.45.1", features = ["rt", "sync"] } 62 | 63 | [target.'cfg(any(windows, unix))'.dependencies] 64 | brush-interactive = { version = "^0.2.18", path = "../brush-interactive", features = [ 65 | "basic", 66 | "reedline", 67 | ] } 68 | crossterm = "0.29.0" 69 | tokio = { version = "1.45.1", features = ["rt", "rt-multi-thread", "sync"] } 70 | 71 | [target.wasm32-unknown-unknown.dependencies] 72 | getrandom = { version = "0.3.3", features = ["wasm_js"] } 73 | uuid = { version = "1.17.0", features = ["js"] } 74 | 75 | [dev-dependencies] 76 | anyhow = "1.0.98" 77 | assert_cmd = "2.0.17" 78 | assert_fs = "1.1.3" 79 | colored = "2.2.0" 80 | descape = "2.0.3" 81 | diff = "0.1.13" 82 | expectrl = { git = "https://github.com/zhiburt/expectrl", rev = "a0f4f7816b9a47a191dd858080e8fd80ff71cd96" } 83 | glob = "0.3.2" 84 | indent = "0.1.1" 85 | junit-report = "0.8.3" 86 | regex = "1.11.1" 87 | serde = { version = "1.0.219", features = ["derive"] } 88 | serde_yaml = "0.9.34" 89 | strip-ansi-escapes = "0.2.1" 90 | version-compare = "0.2.0" 91 | walkdir = "2.5.0" 92 | -------------------------------------------------------------------------------- /brush-shell/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /brush-shell/src/brushctl.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use std::io::Write; 3 | 4 | use crate::events; 5 | 6 | pub(crate) fn register(shell: &mut brush_core::Shell) { 7 | shell.register_builtin( 8 | "brushctl", 9 | brush_core::builtins::builtin::(), 10 | ); 11 | } 12 | 13 | /// Configure the running brush shell. 14 | #[derive(Parser)] 15 | struct BrushCtlCommand { 16 | #[clap(subcommand)] 17 | command_group: CommandGroup, 18 | } 19 | 20 | #[derive(Subcommand)] 21 | enum CommandGroup { 22 | #[clap(subcommand)] 23 | Events(EventsCommand), 24 | } 25 | 26 | /// Commands for configuring tracing events. 27 | #[derive(Subcommand)] 28 | enum EventsCommand { 29 | /// Display status of enabled events. 30 | Status, 31 | 32 | /// Enable event. 33 | Enable { 34 | /// Event to enable. 35 | event: events::TraceEvent, 36 | }, 37 | 38 | /// Disable event. 39 | Disable { 40 | /// Event to disable. 41 | event: events::TraceEvent, 42 | }, 43 | } 44 | 45 | impl brush_core::builtins::Command for BrushCtlCommand { 46 | async fn execute( 47 | &self, 48 | context: brush_core::ExecutionContext<'_>, 49 | ) -> Result { 50 | match self.command_group { 51 | CommandGroup::Events(ref events) => events.execute(&context), 52 | } 53 | } 54 | } 55 | 56 | impl EventsCommand { 57 | fn execute( 58 | &self, 59 | context: &brush_core::ExecutionContext<'_>, 60 | ) -> Result { 61 | let event_config = crate::get_event_config(); 62 | 63 | let mut event_config = event_config.try_lock().map_err(|_| { 64 | brush_core::Error::Unimplemented("Failed to acquire lock on event configuration") 65 | })?; 66 | 67 | if let Some(event_config) = event_config.as_mut() { 68 | match self { 69 | EventsCommand::Status => { 70 | let enabled_events = event_config.get_enabled_events(); 71 | for event in enabled_events { 72 | writeln!(context.stdout(), "{event}").unwrap(); // Add .unwrap() to handle 73 | // any potential write 74 | // errors 75 | } 76 | } 77 | EventsCommand::Enable { event } => event_config.enable(*event)?, 78 | EventsCommand::Disable { event } => event_config.disable(*event)?, 79 | } 80 | 81 | Ok(brush_core::builtins::ExitCode::Success) 82 | } else { 83 | Err(brush_core::Error::Unimplemented( 84 | "event configuration not initialized", 85 | )) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /brush-shell/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub mod args; 4 | pub mod events; 5 | 6 | mod productinfo; 7 | -------------------------------------------------------------------------------- /brush-shell/src/productinfo.rs: -------------------------------------------------------------------------------- 1 | //! Information about this shell project. 2 | 3 | /// The formal name of this product. 4 | pub const PRODUCT_NAME: &str = "brush"; 5 | 6 | const PRODUCT_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); 7 | const PRODUCT_REPO: &str = env!("CARGO_PKG_REPOSITORY"); 8 | 9 | /// The URI to display as the product's homepage. 10 | #[allow(clippy::const_is_empty)] 11 | pub const PRODUCT_DISPLAY_URI: &str = if !PRODUCT_HOMEPAGE.is_empty() { 12 | PRODUCT_HOMEPAGE 13 | } else { 14 | PRODUCT_REPO 15 | }; 16 | 17 | /// The version of the product, in string form. 18 | pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION"); 19 | 20 | /// Info regarding the specific version of sources used to build this product. 21 | pub const PRODUCT_GIT_VERSION: &str = git_version::git_version!( 22 | prefix = "git:", 23 | cargo_prefix = "cargo:", 24 | fallback = "unknown:", 25 | args = ["--always", "--dirty=-modified", "--match", ""] 26 | ); 27 | 28 | pub(crate) fn get_product_display_str() -> String { 29 | std::format!( 30 | "{PRODUCT_NAME} version {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}" 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/and_or.yaml: -------------------------------------------------------------------------------- 1 | name: "and/or" 2 | cases: 3 | - name: "Basic &&" 4 | stdin: | 5 | false && echo 1 6 | true && echo 2 7 | 8 | - name: "Basic ||" 9 | stdin: | 10 | false || echo 1 11 | true || echo 2 12 | 13 | - name: "Longer chains" 14 | stdin: | 15 | false || false || false || echo "Got to the end" 16 | echo "1" && echo "2" && echo "3" && echo "4" 17 | 18 | - name: "Mixed chains" 19 | stdin: | 20 | false && true || echo "1. Got to the end" 21 | false && false || echo "2. Got to the end" 22 | true && false || echo "3. Got to the end" 23 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/arguments.yaml: -------------------------------------------------------------------------------- 1 | name: "Argument handling tests" 2 | common_test_files: 3 | - path: "script.sh" 4 | contents: | 5 | echo \"$0\" \"$1\" \"$2\" \"$@\" 6 | 7 | cases: 8 | - name: "-c mode arguments without --" 9 | args: 10 | - "-c" 11 | - 'echo \"$0\" \"$1\" \"$2\" \"$@\"' 12 | - 1 13 | - "-2" 14 | - "3" 15 | 16 | - name: "-c mode arguments with --" 17 | args: 18 | - "-c" 19 | - 'echo \"$0\" \"$1\" \"$2\" \"$@\"' 20 | - "--" 21 | - 1 22 | - 2 23 | - 3 24 | 25 | - name: "-c mode and arguments with +O" 26 | args: 27 | - "+O" 28 | - "nullglob" 29 | - "-c" 30 | - 'echo \"$0\" \"$1\" \"$2\" \"$@\"' 31 | - "--" 32 | - 1 33 | - 2 34 | - 3 35 | 36 | - name: "-c mode -- torture" 37 | args: 38 | - "-c" 39 | - 'echo \"$0\" \"$1\" \"$2\" \"$@\"' 40 | - -- 41 | - -- 42 | - -&-1 43 | - --! 44 | - "\"-2\"" 45 | - "''--''" 46 | - 3--* 47 | 48 | - name: "-c modeonly one --" 49 | args: 50 | - "-c" 51 | - 'echo \"$0\" \"$1\" \"$2\" \"$@\"' 52 | - -- 53 | 54 | - name: "script arguments without --" 55 | args: 56 | - script.sh 57 | - -1 58 | - -2 59 | - -3 60 | 61 | - name: "script arguments with --" 62 | args: 63 | - script.sh 64 | - -- 65 | - --1 66 | - -2 67 | - 3 68 | 69 | - name: "script -- torture" 70 | args: 71 | - script.sh 72 | - -- 73 | - "--" 74 | - -- 75 | - -!-1* 76 | - "\"-2\"" 77 | - -- 78 | - 3-- 79 | 80 | - name: "script only one --" 81 | args: 82 | - script.sh 83 | - -- 84 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/assignments.yaml: -------------------------------------------------------------------------------- 1 | name: "Assignments" 2 | cases: 3 | - name: "First char is equals sign" 4 | ignore_stderr: true 5 | stdin: | 6 | =x 7 | 8 | - name: "Basic assignment" 9 | stdin: | 10 | x=yz 11 | echo "x: ${x}" 12 | 13 | - name: "Invalid variable name" 14 | ignore_stderr: true 15 | stdin: | 16 | @=something 17 | 18 | - name: "Quoted equals sign" 19 | ignore_stderr: true 20 | stdin: | 21 | x"="3 22 | 23 | - name: "Multiple equals signs" 24 | stdin: | 25 | x=y=z 26 | echo "x: ${x}" 27 | 28 | - name: "Assignment with tilde expansion" 29 | known_failure: true 30 | stdin: | 31 | HOME=/some/dir 32 | 33 | var=~/file1.txt 34 | echo "~/file1.txt: ${var}" 35 | 36 | var=~/file1.txt:~/file2.txt 37 | echo "~/file1.txt:~/file2.txt: ${var}" 38 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/basic.yaml: -------------------------------------------------------------------------------- 1 | name: "Basic tests" 2 | cases: 3 | - name: "Basic -c usage" 4 | args: 5 | - "-c" 6 | - "echo hi" 7 | 8 | - name: "Basic stdin usage" 9 | stdin: | 10 | echo hi 11 | 12 | - name: "Basic sequence" 13 | stdin: | 14 | echo 'hi'; echo 'there' 15 | 16 | - name: "Basic script execution" 17 | test_files: 18 | - path: "script.sh" 19 | contents: | 20 | echo 'hi' 21 | exit 22 22 | args: ["./script.sh"] 23 | 24 | - name: "Ensure ~ is resolvable" 25 | stdin: "test ~" 26 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/alias.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: alias" 2 | cases: 3 | - name: "Basic alias usage" 4 | stdin: | 5 | shopt -s expand_aliases 6 | alias myalias=echo 7 | alias 8 | myalias 'hello' 9 | 10 | - name: "Alias with trailing space" 11 | known_failure: true # Issue #57 12 | stdin: | 13 | shopt -s expand_aliases 14 | alias cmd='echo ' 15 | alias other='replaced ' 16 | alias otherother='also-replaced' 17 | 18 | cmd other otherother 19 | 20 | - name: "Alias referencing to alias" 21 | known_failure: true # Issue #57 22 | stdin: | 23 | shopt -s expand_aliases 24 | alias myalias=echo 25 | alias outeralias=myalias 26 | outeralias 'hello' 27 | 28 | - name: "Alias to keywords" 29 | known_failure: true # Issue #286 30 | stdin: | 31 | shopt -s expand_aliases 32 | alias myalias=if 33 | myalias true; then echo "true"; fi 34 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/builtin.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: builtin" 2 | cases: 3 | - name: "builtin with no builtin" 4 | stdin: builtin 5 | 6 | - name: "builtin with unknown builtin" 7 | ignore_stderr: true 8 | stdin: builtin not-a-builtin args 9 | 10 | - name: "valid builtin" 11 | stdin: builtin echo "Hello world" 12 | 13 | - name: "valid builtin with hyphen args" 14 | stdin: builtin echo -e "Hello\nWorld" 15 | 16 | - name: "builtin passing through results" 17 | stdin: | 18 | builtin false; echo "builtin false => $?" 19 | builtin true; echo "builtin true => $?" 20 | 21 | - name: "builtin with non-decl builtin" 22 | stdin: | 23 | builtin echo variable=value 24 | 25 | - name: "builtin with decl builtin" 26 | stdin: | 27 | builtin declare variable=value 28 | declare -p variable 29 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/colon.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: colon" 2 | cases: 3 | - name: "Basic colon usage" 4 | ignore_stderr: true 5 | stdin: | 6 | : 7 | echo "Result 1: $?" 8 | : something 9 | echo "Result 2: $?" 10 | : --anything=here or here 11 | echo "Result 3: $?" 12 | : --help 13 | echo "Result 4: $?" 14 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/command.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: command" 2 | cases: 3 | - name: "Basic command usage" 4 | ignore_stderr: true 5 | stdin: | 6 | echo "Executing echo built-in" 7 | command echo "Hello" 8 | 9 | echo "Executing ls using name" 10 | command ls -d / 11 | 12 | echo "Executing ls using absolute path" 13 | command $(which ls) -d / 14 | 15 | echo "Executing non-existent command by name" 16 | command non-existent 17 | 18 | echo "Executing non-existent command by path" 19 | command /usr/bin/non-existent 20 | 21 | - name: "command -v" 22 | stdin: | 23 | echo "PATH: $PATH" 24 | 25 | echo "[echo]" 26 | command -v echo 27 | 28 | echo "[non-existent]" 29 | command -v non-existent || echo "1. Not found" 30 | 31 | echo "[/usr/bin/non-existent]" 32 | command -v /usr/bin/non-existent || echo "2. Not found" 33 | 34 | - name: "command -v -p" 35 | stdin: | 36 | unset PATH 37 | 38 | echo "[no -p]" 39 | command -v cat 40 | 41 | echo "[-p]" 42 | command -v -p cat 43 | 44 | - name: "command -v with full paths" 45 | skip: true # TODO: investigate why this fails on arch linux 46 | stdin: | 47 | echo "[cat]" 48 | command -v cat 49 | 50 | echo "[\$(command -v cat)]" 51 | command -v $(command -v cat) 52 | 53 | - name: "command -V" 54 | ignore_stderr: true 55 | stdin: | 56 | command -V echo 57 | command -V ls 58 | command -V $(which ls) 59 | 60 | command -V non-existent || echo "1. Not found" 61 | command -V /usr/bin/non-existent || echo "2. Not found" 62 | 63 | - name: "command with --" 64 | stdin: | 65 | command -- ls 66 | 67 | - name: "command with --help" 68 | stdin: | 69 | command ls --help 70 | 71 | - name: "command with -p" 72 | ignore_stderr: true # In case it fails on the oracle as well, ignore the specific error message. 73 | stdin: | 74 | unset PATH 75 | 76 | command -p -- ls 77 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/common.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtin Common Tests" 2 | cases: 3 | - name: "Piping builtin output" 4 | stdin: | 5 | shopt | wc -l | wc -l 6 | 7 | - name: "Redirecting builtin output" 8 | stdin: | 9 | declare my_variable=10 10 | declare -p my_variable >out.txt 11 | 12 | echo "Dumping file contents..." 13 | cat out.txt 14 | 15 | - name: "Overrides" 16 | ignore_stderr: true 17 | stdin: | 18 | declare -p myvar 19 | myvar=10 declare -p myvar 20 | declare -p myvar 21 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/complete.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: complete" 2 | cases: 3 | - name: "complete with no options" 4 | stdin: | 5 | complete -W foo mycmd 6 | complete 7 | 8 | - name: "Roundtrip: complete -W" 9 | stdin: | 10 | complete -W foo mycmd 11 | complete -p mycmd 12 | 13 | complete -W 'foo bar' mycmd 14 | complete -p mycmd 15 | 16 | - name: "Roundtrip: complete -P" 17 | stdin: | 18 | complete -P myprefix mycmd 19 | complete -p mycmd 20 | 21 | complete -P 'my prefix' mycmd 22 | complete -p mycmd 23 | 24 | - name: "Roundtrip: complete -S" 25 | stdin: | 26 | complete -S mysuffix mycmd 27 | complete -p mycmd 28 | 29 | complete -S 'my suffix' mycmd 30 | complete -p mycmd 31 | 32 | - name: "Roundtrip: complete -F" 33 | stdin: | 34 | complete -Fmyfunc mycmd 35 | complete -p mycmd 36 | 37 | - name: "Roundtrip: complete -F" 38 | stdin: | 39 | complete -G pattern mycmd 40 | complete -p mycmd 41 | 42 | complete -G 'pat tern' mycmd 43 | complete -p mycmd 44 | 45 | - name: "Roundtrip: complete -X" 46 | stdin: | 47 | complete -X pattern mycmd 48 | complete -p mycmd 49 | 50 | complete -X 'pat tern' mycmd 51 | complete -p mycmd 52 | 53 | - name: "Roundtrip: complete -C" 54 | stdin: | 55 | complete -C cmd mycmd 56 | complete -p mycmd 57 | 58 | complete -C 'c md' mycmd 59 | complete -p mycmd 60 | 61 | - name: "Roundtrip: complete -A" 62 | stdin: | 63 | for action in alias arrayvar binding builtin command directory disabled enabled export file 'function' group helptopic hostname job keyword running service setopt shopt signal stopped user variable; do 64 | complete -A ${action} mycmd 65 | complete -p mycmd 66 | done 67 | 68 | - name: "Roundtrip: complete -o options" 69 | stdin: | 70 | for opt in bashdefault default dirnames filenames noquote nosort nospace plusdirs; do 71 | echo "--- Testing option: ${opt} ------------------" 72 | complete -o ${opt} mycmd_${opt} 73 | complete -p mycmd_${opt} 74 | done 75 | 76 | - name: "complete -r" 77 | stdin: | 78 | echo "[Removing non-existent]" 79 | complete -r nonexistent 80 | echo $? 81 | 82 | echo "[Removing existing]" 83 | complete -W token mycmd 84 | complete -r mycmd 85 | echo $? 86 | complete -p mycmd 2>/dev/null 87 | 88 | - name: "complete -r with no args" 89 | stdin: | 90 | complete -W token mycmd1 91 | complete -W token mycmd2 92 | 93 | complete -r 94 | echo $? 95 | 96 | complete -p 97 | 98 | - name: "complete -r with special options" 99 | stdin: | 100 | complete -W token mycmd 101 | complete -W other -E 102 | 103 | complete -r -E 104 | echo $? 105 | 106 | complete -p 107 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/dot.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: dot" 2 | cases: 3 | - name: "Basic dot usage" 4 | test_files: 5 | - path: "script.sh" 6 | contents: | 7 | echo "In sourced script" 8 | var="updated" 9 | stdin: | 10 | var="orig" 11 | . script.sh 12 | echo "var: ${var}" 13 | 14 | - name: "Basic source usage" 15 | test_files: 16 | - path: "script.sh" 17 | contents: | 18 | echo "In sourced script" 19 | for f in ${FUNCNAME[@]}; do 20 | echo "FUNCNAME: '${f}'" 21 | done 22 | for s in ${BASH_SOURCE[@]}; do 23 | echo "BASH_SOURCE: '${s}'" 24 | done 25 | var="updated" 26 | 27 | function script_func() { 28 | echo "In script func" 29 | for f in ${FUNCNAME[@]}; do 30 | echo "FUNCNAME: '${f}'" 31 | done 32 | for s in ${BASH_SOURCE[@]}; do 33 | echo "BASH_SOURCE: '${s}'" 34 | done 35 | } 36 | stdin: | 37 | var="orig" 38 | source ./script.sh 39 | echo "var: ${var}" 40 | script_func 41 | 42 | - name: "Source non-existent file path" 43 | ignore_stderr: true 44 | stdin: | 45 | source script.sh 46 | echo "Result: $?" 47 | 48 | - name: "Source dir" 49 | ignore_stderr: true 50 | stdin: | 51 | source . 52 | echo "Result: $?" 53 | 54 | - name: "Source script with args" 55 | test_files: 56 | - path: "script.sh" 57 | contents: | 58 | echo "In sourced script" 59 | echo "Args: $@" 60 | stdin: | 61 | source script.sh arg1 arg2 62 | 63 | - name: "Source with redirection" 64 | test_files: 65 | - path: "script.sh" 66 | contents: | 67 | echo "In sourced script" 68 | stdin: | 69 | source script.sh arg1 arg2 > out.txt 70 | echo "Sourced script; dumping..." 71 | cat out.txt 72 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/echo.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: echo" 2 | cases: 3 | - name: "echo with only --" 4 | stdin: echo -- 5 | 6 | - name: "echo with -- and args" 7 | stdin: echo -- -1 --"aaa" ?^1as- 8 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/enable.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: enable" 2 | cases: 3 | - name: "List special builtins" 4 | stdin: enable -s 5 | 6 | - name: "List default-disabled builtins" 7 | stdin: enable -n 8 | 9 | - name: "List all builtins" 10 | stdin: | 11 | # List builtins but ignore any brush specific ones. 12 | enable | grep -v brush 13 | 14 | - name: "Disable builtins" 15 | ignore_stderr: true 16 | stdin: | 17 | type printf 18 | 19 | # Disable the builtin 20 | PATH= 21 | enable -n printf 22 | 23 | # Check 24 | type printf 25 | print "Gone\n" 26 | 27 | # Re-enable 28 | enable printf 29 | 30 | # Re-check 31 | type printf 32 | printf "Back\n" 33 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/eval.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: eval" 2 | cases: 3 | - name: "Basic eval usage" 4 | stdin: | 5 | eval 'echo 1 + 1 == $((1 + 1))' 6 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/exec.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: exec" 2 | cases: 3 | - name: "Exec an invalid path" 4 | ignore_stderr: true 5 | stdin: | 6 | exec /some/nonexistent/path 7 | 8 | - name: "Exec with no arguments" 9 | stdin: | 10 | pid=$$ 11 | exec 12 | [[ "${pid}" == "$$" ]] || echo "PID changed" 13 | 14 | - name: "Exec with no arguments and redirection" 15 | stdin: | 16 | exec 3>&1 17 | echo "Hello, fd 3!" >&3 18 | 19 | - name: "Exec a command" 20 | stdin: | 21 | exec $0 -c 'echo "In child shell"' 22 | echo "This is never reached" 23 | 24 | - name: "exec without -c" 25 | stdin: | 26 | export myvar=value 27 | exec $0 -c 'echo "myvar: ${myvar}"' 28 | 29 | - name: "exec -c" 30 | stdin: | 31 | export myvar=value 32 | exec -c $0 -c 'echo "myvar: ${myvar}"' 33 | 34 | - name: "exec -a" 35 | stdin: | 36 | exec -a shellname $0 -c 'echo "0: $0"' 37 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/exit.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: exit" 2 | cases: 3 | - name: "Exit without code" 4 | stdin: | 5 | exit 6 | 7 | - name: "Exit with code" 8 | stdin: | 9 | exit 10 10 | 11 | - name: "Exit in for loop" 12 | stdin: | 13 | for i in 1 2 3; do 14 | exit 42 15 | echo "Got past exit in loop" 16 | done 17 | echo "Got past loop" 18 | 19 | - name: "Exit in arithmetic for loop body" 20 | stdin: | 21 | for ((i = 0; i < 10; i++)); do 22 | exit 42 23 | echo "Got past exit in loop" 24 | done 25 | echo "Got past loop" 26 | 27 | - name: "Exit in while loop condition" 28 | stdin: | 29 | while exit 42; do 30 | echo "In loop" 31 | done 32 | echo "Got past loop" 33 | 34 | - name: "Exit in while loop body" 35 | stdin: | 36 | while true; do 37 | exit 42 38 | echo "Got past exit in body" 39 | break 40 | done 41 | echo "Got past loop" 42 | 43 | - name: "Exit in sequence" 44 | stdin: | 45 | exit 42; echo "Should not be printed" 46 | 47 | - name: "Exit in function" 48 | stdin: | 49 | myfunc() { 50 | exit 42 51 | echo "Got past exit in function" 52 | } 53 | 54 | myfunc 55 | echo "Got past function call" 56 | 57 | - name: "Exit in subshell" 58 | stdin: | 59 | (exit 42) 60 | echo "Got past subshell" 61 | 62 | - name: "Exit in command substitution" 63 | stdin: | 64 | output=$(echo hi; exit 42; echo there) 65 | echo "Got past command substitution" 66 | 67 | - name: "Exit in and/or" 68 | stdin: | 69 | exit 42 || echo "Got past exit" 70 | 71 | - name: "Exit in brace group" 72 | stdin: | 73 | { 74 | exit 42 75 | echo "Got past exit" 76 | } 77 | echo "Got past brace group" 78 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/export.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: export" 2 | cases: 3 | - name: "Basic export usage" 4 | stdin: | 5 | MY_TEST_VAR="value" 6 | echo "Looking for unexported variable..." 7 | env | grep MY_TEST_VAR 8 | 9 | echo "Exporting and looking for it again..." 10 | export MY_TEST_VAR 11 | env | grep MY_TEST_VAR 12 | 13 | echo "Exporting with new value..." 14 | export MY_TEST_VAR="changed value" 15 | env | grep MY_TEST_VAR 16 | 17 | - name: "Exporting array" 18 | stdin: | 19 | export arr=(a 1 2) 20 | declare -p arr 21 | 22 | - name: "Unexporting variable" 23 | stdin: | 24 | MY_TEST_VAR="value" 25 | export MY_TEST_VAR="value" 26 | export -n MY_TEST_VAR 27 | 28 | env | grep MY_TEST_VAR 29 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/hash.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: hash" 2 | cases: 3 | - name: "Non-existent program" 4 | ignore_stderr: true 5 | stdin: | 6 | hash non-existent-program 7 | echo "Result: $?" 8 | 9 | - name: "Re-hash non-existent program" 10 | ignore_stderr: true 11 | stdin: | 12 | hash -p /some/path non-existent-program 13 | hash -t non-existent-program && echo "1. Result: $?" 14 | hash non-existent-program && echo "2. Result: $?" 15 | hash -t non-existent-program && echo "3. Result: $?" 16 | 17 | - name: "Existent program" 18 | stdin: | 19 | hash ls 20 | echo "Result: $?" 21 | 22 | - name: "Display not-yet-hashed program" 23 | ignore_stderr: true 24 | stdin: | 25 | hash -t ls 26 | echo "1. Result: $?" 27 | ls >/dev/null 2>&1 28 | hash -t ls 29 | echo "2. Result: $?" 30 | 31 | - name: "Display hashed program" 32 | stdin: | 33 | hash -p /some/path somecmd && echo "1. Result: $?" 34 | hash -t somecmd && echo "2. Result: $?" 35 | 36 | - name: "Display multiple hashed programs" 37 | stdin: | 38 | hash -p /some/path somecmd1 && echo "1. Result: $?" 39 | hash -p /some/path somecmd2 && echo "2. Result: $?" 40 | hash -t somecmd1 somecmd2 && echo "3. Result: $?" 41 | 42 | - name: "Display -l path" 43 | stdin: | 44 | hash -p "/some/spaces path" somecmd && echo "1. Result: $?" 45 | hash -t somecmd && echo "2. Result: $?" 46 | hash -t -l somecmd && echo "3. Result: $?" 47 | 48 | - name: "Remove" 49 | ignore_stderr: true 50 | stdin: | 51 | hash -p /some/path somecmd && echo "1. Result: $?" 52 | hash -t somecmd && echo "2. Result: $?" 53 | hash -d somecmd && echo "3. Result: $?" 54 | hash -t somecmd && echo "4. Result: $?" 55 | 56 | - name: "Remove all" 57 | ignore_stderr: true 58 | stdin: | 59 | hash -p /some/path somecmd1 && echo "1. Result: $?" 60 | hash -p /some/path somecmd2 && echo "2. Result: $?" 61 | hash -r && echo "3. Result: $?" 62 | hash -t somecmd1 && echo "4. Result: $?" 63 | hash -t somecmd2 && echo "5. Result: $?" 64 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/help.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: help" 2 | cases: 3 | - name: "Help" 4 | ignore_stdout: true 5 | ignore_stderr: true 6 | stdin: | 7 | help 8 | 9 | - name: "Topic-specific help" 10 | ignore_stdout: true 11 | ignore_stderr: true 12 | stdin: | 13 | help echo 14 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/jobs.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: job-related builtins" 2 | cases: 3 | - name: "Basic async job" 4 | stdin: | 5 | set +m 6 | echo hi & 7 | wait 8 | jobs 9 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/kill.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: kill" 2 | cases: 3 | - name: "kill -l" 4 | ignore_stderr: true 5 | stdin: | 6 | for i in $(seq 1 31); do kill -l $i; done 7 | # limit the number of signals to 31. Realtime signals are not implemented yet. 8 | for i in $(kill -l | sed -e "s/[[:digit:]]*)//g"); do echo $i; done | head -31 9 | # invalid option 10 | kill -l 9999 11 | kill -l HUP 12 | kill -l iNt 13 | kill -l int 14 | kill -l SIGHUP 15 | kill -l EXIT 16 | 17 | - name: "kill -s" 18 | stdin: | 19 | kill -s USR1 $$ 20 | 21 | - name: "kill -n" 22 | stdin: | 23 | kill -n 9 $$ 24 | 25 | - name: "kill -sigspec" 26 | stdin: | 27 | kill -USR1 $$ 28 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/let.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: let" 2 | cases: 3 | - name: "Basic let usage" 4 | stdin: | 5 | let 0; echo "0 => $?" 6 | let 1; echo "1 => $?" 7 | 8 | let 0==0; echo "0==0 => $?" 9 | let 0!=0; echo "0!=0 => $?" 10 | 11 | let 1 0; echo "1 0 => $?" 12 | let 0 1; echo "0 1 => $?" 13 | 14 | - name: "let with assignment" 15 | stdin: | 16 | let x=10; echo "x=10 => $?; x==${x}" 17 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/local.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: local" 2 | cases: 3 | - name: "Basic local usage" 4 | stdin: | 5 | myfunc() { 6 | local x=10 7 | echo "in myfunc: x==$x" 8 | } 9 | x=5 10 | echo "before call: x==$x" 11 | myfunc 12 | echo "after call: x==$x" 13 | 14 | - name: "Local with empty array" 15 | stdin: | 16 | myfunc() { 17 | local x=() 18 | declare -p x 19 | echo "x[0]: ${x[0]}" 20 | } 21 | myfunc 22 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/mapfile.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: mapfile" 2 | cases: 3 | - name: "mapfile -t" 4 | stdin: | 5 | mapfile -t myarray < /dev/null 6 | (echo "hello"; echo "there") | (mapfile -t myarray && declare -p myarray) 7 | - name: "readarray -t" 8 | stdin: | 9 | readarray -t myarray < /dev/null 10 | (echo "hello"; echo "there") | (readarray -t myarray && declare -p myarray) 11 | - name: "mapfile -d" 12 | stdin: | 13 | mapfile -t -d 'Z' myarray < <(echo -ne "helloZthereZgeneralZkenobi") 14 | [[ "${myarray[*]}" == "hello there general kenobi" ]] 15 | - name: "mapfile no array name" 16 | stdin: | 17 | mapfile -t < <(echo -ne "woo\nah!") 18 | [[ "${MAPFILE[*]}" == "woo ah!" ]] 19 | - name: "mapfile -n" 20 | stdin: | 21 | mapfile -t -n 2 -d 'Z' myarray < <(echo -ne "helloZthereZgeneralZkenobi") 22 | [[ "${myarray[*]}" == "hello there" ]] 23 | - name: "mapfile -s" 24 | stdin: | 25 | mapfile -t -n 2 -s 1 -d 'Z' myarray < <(echo -ne "helloZthereZgeneralZkenobi") 26 | [[ "${myarray[*]}" == "there general" ]] 27 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/printf.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: printf" 2 | cases: 3 | - name: "One-variable printf" 4 | stdin: | 5 | printf "%s" "0" 6 | 7 | - name: "Basic printf" 8 | stdin: | 9 | printf "%s, %s" "Hello" "world" 10 | 11 | - name: "printf -v" 12 | stdin: | 13 | printf -v myvar "%s, %s" "Hello" "world" 14 | echo "myvar: '${myvar}'" 15 | 16 | - name: "printf -v with array index" 17 | stdin: | 18 | declare -a myarray=() 19 | printf -v 'myarray[5]' "%s, %s" "Hello" "world" 20 | declare | grep myarray 21 | 22 | - name: "printf with -v as a format arg" 23 | stdin: | 24 | printf "%s\n" "-v" 25 | 26 | - name: "printf %q" 27 | stdin: | 28 | echo "[1]" 29 | printf "%q" 'TEXT'; echo 30 | 31 | echo "[2]" 32 | printf "%q" '$VAR'; echo 33 | 34 | echo "[3]" 35 | printf "%q" '"'; echo 36 | 37 | - name: "printf ~%q" 38 | stdin: | 39 | echo "[1]" 40 | printf "~%q" 'TEXT'; echo 41 | 42 | echo "[2]" 43 | printf "~%q" '$VAR'; echo 44 | 45 | echo "[3]" 46 | printf "~%q" '"'; echo 47 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/ps4.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: PS4" 2 | cases: 3 | - name: "PS4 expansion" 4 | stdin: | 5 | PS4='+$FUNCNAME ' 6 | bar() { true; } 7 | foo() { set -x; bar; set +x; } 8 | foo 9 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/pushd_popd_dirs.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: pushd/popd/dirs" 2 | incompatible_configs: ["sh"] 3 | cases: 4 | - name: "Basic pushd usage" 5 | stdin: | 6 | cd / 7 | pushd / 8 | echo $? 9 | echo $PWD 10 | pushd /usr 11 | dirs 12 | echo $? 13 | echo $PWD 14 | popd 15 | echo $? 16 | echo $PWD 17 | popd 18 | echo $? 19 | 20 | - name: "pushd without dir change" 21 | stdin: | 22 | cd / 23 | pushd -n /usr 24 | dirs 25 | 26 | - name: "popd without dir change" 27 | stdin: | 28 | cd / 29 | pushd / 30 | pushd /usr 31 | popd -n 32 | dirs 33 | 34 | - name: "popd with empty stack" 35 | ignore_stderr: true 36 | stdin: popd 37 | 38 | - name: "pushd to non-existent dir" 39 | ignore_stderr: true 40 | stdin: pushd /non-existent-dir 41 | 42 | - name: "basic dirs usage" 43 | stdin: | 44 | cd / 45 | dirs 46 | 47 | - name: "dirs with tilde replacement" 48 | stdin: | 49 | HOME=/usr 50 | cd ~ 51 | echo "PWD: $PWD" 52 | dirs 53 | dirs -l 54 | 55 | - name: "dirs to clear" 56 | stdin: | 57 | cd /usr 58 | pushd /usr 59 | pushd / 60 | dirs -c 61 | dirs 62 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/pwd.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: pwd" 2 | cases: 3 | - name: "Basic pwd usage" 4 | stdin: | 5 | cd / 6 | pwd 7 | echo "Result: $?" 8 | # 9 | cd usr 10 | pwd 11 | echo "Result: $?" 12 | 13 | - name: "pwd -LP" 14 | stdin: | 15 | mkdir -p ./level1/level2/level3 16 | cd level1 17 | ln -s ./level2/level3 ./symlink 18 | 19 | ( 20 | cd ./symlink 21 | basename $(pwd) 22 | basename $(pwd -L) 23 | basename $(pwd -P) 24 | ) 25 | ( 26 | cd ./symlink 27 | export PWD= 28 | basename $(pwd) 29 | basename $(pwd -L) 30 | basename $(pwd -P) 31 | ) 32 | ( 33 | cd ./symlink 34 | export PWD= 35 | # start a shell without $PWD 36 | ( 37 | basename $(pwd) 38 | basename $(pwd -L) 39 | basename $(pwd -P) 40 | ) 41 | ) 42 | 43 | cd ~ 44 | pwd 45 | pwd -L 46 | pwd -P 47 | 48 | - name: "pwd with moved dir" 49 | known_failure: true # Needs investigation 50 | stdin: | 51 | root=$(pwd) 52 | 53 | mkdir -p ${root}/subdir 54 | cd ${root}/subdir 55 | mv ${root}/subdir ${root}/renamed 56 | 57 | echo "pwd -L: $(basename $(pwd -L))" 58 | echo "pwd -P: $(basename $(pwd -P))" 59 | 60 | - name: "pwd with removed dir" 61 | known_failure: true # Needs investigation 62 | stdin: | 63 | root=$(pwd) 64 | 65 | mkdir -p ${root}/subdir 66 | cd ${root}/subdir 67 | rmdir ${root}/subdir 68 | 69 | echo "pwd -L: $(basename $(pwd -L))" 70 | echo "pwd -P: $(basename $(pwd -P))" 71 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/readonly.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: readonly" 2 | cases: 3 | - name: "making var readonly" 4 | stdin: | 5 | my_var="value" 6 | readonly my_var 7 | 8 | echo "Invoking declare -p..." 9 | declare -p my_var 10 | 11 | - name: "using readonly with value" 12 | stdin: | 13 | readonly my_var="my_value" 14 | 15 | echo "Invoking declare -p..." 16 | declare -p my_var 17 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/set.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: set" 2 | cases: 3 | - name: "set with no args" 4 | stdin: | 5 | MYVARIABLE=VALUE 6 | set > set-output.txt 7 | grep MYVARIABLE set-output.txt 8 | 9 | # Remove set-output.txt to avoid it being byte-for-byte compared. 10 | rm set-output.txt 11 | 12 | - name: "Basic set usage" 13 | stdin: | 14 | set a b c d 15 | echo ${*} 16 | 17 | - name: "set with options" 18 | stdin: | 19 | function dumpopts { 20 | # Dump the options 21 | echo "[Options: $1]" 22 | echo "set options: " $- 23 | shopt -p -o pipefail 24 | } 25 | 26 | set -e -u -o pipefail 27 | dumpopts enabled 28 | echo '*: ' $* 29 | 30 | set +e +u +o pipefail 31 | dumpopts disabled 32 | echo '*: ' $* 33 | 34 | - name: "set with multiple combined options" 35 | stdin: | 36 | function dumpopts { 37 | # Dump the options 38 | echo "[Options: $1]" 39 | echo "set options: " $- 40 | shopt -p -o pipefail 41 | } 42 | 43 | set -euo pipefail 44 | dumpopts enabled 45 | echo '$*: ' $* 46 | 47 | set +euo pipefail 48 | dumpopts disabled 49 | echo '$*: ' $* 50 | 51 | - name: "set clearing args" 52 | stdin: | 53 | set a b c 54 | echo ${*} 55 | set a 56 | echo ${*} 57 | 58 | - name: "set with -" 59 | stdin: | 60 | set - a b c 61 | echo "args: " ${*} 62 | set - 63 | echo "args: " ${*} 64 | 65 | - name: "set with --" 66 | stdin: | 67 | set -- a b c 68 | echo "args: " ${*} 69 | set -- 70 | echo "args: " ${*} 71 | 72 | - name: "set with option-looking args" 73 | stdin: | 74 | set -- a -v 75 | echo ${*} 76 | 77 | set - a -v 78 | echo ${*} 79 | 80 | set a -v 81 | echo ${*} 82 | 83 | set -- a +x 84 | echo ${*} 85 | 86 | set - a +x 87 | echo ${*} 88 | 89 | set a +x 90 | echo ${*} 91 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/shopt.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: shopt" 2 | cases: 3 | - name: "shopt defaults" 4 | min_oracle_version: 5.2 5 | known_failure: true # TODO: new options from newer version of bash? 6 | stdin: | 7 | shopt | sort | grep -v extglob 8 | 9 | - name: "shopt interactive defaults" 10 | min_oracle_version: 5.2 11 | known_failure: true # TODO: new options from newer version of bash? 12 | pty: true 13 | args: ["-i", "-c", "shopt | sort | grep -v extglob"] 14 | 15 | - name: "shopt -o defaults" 16 | stdin: | 17 | shopt -o | sort 18 | 19 | - name: "shopt -o interactive defaults" 20 | pty: true 21 | args: ["-i", "-c", "shopt -o | sort | grep -v monitor"] 22 | 23 | - name: "extglob defaults" 24 | known_failure: true # TODO: we force this setting on in our shell 25 | stdin: | 26 | shopt extglob 27 | 28 | - name: "extglob interactive defaults" 29 | pty: true 30 | args: ["-i", "-c", "shopt extglob"] 31 | known_failure: true 32 | 33 | - name: "shopt -o interactive monitor default" 34 | pty: true 35 | args: ["-i", "-c", "shopt -o monitor"] 36 | 37 | - name: "shopt toggle" 38 | stdin: | 39 | echo "Setting checkwinsize" 40 | shopt -s checkwinsize 41 | 42 | echo "Displaying checkwinsize" 43 | shopt checkwinsize 44 | shopt -p checkwinsize 45 | 46 | echo "Unsetting checkwinsize" 47 | shopt -u checkwinsize 48 | 49 | echo "Displaying checkwinsize" 50 | shopt checkwinsize 51 | shopt -p checkwinsize 52 | 53 | - name: "shopt -o usage" 54 | stdin: | 55 | echo "Setting emacs" 56 | shopt -o -s emacs 57 | 58 | echo "Displaying emacs" 59 | shopt -o emacs 60 | shopt -o -p emacs 61 | 62 | echo "Unsetting emacs" 63 | shopt -o -u emacs 64 | 65 | echo "Displaying emacs" 66 | shopt -o emacs 67 | shopt -o -p emacs 68 | 69 | - name: "shopt -s lastpipe" 70 | stdin: | 71 | echo ignored | var=value 72 | echo "1. var='${var}'" 73 | 74 | shopt -s lastpipe 75 | set +o monitor 76 | echo ignored | var=value 77 | echo "2. var='${var}'" 78 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/test.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: test" 2 | cases: 3 | - name: "test: = operator" 4 | stdin: | 5 | shopt -u nocasematch 6 | test "ab" = "ab" && echo "ab = ab" 7 | test "ab" = "AB" && echo "ab = AB" 8 | test "ab" = "cd" && echo "ab = cd" 9 | test "ab" = "a?" && echo "ab = a?" 10 | 11 | shopt -s nocasematch 12 | test "ab" = "ab" && echo "ab = ab" 13 | test "ab" = "AB" && echo "ab = AB" 14 | test "ab" = "cd" && echo "ab = cd" 15 | test "ab" = "a?" && echo "ab = a?" 16 | 17 | - name: "test: == operator" 18 | stdin: | 19 | shopt -u nocasematch 20 | test "ab" == "ab" && echo "ab == ab" 21 | test "ab" == "AB" && echo "ab == AB" 22 | test "ab" == "cd" && echo "ab == cd" 23 | test "ab" == "a?" && echo "ab == a?" 24 | 25 | shopt -s nocasematch 26 | test "ab" == "ab" && echo "ab == ab" 27 | test "ab" == "AB" && echo "ab == AB" 28 | test "ab" == "cd" && echo "ab == cd" 29 | test "ab" == "a?" && echo "ab == a?" 30 | 31 | - name: "test: files refer to same device and inode" 32 | stdin: | 33 | [ /bin/sh -ef /bin/sh ] && echo "-ef correctly identified device and inode numbers" 34 | 35 | [ ! /etc/os-release -ef /bin/sh ] && echo "-ef correctly identified device and inode numbers that do not match" 36 | 37 | - name: "test: file is newer" 38 | stdin: | 39 | touch -d "2 hours ago" bar 40 | touch foo 41 | 42 | [ foo -nt bar ] && echo "-nt correctly identified newer file" 43 | [ foo -nt foo ] && echo "-nt incorrectly identified file as newer than itself" 44 | [ foo -nt file_no_exists ] && echo "-nt correctly identified when file2 does not exist" 45 | 46 | - name: "test: file is older" 47 | stdin: | 48 | touch -d "2 hours ago" foo 49 | touch bar 50 | 51 | [ foo -ot bar ] && echo "-ot correctly identified older file" 52 | [ foo -ot foo ] && echo "-ot incorrectly identified file as older than itself" 53 | [ file_no_exists -ot foo ] && echo "-ot correctly identified when file1 does not exist" 54 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/times.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: times" 2 | cases: 3 | - name: "Basic usage" 4 | ignore_stdout: true 5 | stdin: | 6 | times 7 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/trap.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: trap" 2 | cases: 3 | - name: "trap registration" 4 | stdin: | 5 | trap "echo 1" SIGINT 6 | trap "echo 2" SIGINT 7 | trap -p INT 8 | 9 | trap "echo 3" int 10 | trap -p INT 11 | 12 | trap "echo 4" 2 13 | trap -p INT 14 | 15 | - name: "trap unregistering" 16 | stdin: | 17 | echo "[Case 1]" 18 | trap "echo 1" SIGINT 19 | trap SIGINT 20 | trap -p INT 21 | 22 | echo "[Case 2]" 23 | trap "echo 2" SIGINT 24 | trap - SIGINT 25 | trap -p INT 26 | 27 | - name: "trap EXIT" 28 | known_failure: true # TODO: needs triage and debugging 29 | stdin: | 30 | trap "echo [exit]" EXIT 31 | trap -p EXIT 32 | 33 | - name: "trap DEBUG" 34 | stdin: | 35 | trap 'echo [command: ${BASH_COMMAND}]' DEBUG 36 | trap -p DEBUG 37 | 38 | - name: "trap ERR" 39 | stdin: | 40 | trap "echo [err]" ERR 41 | trap -p ERR 42 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/type.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: type" 2 | cases: 3 | - name: "Test type with no arguments" 4 | stdin: type 5 | 6 | - name: "Test type with a valid command" 7 | stdin: type ls 8 | 9 | - name: "Test type with an invalid command" 10 | ignore_stderr: true 11 | stdin: type invalid_command 12 | 13 | - name: "Test type with -t option and a builtin command" 14 | stdin: type -t cd 15 | 16 | - name: "Test type with -t option and an external command" 17 | stdin: type -t ls 18 | 19 | - name: "Test type with -t option and an undefined command" 20 | ignore_stderr: true 21 | stdin: type -t undefined_command 22 | 23 | - name: "Test type with -a option and a command with multiple definitions" 24 | stdin: type -a true 25 | 26 | - name: Test type with -p option and a builtin command 27 | stdin: type -p cd 28 | 29 | - name: Test type with -p option and an external command 30 | stdin: type -p ls 31 | 32 | - name: Test type with -P option and a builtin command 33 | stdin: type -P cd 34 | 35 | - name: Test type with -P option and an external command 36 | stdin: type -P ls 37 | 38 | - name: Test type with -f option and a function 39 | ignore_stderr: true 40 | stdin: | 41 | function myfunc() { echo "Hello, world!"; } 42 | type -f myfunc 43 | 44 | - name: Test type with -f option and a command 45 | stdin: type -f ls 46 | 47 | - name: Test type with -a option and a function 48 | stdin: | 49 | function myfunc() { echo "Hello, world!"; } 50 | type -a myfunc 51 | 52 | - name: Test type with hashed path 53 | stdin: | 54 | hash -p /some/ls ls 55 | type ls 56 | 57 | - name: Test type -a with hashed path 58 | stdin: | 59 | hash -p /some/ls ls 60 | type -a ls 61 | 62 | - name: Test type -P with hashed path 63 | stdin: | 64 | hash -p /some/ls ls 65 | type -P ls 66 | 67 | - name: Test type -P -a with hashed path 68 | stdin: | 69 | hash -p /some/ls ls 70 | type -P -a ls 71 | 72 | - name: Test type -p -a with hashed path 73 | stdin: | 74 | hash -p /some/ls ls 75 | type -p -a ls 76 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/typeset.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: typeset" 2 | 3 | cases: 4 | - name: "Display vars" 5 | stdin: | 6 | typeset myvar=something 7 | typeset -p myvar 8 | 9 | typeset myarr=(a b c) 10 | typeset -p myarr 11 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/ulimit.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: ulimit" 2 | cases: 3 | - name: "ulimit -c" 4 | stdin: | 5 | ulimit -c 6 | 7 | - name: "ulimit -d" 8 | stdin: | 9 | ulimit -d 10 | 11 | - name: "ulimit -f" 12 | stdin: | 13 | ulimit -f 14 | 15 | - name: "ulimit -l" 16 | stdin: | 17 | ulimit -l 18 | 19 | - name: "ulimit -m" 20 | stdin: | 21 | ulimit -m 22 | 23 | - name: "ulimit -n" 24 | stdin: | 25 | ulimit -n 26 | 27 | - name: "ulimit -d unlimited" 28 | stdin: | 29 | ulimit -d unlimited 30 | 31 | - name: "ulimit -f unlimited" 32 | stdin: | 33 | ulimit -f unlimited 34 | 35 | - name: "ulimit -m unlimited" 36 | stdin: | 37 | ulimit -m unlimited 38 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/builtins/unalias.yaml: -------------------------------------------------------------------------------- 1 | name: "Builtins: unalias" 2 | cases: 3 | - name: "Unalias basic usage" 4 | stdin: | 5 | shopt -s expand_aliases 6 | alias echo='echo prefixed' 7 | echo 'something' 8 | unalias echo 9 | echo 'something' 10 | 11 | - name: "Unalias non-existent alias" 12 | ignore_stderr: true # Slightly different error messages 13 | stdin: | 14 | shopt -s expand_aliases 15 | unalias not_an_alias 16 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/command_substitution.yaml: -------------------------------------------------------------------------------- 1 | name: "Command substitution" 2 | cases: 3 | - name: "Ignore single quote in comment in command substitution" 4 | stdin: | 5 | var=$( 6 | # I'm 7 | echo "Batman" 8 | ) 9 | echo $var 10 | 11 | - name: "Ignore double quote in comment in command substitution" 12 | stdin: | 13 | var=$( 14 | # This " is not being 15 | echo "parsed" 16 | ) 17 | echo $var 18 | 19 | - name: "Ignore parentheses in comment in command substitution" 20 | stdin: | 21 | var=$( 22 | # :( 23 | echo "Sad" 24 | ) 25 | echo $var 26 | 27 | - name: "Ignore dollar in comment in command substitution" 28 | stdin: | 29 | var=$( 30 | # $ 31 | echo "Mr. Crabs ^" 32 | ) 33 | echo $var 34 | 35 | - name: "Positional parameter count not mistaken for comment" 36 | stdin: | 37 | echo $(echo $#) 38 | 39 | - name: "Ignore single quote in comment in command substitution (backticks)" 40 | stdin: | 41 | var=` 42 | # I'm 43 | echo "Batman" 44 | ` 45 | echo $var 46 | 47 | - name: "Ignore double quote in comment in command substitution (backticks)" 48 | stdin: | 49 | var=` 50 | # This " is not being 51 | echo "parsed" 52 | ` 53 | echo $var 54 | 55 | - name: "Ignore parentheses in comment in command substitution (backticks)" 56 | stdin: | 57 | var=` 58 | # :( 59 | echo "Sad" 60 | ` 61 | echo $var 62 | 63 | - name: "Ignore dollar in comment in command substitution (backticks)" 64 | stdin: | 65 | var=` 66 | # $ 67 | echo "Mr. Crabs ^" 68 | ` 69 | echo $var 70 | 71 | - name: "Positional parameter count not mistaken for comment (backticks)" 72 | stdin: | 73 | echo `echo $#` 74 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/complete_commands.yaml: -------------------------------------------------------------------------------- 1 | name: "Complete commands" 2 | cases: 3 | - name: "Multi-command sequence" 4 | stdin: | 5 | echo 1; echo 2; echo 3 6 | 7 | - name: "Semicolon-terminated sequence" 8 | stdin: | 9 | echo 1; echo 2; 10 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/arithmetic.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: arithmetic" 2 | cases: 3 | - name: "Basic arithmetic statements" 4 | stdin: | 5 | ((0 == 0)) && echo "0 == 0" 6 | ((0 != 0)) && echo "0 != 0" 7 | 8 | - name: "Arithmetic statements with parens" 9 | stdin: | 10 | (( (0) )) && echo "0" 11 | (( (1) )) && echo "1" 12 | 13 | - name: "Arithmetic statements with parens and operators" 14 | stdin: | 15 | (( (0) == 0 )) && echo "0 == 0" 16 | (( (1) != 0 )) && echo "1 != 0" 17 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/arithmetic_for.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: arithmetic for" 2 | cases: 3 | - name: "Single-line arithmetic for loop" 4 | stdin: | 5 | for ((i = 0; i < 5; i++)); do echo $i; done 6 | echo "Result: $?" 7 | 8 | - name: "Break in arithmetic for loop" 9 | stdin: | 10 | for ((i = 0; i < 5; i++)); do 11 | echo $i 12 | break 13 | done 14 | echo "Result: $?" 15 | 16 | - name: "Continue in arithmetic for loop" 17 | stdin: | 18 | for ((i = 0; i < 5; i++)); do 19 | continue 20 | echo "Should not print" 21 | done 22 | echo "Result: $?" 23 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/brace.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: brace" 2 | cases: 3 | - name: "Brace command" 4 | stdin: | 5 | { echo 'hi'; } 6 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/if.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: if" 2 | cases: 3 | - name: "Basic if" 4 | stdin: | 5 | if false; then echo 1; else echo 2; fi 6 | if false; then echo 3; elif false; then echo 4; else echo 5; fi 7 | if true; then echo 6; else echo 7; fi 8 | if false; then echo 8; elif true; then echo 10; else echo 11; fi 9 | 10 | - name: "Multi-line if" 11 | test_files: 12 | - path: "script.sh" 13 | contents: | 14 | if false; then 15 | echo 1 16 | else 17 | echo 2 18 | fi 19 | 20 | if false; then 21 | echo 3 22 | elif false; then 23 | echo 4 24 | else 25 | echo 5 26 | fi 27 | 28 | if true; then 29 | echo 6 30 | else 31 | echo 7 32 | fi 33 | 34 | if false; then 35 | echo 8 36 | elif true; then 37 | echo 10 38 | else 39 | echo 11 40 | fi 41 | args: ["./script.sh"] 42 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/subshell.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: subshell" 2 | cases: 3 | - name: "Basic subshell usage" 4 | stdin: | 5 | (echo hi) 6 | 7 | - name: "Subshells in sequence" 8 | ignore_stderr: true 9 | stdin: | 10 | (echo hi)(echo there) 11 | 12 | - name: "Subshell env usage" 13 | stdin: | 14 | (subshell_var=value && echo "subshell_var: ${subshell_var}") 15 | echo "subshell_var: ${subshell_var}" 16 | 17 | - name: "Subshell output redirection" 18 | stdin: | 19 | (echo Hello; echo world) >out.txt 20 | echo "Dumping out.txt..." 21 | cat out.txt 22 | 23 | - name: "Piped subshell usage" 24 | stdin: | 25 | (echo hi) | wc -l 26 | 27 | - name: "Breaks in subshell" 28 | stdin: | 29 | for i in 1 2 3; do 30 | echo $i 31 | (for i in 1 2 3; do break 2; done) 32 | done 33 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/until.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: until" 2 | cases: 3 | - name: "Single-line until loop" 4 | stdin: | 5 | until true; do echo 'In loop'; done 6 | 7 | - name: "Arithmetic in until loop" 8 | incompatible_configs: ["sh"] 9 | stdin: | 10 | i=5 11 | until ((i == 0)); do echo $i; i=$((i - 1)); done 12 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/compound_cmds/while.yaml: -------------------------------------------------------------------------------- 1 | name: "Compound commands: while" 2 | cases: 3 | - name: "Single-line while loop" 4 | stdin: | 5 | while false; do echo 'In loop'; done 6 | 7 | - name: "break in while loop" 8 | stdin: | 9 | while true; do 10 | echo 'In loop' 11 | break 12 | done 13 | 14 | - name: "break 2 in nested loops" 15 | stdin: | 16 | while false; do 17 | echo 'Starting inner loop' 18 | while true; do 19 | echo 'In loop' 20 | break 2 21 | done 22 | echo 'Finished inner loop' 23 | done 24 | 25 | - name: "Arithmetic in while loop" 26 | stdin: | 27 | i=5 28 | while ((i > 0)); do echo $i; i=$((i - 1)); done 29 | 30 | - name: "Alternative arithmetic in while loop" 31 | stdin: | 32 | c=0 33 | limit=4 34 | while [ $c -lt $limit ]; do 35 | case "$c" in 36 | 0) 37 | echo "0" 38 | ;; 39 | 1) 40 | echo "1" 41 | ;; 42 | *) 43 | break 44 | ;; 45 | esac 46 | ((c++)) 47 | done 48 | echo "Done" 49 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/functions.yaml: -------------------------------------------------------------------------------- 1 | name: "Functions" 2 | cases: 3 | - name: "Basic function invocation" 4 | test_files: 5 | - path: "script.sh" 6 | contents: | 7 | myfunc() { 8 | echo "In myfunc()." 9 | } 10 | echo "Calling myfunc()..." 11 | myfunc 12 | echo "Returned." 13 | args: ["./script.sh"] 14 | 15 | - name: "Function invocation with args" 16 | test_files: 17 | - path: "script.sh" 18 | contents: | 19 | myfunc() { 20 | echo "In myfunc()" 21 | echo "1: $1" 22 | echo "*: $*" 23 | } 24 | echo "Calling myfunc()..." 25 | myfunc a b c 26 | echo "Returned." 27 | args: ["./script.sh"] 28 | 29 | - name: "Function invocation with empty arg" 30 | stdin: | 31 | myfunc() { 32 | echo "count: ${#*}" 33 | echo "\$1: $1" 34 | echo "\$2: $2" 35 | echo "\$3: $3" 36 | } 37 | 38 | myfunc a b c 39 | myfunc a "" c 40 | 41 | - name: "Function definition with output redirection" 42 | stdin: | 43 | myfunc() { 44 | echo "In myfunc()" 45 | } >>./test.txt 46 | 47 | myfunc 48 | myfunc 49 | 50 | - name: "Function call with env variables" 51 | stdin: | 52 | myfunc() { 53 | echo ${myvar} 54 | } 55 | 56 | myvar="default" 57 | myfunc 58 | myvar="overridden" myfunc 59 | myfunc 60 | 61 | - name: "Function definition without braces" 62 | stdin: | 63 | myfunc() 64 | if true; then 65 | echo true 66 | else 67 | echo false 68 | fi 69 | 70 | myfunc 71 | 72 | - name: "Nested function definition" 73 | stdin: | 74 | outer() { 75 | echo "Entered outer" 76 | 77 | inner() { 78 | echo "In inner" 79 | } 80 | 81 | echo "Invoking inner" 82 | 83 | inner 84 | 85 | echo "Returning from outer" 86 | } 87 | 88 | echo "Calling outer from toplevel" 89 | outer 90 | 91 | echo "Calling inner from toplevel" 92 | inner 93 | 94 | - name: "Exporting functions to child instance" 95 | stdin: | 96 | mytestfunc() { 97 | echo "In mytestfunc" 98 | } 99 | 100 | export -f mytestfunc 101 | 102 | $0 -c 'mytestfunc' 103 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/here.yaml: -------------------------------------------------------------------------------- 1 | name: "Here docs/strings" 2 | cases: 3 | - name: "Basic here doc" 4 | stdin: | 5 | cat < non-existing-file 54 | echo "Result (non existing): $?" 55 | echo "File contents: $(cat non-existing-file)" 56 | echo 57 | 58 | echo hi > /dev/null 59 | echo "Result (device file): $?" 60 | echo 61 | 62 | echo hi > existing-file 63 | echo "Result (existing file): $?" 64 | echo "File contents: $(cat existing-file)" 65 | echo 66 | 67 | echo hi >| existing-file 68 | echo "Result (clobber): $?" 69 | echo "File contents: $(cat existing-file)" 70 | echo 71 | 72 | - name: "set -x" 73 | stdin: | 74 | set -x 75 | 76 | ls 77 | 78 | for f in 1 2 3; do echo ${f}; done 79 | 80 | case 1 in 81 | 1) echo "1";; 82 | *) echo "not";; 83 | esac 84 | 85 | while false; do 86 | echo body 87 | done 88 | 89 | newvar=$(echo "new") 90 | 91 | x=$((3 + 7)) 92 | 93 | if [[ x == 10 && ! 0 ]]; then 94 | echo "Math checks" 95 | fi 96 | 97 | var="x" 98 | [[ ${var} && ${var//[[:space:]]/} ]] 99 | 100 | for ((i = 0; i < 3; i++)); do 101 | echo $i 102 | done 103 | 104 | ((x = 3)) || ((x = 4)) 105 | 106 | override=value echo some_output 107 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: "Pipeline" 2 | cases: 3 | - name: "Basic pipe" 4 | stdin: | 5 | echo hi | grep -l h 6 | 7 | - name: "Longer pipe" 8 | stdin: | 9 | echo hi | grep h | wc -l 10 | 11 | - name: "Invert result" 12 | stdin: | 13 | ! false 14 | echo "! false: $?" 15 | ! true 16 | echo "! true: $?" 17 | 18 | - name: "Exit codes for piped commands" 19 | test_files: 20 | - path: "script.sh" 21 | executable: true 22 | contents: | 23 | #!/bin/sh 24 | (cat; echo -n "-> $1") 25 | exit $1 26 | stdin: | 27 | ./script.sh 10 | ./script.sh 0 | ./script.sh 33 28 | 29 | - name: "Side effects in pipe commands" 30 | stdin: | 31 | var=0 32 | echo "var: ${var}" 33 | { var=1; } 34 | echo "var: ${var}" 35 | { var=2; echo hi; } | cat 36 | echo "var: ${var}" 37 | echo hi | { var=3; cat; } 38 | echo "var: ${var}" 39 | 40 | - name: "pipeline extension" 41 | stdin: | 42 | echo -e "hello" |& wc -l 43 | cat dfdfgdfgdf |& wc -l 44 | foo() { cat dfgdfg; } |& wc -l 45 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/process.yaml: -------------------------------------------------------------------------------- 1 | name: "Process" 2 | common_test_files: 3 | - path: "process-helpers.sh" 4 | source_path: "../utils/process-helpers.sh" 5 | 6 | cases: 7 | - name: "Basic process" 8 | stdin: | 9 | # TODO: Figure out how to make this work elsewhere 10 | if [[ "$(uname)" != "Linux" ]]; then 11 | echo "Skipping test on non-Linux platform" 12 | exit 0 13 | fi 14 | 15 | source process-helpers.sh 16 | echo "pid != ppid: $(( $(get-pid) != $(get-ppid) ))" 17 | echo "pid == pgrp: $(( $(get-pid) != $(get-pgrp) ))" 18 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/simple_commands.yaml: -------------------------------------------------------------------------------- 1 | name: "Simple commands" 2 | cases: 3 | - name: "Simple command" 4 | stdin: | 5 | echo 1 6 | 7 | - name: "Simple command with reserved word args" 8 | stdin: | 9 | echo then 10 | 11 | - name: "Command that's a directory" 12 | ignore_stderr: true 13 | stdin: | 14 | mkdir test-dir 15 | ./test-dir 16 | echo "Result: $?" 17 | 18 | - name: "Non-existent command" 19 | ignore_stderr: true 20 | stdin: | 21 | ./non-existent-command 22 | echo "Result: $?" 23 | 24 | - name: "Redirection of errors" 25 | stdin: | 26 | ./non-existent-command 2>/dev/null 27 | echo "Result: $?" 28 | 29 | - name: "Simple command with non-existent cwd" 30 | # N.B. We intentionally fail here because there's no safe way to pick an 31 | # alternate working directory that we are okay with. To our knowledge, 32 | # there's no easy way to hold onto the current working directory past 33 | # its deletion. 34 | known_failure: true 35 | stdin: | 36 | mkdir test-dir 37 | cd test-dir 38 | rmdir $(pwd) 39 | ls 40 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/special_parameters.yaml: -------------------------------------------------------------------------------- 1 | name: "Special parameters" 2 | cases: 3 | - name: "$@ and $*" 4 | stdin: | 5 | function myfunc() { 6 | echo "--myfunc called--" 7 | 8 | echo "COUNT: $#" 9 | echo "AT-SIGN VALUE: '$@'" 10 | echo "STAR VALUE: '$*'" 11 | 12 | for arg in $@; do 13 | echo "AT-SIGN ELEMENT: '${arg}'" 14 | done 15 | 16 | for arg in $*; do 17 | echo "STAR ELEMENT: '${arg}'" 18 | done 19 | 20 | for arg in "$@"; do 21 | echo "DOUBLE-QUOTED AT-SIGN ELEMENT: '${arg}'" 22 | done 23 | 24 | for arg in "$*"; do 25 | echo "DOUBLE-QUOTED STAR ELEMENT: '${arg}'" 26 | done 27 | } 28 | 29 | myfunc 30 | myfunc 1 31 | myfunc 1 2 32 | myfunc "a b c" 2 33 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/status.yaml: -------------------------------------------------------------------------------- 1 | name: "Status tests" 2 | cases: 3 | - name: "Basic status" 4 | stdin: | 5 | cat /non/existent >/dev/null 6 | echo "[1] Status: $?; pipe status: ${PIPESTATUS[@]}" 7 | 8 | - name: "Parse error status" 9 | known_failure: true # Needs investigation 10 | ignore_stderr: true 11 | stdin: | 12 | # Generate parse error 13 | for f done 14 | echo "[2] Status: $?; pipe status: ${PIPESTATUS[@]}" 15 | 16 | - name: "Pipeline status" 17 | stdin: | 18 | /non/existent/program 2>/dev/null | cat 19 | echo "Status: $?; pipe status: ${PIPESTATUS[@]}" 20 | 21 | - name: "Command substitution status" 22 | stdin: | 23 | x=$(echo hi | wc -l) 24 | echo "[1] Status: $?; pipe status: ${PIPESTATUS[@]}" 25 | 26 | x=$(cat /non/existent 2>/dev/null) 27 | echo "[2] Status: $?; pipe status: ${PIPESTATUS[@]}" 28 | 29 | - name: "Subshell status" 30 | stdin: | 31 | (echo hi | wc -l) 32 | echo "[1] Status: $?; pipe status: ${PIPESTATUS[@]}" 33 | 34 | (cat /non/existent 2>/dev/null) 35 | echo "[2] Status: $?; pipe status: ${PIPESTATUS[@]}" 36 | -------------------------------------------------------------------------------- /brush-shell/tests/cases/variables.yaml: -------------------------------------------------------------------------------- 1 | name: "Variable tests" 2 | cases: 3 | - name: "Appending to a variable" 4 | stdin: | 5 | x=something 6 | x+=here 7 | echo "x: ${x}" 8 | 9 | - name: "Appending an integer to an integer variable" 10 | stdin: | 11 | declare -i x=10 12 | echo "x: ${x}" 13 | x+=5 14 | echo "x: ${x}" 15 | x+=value 16 | echo "x: ${x}" 17 | 18 | y=value 19 | declare -i y 20 | echo "y: ${y}" 21 | y+=5 22 | echo "y: ${y}" 23 | 24 | - name: "Append to an unset variable" 25 | stdin: | 26 | declare -a myvar 27 | myvar+=abc 28 | echo "myvar: ${myvar}" 29 | 30 | declare -i myint 31 | myint+=abc 32 | echo "myint: ${myint}" 33 | -------------------------------------------------------------------------------- /brush-shell/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for brush shell 2 | 3 | // For now, only compile this for Unix-like platforms (Linux, macOS). 4 | #![cfg(unix)] 5 | #![allow(clippy::panic_in_result_fn)] 6 | 7 | use anyhow::Context; 8 | 9 | #[test] 10 | fn get_version_variables() -> anyhow::Result<()> { 11 | let shell_path = assert_cmd::cargo::cargo_bin("brush"); 12 | let brush_ver_str = get_variable(&shell_path, "BRUSH_VERSION")?; 13 | let bash_ver_str = get_variable(&shell_path, "BASH_VERSION")?; 14 | 15 | assert_eq!(brush_ver_str, env!("CARGO_PKG_VERSION")); 16 | assert_ne!( 17 | brush_ver_str, bash_ver_str, 18 | "Should differ for scripting use-case" 19 | ); 20 | 21 | Ok(()) 22 | } 23 | 24 | fn get_variable(shell_path: &std::path::Path, var: &str) -> anyhow::Result { 25 | let output = std::process::Command::new(shell_path) 26 | .arg("--norc") 27 | .arg("--noprofile") 28 | .arg("-c") 29 | .arg(format!("echo -n ${{{var}}}")) 30 | .output() 31 | .with_context(|| format!("failed to retrieve {var}"))? 32 | .stdout; 33 | Ok(String::from_utf8(output)?) 34 | } 35 | -------------------------------------------------------------------------------- /brush-shell/tests/utils/process-helpers.sh: -------------------------------------------------------------------------------- 1 | function get-proc-stat-value() { 2 | cat /proc/self/stat | cut -d ' ' --output-delimiter=, -f$1 3 | } 4 | 5 | function get-pid() { 6 | get-proc-stat-value 1 7 | } 8 | 9 | function get-ppid() { 10 | get-proc-stat-value 4 11 | } 12 | 13 | function get-pgrp() { 14 | get-proc-stat-value 5 15 | } 16 | 17 | function get-session-id() { 18 | get-proc-stat-value 6 19 | } 20 | 21 | function get-tty-nr() { 22 | get-proc-stat-value 7 23 | } 24 | 25 | function get-term-pgid() { 26 | get-proc-stat-value 8 27 | } 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # brush documentation 2 | 3 | The docs are grouped into: 4 | 5 | * [How-to guides](how-to/README.md) 6 | * [Tutorials](tutorials/README.md) 7 | * [Reference material](reference/README.md) 8 | 9 | If you're just getting started building this project, you should consult the [How to Build](how-to/build.md) guide. 10 | 11 | --- 12 | 13 | > _Note: The structure of our docs is inspired by the [Diátaxis](https://diataxis.fr/) approach; we've found over time that best helps readers find the material most relevant to them, as well as provides a rough shape for where to place the right docs._ 14 | -------------------------------------------------------------------------------- /docs/demos/demo.tape: -------------------------------------------------------------------------------- 1 | # Works with https://github.com/charmbracelet/vhs 2 | 3 | Output sample.gif 4 | 5 | Set FontFamily "CaskaydiaMono Nerd Font Mono" 6 | Set FontSize 20 7 | Set Theme "Monokai Pro" 8 | Set Width 1600 9 | Set Height 600 10 | Set CursorBlink false 11 | 12 | # Setup environment to launch brush in 13 | Env HISTFILE "" 14 | Env PS1 '$0$ ' 15 | 16 | # Launch brush and set up bash-completion 17 | Hide 18 | Type `brush --enable-highlighting --norc --noprofile` 19 | Enter 20 | Type `source /usr/share/bash-completion/bash_completion && clear` 21 | Enter 22 | Show 23 | 24 | # Enable starship 25 | Type `# Let's start with a better prompt. starship to the rescue!` 26 | Sleep 0.8s 27 | Enter 28 | Type `eval "$(starship init bash)"` 29 | Sleep 0.8s 30 | Enter 31 | Sleep 1s 32 | 33 | # git describe 34 | Type `git d` 35 | Tab 36 | Sleep 1.3s 37 | Enter 38 | Sleep 0.4s 39 | Type `--l` 40 | Sleep 0.4s 41 | Tab 42 | Sleep 0.8s 43 | Type `brush-she` 44 | Sleep 0.5s 45 | Tab 46 | Sleep 0.2s 47 | Right 48 | Sleep 0.2s 49 | Right 50 | Sleep 0.8s 51 | Enter 52 | Sleep 0.8s 53 | Enter 54 | Sleep 1s 55 | 56 | # vim 57 | Type `vim Ca` 58 | Sleep 0.5s 59 | Tab 60 | Sleep 0.8s 61 | Right 62 | Sleep 0.5s 63 | Enter 64 | Sleep 1s 65 | Enter 66 | 67 | Type `1G` 68 | Type `i` 69 | Enter 70 | Up 71 | Type `# Let's try suspending vim...` 72 | Enter 73 | Escape 74 | Sleep 1s 75 | Ctrl+Z 76 | Sleep 0.7s 77 | 78 | Type `# Yep, it's suspended.` 79 | Sleep 0.4s 80 | Type ` Let's bring it back.` 81 | Enter 82 | Sleep 0.8s 83 | 84 | Type `fg` 85 | Sleep 0.4s 86 | Enter 87 | Sleep 0.6s 88 | 89 | Type `:q!` 90 | Sleep 0.3s 91 | Enter 92 | Sleep 0.5s 93 | Ctrl+L 94 | Sleep 0.2s 95 | 96 | Type `# Let's properly greet the world.` 97 | Enter 98 | Sleep 0.4s 99 | 100 | # Figure out version 101 | Type `verline=$(help | head -n1)` 102 | Sleep 0.2s 103 | Enter 104 | Sleep 0.3s 105 | Type `[[ "${verline}" =~ ^.*version\ ([[:digit:]\.]+).*$ ]] && ver=${BASH_REMATCH[1]}` 106 | Sleep 0.4s 107 | Enter 108 | Sleep 0.2s 109 | Type `declare -p ver` 110 | Enter 111 | Sleep 0.4s 112 | 113 | # Declare function 114 | Type `function greet() {` 115 | Enter 116 | Type ` echo "Hello from brush ${ver}!"` 117 | Enter 118 | Type `}` 119 | Sleep 1s 120 | Enter 121 | Type `type greet` 122 | Sleep 0.4s 123 | Enter 124 | Sleep 1s 125 | 126 | # Use function 127 | Ctrl+L 128 | Type `for ((i = 0; i < 5; i++)); do greet; done` 129 | Sleep 1s 130 | Enter 131 | Sleep 0.8s 132 | 133 | Type `# Surely we can make that more colorful.` 134 | Enter 135 | Sleep 1s 136 | 137 | # Now with lolcat 138 | Type `for ` 139 | Sleep 1s 140 | Right 141 | Sleep 0.8s 142 | Type ` | lolcat -F 0.3 -S 12` 143 | Sleep 0.6s 144 | Enter 145 | 146 | Sleep 3s 147 | -------------------------------------------------------------------------------- /docs/extras/brush-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reubeno/brush/eb92d134000318eb880841c973a777a98a0abb9e/docs/extras/brush-screenshot.png -------------------------------------------------------------------------------- /docs/how-to/README.md: -------------------------------------------------------------------------------- 1 | # How-to guides 2 | 3 | * [How to build](build.md) 4 | * [How to run tests](run-tests.md) 5 | * [How to run benchmarks](run-benchmarks.md) 6 | * [How to release](release.md) 7 | -------------------------------------------------------------------------------- /docs/how-to/build.md: -------------------------------------------------------------------------------- 1 | # How to build and run 2 | 3 | 1. Install Rust toolchain. We recommend using [rustup](https://rustup.rs/). 4 | 1. Build `brush`: `cargo build` 5 | 1. Run `brush`: `cargo run` 6 | -------------------------------------------------------------------------------- /docs/how-to/release.md: -------------------------------------------------------------------------------- 1 | # How to release 2 | 3 | _(This is only relevant for project maintainers.)_ 4 | 5 | * Install [release-plz](https://github.com/MarcoIeni/release-plz) 6 | * Checkout the `main` branch (with a clean working tree). 7 | * Run: `release-plz update`. Review its changes, notable including the changelog updates. 8 | * PR through any generated changes with a `chore: prepare release` commit summary. 9 | * After the changes have merged into `main`, update your local `main` branch. 10 | * Acquire GitHub and `crates.io` tokens that have sufficient permissions to publish. 11 | * Authenticate with `crates.io` by running: `cargo login`. 12 | * Run: `release-plz release --backend github --git-token `. 13 | * Update the published GitHub release to include an auto-generated changelog. 14 | * Run: `cargo install --locked brush-shell` to verify the release. 15 | -------------------------------------------------------------------------------- /docs/how-to/run-benchmarks.md: -------------------------------------------------------------------------------- 1 | # How to run benchmarks 2 | 3 | To run performance benchmarks: 4 | 5 | ```bash 6 | cargo bench --workspace 7 | ``` 8 | 9 | ## Collecting flamegraphs 10 | 11 | To collect flamegraphs from performance benchmarks (running for 10 seconds): 12 | 13 | ```bash 14 | cargo bench --workspace -- --profile-time 10 15 | ``` 16 | 17 | The flamegraphs will be created as `.svg` files and placed under `target/criterion//profile`. 18 | -------------------------------------------------------------------------------- /docs/how-to/run-tests.md: -------------------------------------------------------------------------------- 1 | # How to run tests 2 | 3 | To run all workspace tests: 4 | 5 | ```bash 6 | cargo test --workspace 7 | ``` 8 | 9 | To run just bash compatibility tests: 10 | 11 | ```bash 12 | cargo test --test brush-compat-tests 13 | ``` 14 | 15 | To run a specific compatibility test case 16 | 17 | ```bash 18 | cargo test --test brush-compat-tests -- '' 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/reference/README.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | * [Integration testing](integration-testing.md) 4 | -------------------------------------------------------------------------------- /docs/reference/integration-testing.md: -------------------------------------------------------------------------------- 1 | # Integration testing 2 | 3 | Our approach to integration testing relies heavily on using test oracles to provide the "correct" answers/expectations for test cases. In practice, we use existing alternate shell implementations as oracles. 4 | 5 | Test cases are defined in YAML files. The test cases defined in a given file comprise a test case set. Running the integration tests for this project executes test case sets in parallel. 6 | 7 | ```yaml 8 | name: "Example tests" 9 | cases: 10 | - name: "Basic usage" 11 | stdin: | 12 | echo hi 13 | ``` 14 | 15 | This defines a new test case set with the name "Example tests". It contains one defined test case called "Basic usage". This test case will launch the shell without any additional custom arguments (beyond a few standard ones to disable processing default profiles and rc files), write "echo hi" (with a trailing newline) to stdin of the shell, and then close that stream. The test harness will capture the shell's stdout, stderr, and exit code. After repeating these steps with the test oracle, each of these 3 data are compared. An error is flagged if any of the 3 differ. 16 | 17 | Test cases are run with the working directory initialized to a temporary directory. The contents of the temporary directory are inspected after the shell-under-test has exited, and compared against their counterparts in the oracle's run. This enables easy checking of files created, deleted, or mutated as side effects of running the test case. -------------------------------------------------------------------------------- /docs/tutorials/README.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | _To be written_ -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brush-fuzz" 3 | description = "Fuzz tests for brush" 4 | publish = false 5 | version = "0.2.2" 6 | authors.workspace = true 7 | categories.workspace = true 8 | edition.workspace = true 9 | keywords.workspace = true 10 | license.workspace = true 11 | readme.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | 15 | [package.metadata] 16 | cargo-fuzz = true 17 | 18 | [lints] 19 | workspace = true 20 | 21 | [dependencies] 22 | anyhow = "1.0.98" 23 | assert_cmd = "2.0.17" 24 | lazy_static = "1.5.0" 25 | libfuzzer-sys = "0.4" 26 | tokio = { version = "1.45.1", features = ["rt"] } 27 | 28 | [dependencies.brush-core] 29 | path = "../brush-core" 30 | 31 | [dependencies.brush-parser] 32 | path = "../brush-parser" 33 | features = ["fuzz-testing"] 34 | 35 | [[bin]] 36 | name = "fuzz_parse" 37 | path = "fuzz_targets/fuzz_parse.rs" 38 | test = false 39 | doc = false 40 | bench = false 41 | 42 | [[bin]] 43 | name = "fuzz_arithmetic" 44 | path = "fuzz_targets/fuzz_arithmetic.rs" 45 | test = false 46 | doc = false 47 | bench = false 48 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_arithmetic.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use anyhow::Result; 4 | use brush_parser::ast; 5 | use libfuzzer_sys::fuzz_target; 6 | 7 | lazy_static::lazy_static! { 8 | static ref TOKIO_RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new().unwrap(); 9 | static ref SHELL_TEMPLATE: brush_core::Shell = { 10 | let options = brush_core::CreateOptions { 11 | no_profile: true, 12 | no_rc: true, 13 | ..Default::default() 14 | }; 15 | TOKIO_RT.block_on(brush_core::Shell::new(&options)).unwrap() 16 | }; 17 | } 18 | 19 | fn eval_arithmetic(mut shell: brush_core::Shell, input: &ast::ArithmeticExpr) -> Result<()> { 20 | const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15; 21 | 22 | // 23 | // Turn it back into a string so we can pass it in on the command-line. 24 | // 25 | let input_str = input.to_string(); 26 | 27 | // 28 | // Instantiate a brush shell with defaults, then try to evaluate the expression. 29 | // 30 | let parsed_expr = brush_parser::arithmetic::parse(input_str.as_str()).ok(); 31 | let our_eval_result = if let Some(parsed_expr) = parsed_expr { 32 | shell.eval_arithmetic(&parsed_expr).ok() 33 | } else { 34 | None 35 | }; 36 | 37 | // 38 | // Now run it under 'bash' 39 | // 40 | let mut oracle_cmd = std::process::Command::new("bash"); 41 | oracle_cmd 42 | .arg("--noprofile") 43 | .arg("--norc") 44 | .arg("-O") 45 | .arg("extglob") 46 | .arg("-t"); 47 | 48 | let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd); 49 | 50 | oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS)); 51 | 52 | let input = std::format!("echo \"$(( {input_str} ))\"\n"); 53 | oracle_cmd.write_stdin(input.as_bytes()); 54 | 55 | let oracle_result = oracle_cmd.output()?; 56 | let oracle_eval_result = if oracle_result.status.success() { 57 | let oracle_output = std::str::from_utf8(&oracle_result.stdout)?; 58 | oracle_output.trim().parse::().ok() 59 | } else { 60 | None 61 | }; 62 | 63 | // 64 | // Compare results. 65 | // 66 | if our_eval_result != oracle_eval_result { 67 | Err(anyhow::anyhow!( 68 | "Mismatched eval results: {oracle_eval_result:?} from oracle vs. {our_eval_result:?} from our test (expr: '{input_str}', oracle result: {oracle_result:?})" 69 | )) 70 | } else { 71 | Ok(()) 72 | } 73 | } 74 | 75 | fuzz_target!(|input: ast::ArithmeticExpr| { 76 | let s = input.to_string(); 77 | let s = s.trim(); 78 | 79 | // For now, intentionally ignore known problematic cases without actually running them. 80 | if s.contains("+ 0") 81 | || s.is_empty() 82 | || s.contains(|c: char| c.is_ascii_control() || !c.is_ascii()) 83 | || s.contains("$[") 84 | // old deprecated form of arithmetic expansion 85 | { 86 | return; 87 | } 88 | 89 | let shell = SHELL_TEMPLATE.clone(); 90 | eval_arithmetic(shell, &input).unwrap(); 91 | }); 92 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_parse.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use anyhow::Result; 4 | use libfuzzer_sys::fuzz_target; 5 | 6 | lazy_static::lazy_static! { 7 | static ref TOKIO_RT: tokio::runtime::Runtime = tokio::runtime::Runtime::new().unwrap(); 8 | static ref SHELL_TEMPLATE: brush_core::Shell = { 9 | let options = brush_core::CreateOptions { 10 | no_profile: true, 11 | no_rc: true, 12 | ..Default::default() 13 | }; 14 | TOKIO_RT.block_on(brush_core::Shell::new(&options)).unwrap() 15 | }; 16 | } 17 | 18 | #[allow(clippy::unused_async)] 19 | async fn parse_async(shell: brush_core::Shell, input: String) -> Result<()> { 20 | const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15; 21 | 22 | // 23 | // Instantiate a brush shell with defaults, then try to parse the input. 24 | // 25 | let our_parse_result = shell.parse_string(input.clone()); 26 | 27 | // 28 | // Now run it under 'bash -n -t' as a crude way to see if it's at least valid syntax. 29 | // 30 | let mut oracle_cmd = std::process::Command::new("bash"); 31 | oracle_cmd 32 | .arg("--noprofile") 33 | .arg("--norc") 34 | .arg("-O") 35 | .arg("extglob") 36 | .arg("-n") 37 | .arg("-t"); 38 | 39 | let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd); 40 | 41 | oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS)); 42 | 43 | let mut input = input; 44 | input.push('\n'); 45 | oracle_cmd.write_stdin(input.as_bytes()); 46 | 47 | let oracle_result = oracle_cmd.output()?; 48 | 49 | // 50 | // Compare results. 51 | // 52 | if our_parse_result.is_ok() != oracle_result.status.success() { 53 | Err(anyhow::anyhow!( 54 | "Mismatched parse results: {oracle_result:?} vs {our_parse_result:?}" 55 | )) 56 | } else { 57 | Ok(()) 58 | } 59 | } 60 | 61 | fuzz_target!(|input: String| { 62 | // Ignore known problematic cases without actually running them. 63 | if input.is_empty() 64 | || input.contains(|c: char| c.is_ascii_control() || !c.is_ascii()) // non-ascii chars (or control sequences) 65 | || input.contains('!') // history expansions 66 | || (input.contains('[') && !input.contains(']')) // ??? 67 | || input.contains("<<") // weirdness with here docs 68 | || input.ends_with('\\') // unterminated trailing escape char? 69 | || input.contains("|&") 70 | // unimplemented bash-ism 71 | { 72 | return; 73 | } 74 | 75 | let shell = SHELL_TEMPLATE.clone(); 76 | TOKIO_RT.block_on(parse_async(shell, input)).unwrap(); 77 | }); 78 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | # disable the changelog for all packages 3 | # ref: https://release-plz.ieni.dev/docs/extra/single-changelog 4 | changelog_config = "cliff.toml" 5 | changelog_update = false 6 | git_release_enable = false 7 | publish = true 8 | 9 | [[package]] 10 | name = "brush-shell" 11 | changelog_update = true 12 | changelog_path = "./CHANGELOG.md" 13 | changelog_include = ["brush-core", "brush-interactive-shell", "brush-parser"] 14 | git_release_latest = true 15 | git_release_draft = true 16 | git_release_enable = true 17 | git_release_name = "brush v{{ version }}" 18 | git_release_body = """ 19 | {{ changelog }} 20 | {% if remote.contributors %} 21 | ### Contributors 22 | {% for contributor in remote.contributors %} 23 | * @{{ contributor.username }} 24 | {% endfor %} 25 | {% endif %} 26 | """ 27 | 28 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | comment_width = 100 3 | wrap_comments = true 4 | -------------------------------------------------------------------------------- /scripts/summarize-pytest-results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import argparse 3 | import json 4 | 5 | parser = argparse.ArgumentParser(description='Summarize pytest results') 6 | parser.add_argument("-r", "--results", dest="results_file_path", type=str, required=True, help="Path to .json pytest results file") 7 | parser.add_argument("--title", dest="title", type=str, default="Pytest results", help="Title to display") 8 | 9 | args = parser.parse_args() 10 | 11 | with open(args.results_file_path, "r") as results_file: 12 | results = json.load(results_file) 13 | 14 | summary = results["summary"] 15 | 16 | error_count = summary.get("error") or 0 17 | fail_count = summary.get("failed") or 0 18 | pass_count = summary.get("passed") or 0 19 | skip_count = summary.get("skipped") or 0 20 | expected_fail_count = summary.get("xfailed") or 0 21 | unexpected_pass_count = summary.get("xpassed") or 0 22 | 23 | total_count = summary.get("total") or 0 24 | collected_count = summary.get("collected") or 0 25 | deselected_count = summary.get("deselected") or 0 26 | 27 | # 28 | # Output 29 | # 30 | 31 | print(f"# {args.title}") 32 | 33 | print(f"| Outcome | Count | Percentage |") 34 | print(f"| ------------------ | ----------------------: | ---------: |") 35 | print(f"| ✅ Pass | {pass_count} | {pass_count * 100 / total_count:.2f} |") 36 | 37 | if error_count > 0: 38 | print(f"| ❗️ Error | {error_count} | {error_count * 100 / total_count:.2f} |") 39 | if fail_count > 0: 40 | print(f"| ❌ Fail | {fail_count} | {fail_count * 100 / total_count:.2f} |") 41 | if skip_count > 0: 42 | print(f"| ⏩ Skip | {skip_count} | {skip_count * 100 / total_count:.2f} |") 43 | if expected_fail_count > 0: 44 | print(f"| ❎ Expected Fail | {expected_fail_count} | {expected_fail_count * 100 / total_count:.2f} |") 45 | if unexpected_pass_count > 0: 46 | print(f"| ✔️ Unexpected Pass | {unexpected_pass_count} | {unexpected_pass_count * 100 / total_count:.2f} |") 47 | 48 | print(f"| 📊 Total | {total_count} | {total_count * 100 / total_count:.2f} |") 49 | -------------------------------------------------------------------------------- /scripts/test-code-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | set -euo pipefail 3 | 4 | script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) 5 | workspace_root="$(realpath "${script_dir}/..")" 6 | 7 | export CARGO_TARGET_DIR="${workspace_root}/target/cov" 8 | 9 | cd "${workspace_root}" 10 | source <(cargo llvm-cov show-env --export-prefix) 11 | 12 | cargo llvm-cov clean --workspace 13 | 14 | cargo test -- --show-output || true 15 | 16 | cargo llvm-cov report --html --ignore-filename-regex "target/.*" -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | publish = false 4 | version = "0.1.0" 5 | authors.workspace = true 6 | categories.workspace = true 7 | edition.workspace = true 8 | keywords.workspace = true 9 | license.workspace = true 10 | readme.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | anyhow = "1.0.98" 19 | brush-shell = { version = "^0.2.18", path = "../brush-shell" } 20 | clap = { version = "4.5.37", features = ["derive"] } 21 | clap_mangen = "0.2.26" 22 | clap-markdown = "0.1.5" 23 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use clap::{CommandFactory, Parser}; 5 | 6 | #[derive(Parser)] 7 | struct CommandLineArgs { 8 | #[clap(subcommand)] 9 | command: Command, 10 | } 11 | 12 | #[derive(Parser)] 13 | enum Command { 14 | /// Generate man content. 15 | GenerateMan(GenerateManArgs), 16 | /// Generate help content in markdown format. 17 | GenerateMarkdown(GenerateMarkdownArgs), 18 | } 19 | 20 | #[derive(Parser)] 21 | struct GenerateManArgs { 22 | /// Output directory. 23 | #[clap(long = "output-dir", short = 'o')] 24 | output_dir: PathBuf, 25 | } 26 | 27 | #[derive(Parser)] 28 | struct GenerateMarkdownArgs { 29 | /// Output directory. 30 | #[clap(long = "out", short = 'o')] 31 | output_path: PathBuf, 32 | } 33 | 34 | fn main() -> Result<()> { 35 | let args = CommandLineArgs::parse(); 36 | 37 | match &args.command { 38 | Command::GenerateMan(gen_args) => generate_man(gen_args), 39 | Command::GenerateMarkdown(gen_args) => generate_markdown(gen_args), 40 | } 41 | } 42 | 43 | fn generate_man(args: &GenerateManArgs) -> Result<()> { 44 | // Create the output dir if it doesn't exist. If it already does, we proceed 45 | // onward and hope for the best. 46 | if !args.output_dir.exists() { 47 | std::fs::create_dir_all(&args.output_dir)?; 48 | } 49 | 50 | // Generate! 51 | let cmd = brush_shell::args::CommandLineArgs::command(); 52 | clap_mangen::generate_to(cmd, &args.output_dir)?; 53 | 54 | Ok(()) 55 | } 56 | 57 | fn generate_markdown(args: &GenerateMarkdownArgs) -> Result<()> { 58 | let options = clap_markdown::MarkdownOptions::new() 59 | .show_footer(false) 60 | .show_table_of_contents(true); 61 | 62 | // Generate! 63 | let markdown = 64 | clap_markdown::help_markdown_custom::(&options); 65 | std::fs::write(&args.output_path, markdown)?; 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | unpinned-uses: 3 | config: 4 | policies: 5 | # For now, we allow rust-toolchain to be used with a branch. 6 | dtolnay/rust-toolchain: any 7 | --------------------------------------------------------------------------------