{"id":28450121,"url":"https://github.com/raphamorim/flash","last_synced_at":"2025-06-11T23:02:46.171Z","repository":{"id":291170705,"uuid":"970278013","full_name":"raphamorim/flash","owner":"raphamorim","description":"⚡ shell parser, formatter, and interpreter with Bash support","archived":false,"fork":false,"pushed_at":"2025-06-06T11:15:16.000Z","size":888,"stargazers_count":40,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-06T15:06:33.886Z","etag":null,"topics":["bash","bash-parser","rust","rust-lang","shell","shell-parser"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/raphamorim.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-04-21T19:03:40.000Z","updated_at":"2025-06-06T11:15:17.000Z","dependencies_parsed_at":"2025-05-02T21:30:53.915Z","dependency_job_id":"c38b0ae9-40c0-47de-a97d-c49c90cd880d","html_url":"https://github.com/raphamorim/flash","commit_stats":null,"previous_names":["raphamorim/myst","raphamorim/flash"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/raphamorim/flash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raphamorim%2Fflash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raphamorim%2Fflash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raphamorim%2Fflash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raphamorim%2Fflash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/raphamorim","download_url":"https://codeload.github.com/raphamorim/flash/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raphamorim%2Fflash/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259360728,"owners_count":22845817,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","bash-parser","rust","rust-lang","shell","shell-parser"],"created_at":"2025-06-06T15:06:33.504Z","updated_at":"2025-06-11T23:02:46.165Z","avatar_url":"https://github.com/raphamorim.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"# ϟ Flash (work in progress)\n\n*A shell parser, formatter, and interpreter written in Rust.*\n\nFlash is a fast, extensible, and hackable toolkit for working with POSIX-style shell scripts. It includes a parser, formatter, and interpreter built from scratch in Rust. Flash understands real-world shell syntax and provides structured AST access for static analysis, tooling, and transformation.\n\n\u003e Inspired by [mvdan/sh](https://pkg.go.dev/mvdan.cc/sh/v3/syntax), but engineered from the ground up with performance and extensibility in mind.\n\nIdeally I would like to use Flash in my daily basis. It's still far from proper usage.\n\n## Summary\n\n- [Feature Coverage](#feature-coverage)\n- [Flash as shell](#as-shell)\n- [Flash as library or shell backend](#as-library)\n\n## Feature Coverage\n\nThis table outlines the supported features of POSIX Shell and Bash. Use it to track what your **Flash** parser and interpreter implementation in Rust supports.\n\nLegends:\n\n- ✅ fully supported.\n- ⚠️ only supported in parser and formatter.\n- ❌ not supported.\n\n| Category              | Functionality / Feature                         | POSIX Shell | Bash | Flash | Notes |\n|-----------------------|--------------------------------------------------|-------------|------|------|-------|\n| **Basic Syntax**      | Variable assignment                             | ✅          | ✅   | ✅  | `VAR=value` |\n|                       | Command substitution                            | ✅          | ✅   | ✅  | `$(cmd)` and `` `cmd` `` |\n|                       | Arithmetic substitution                         | ❌          | ✅   | ✅  | `$((expr))` |\n|                       | Comments (`#`)                                  | ✅          | ✅   | ✅  | |\n|                       | Quoting (`'`, \"\", `\\`)                          | ✅          | ✅   | ✅  | |\n|                       | Globbing (`*`, `?`, `[...]`)                    | ✅          | ✅   | ✅  | |\n| **Control Structures**| `if` / `else` / `elif`                          | ✅          | ✅   | ✅  | |\n|                       | `case` / `esac`                                 | ✅          | ✅   | ✅  | |\n|                       | `for` loops                                     | ✅          | ✅   | ✅  | |\n|                       | `while`, `until` loops                          | ✅          | ✅   | ✅  | |\n|                       | `select` loop                                   | ❌          | ✅   | ✅  | |\n|                       | `[[ ... ]]` test command                        | ❌          | ✅   | ✅  | Extended test |\n| **Functions**         | Function definition (`name() {}`)               | ✅          | ✅   | ✅  | |\n|                       | `function` keyword                              | ❌          | ✅   | ✅  | Bash-specific |\n| **I/O Redirection**   | Output/input redirection (`\u003e`, `\u003c`, `\u003e\u003e`)       | ✅          | ✅   | ✅  | |\n|                       | Here documents (`\u003c\u003c`, `\u003c\u003c-`)                    | ✅          | ✅   | ❌  | |\n|                       | Here strings (`\u003c\u003c\u003c`)                            | ❌          | ✅   | ❌  | |\n|                       | File descriptor duplication (`\u003e\u0026`, `\u003c\u0026`)        | ✅          | ✅   | ❌  | |\n| **Job Control**       | Background execution (`\u0026`)                      | ✅          | ✅   | ✅  | |\n|                       | Job control commands (`fg`, `bg`, `jobs`)       | ✅          | ✅   | ✅  | May be interactive-only |\n|                       | Process substitution (`\u003c(...)`, `\u003e(...)`)       | ❌          | ✅   | ❌  | |\n| **Arrays**            | Indexed arrays                                  | ❌          | ✅   | ✅  | `arr=(a b c)` |\n|                       | Associative arrays                              | ❌          | ✅   | ❌  | `declare -A` |\n| **Parameter Expansion** | `${var}` basic expansion                      | ✅          | ✅   | ❌  | |\n|                       | `${var:-default}`, `${var:=default}`            | ✅          | ✅   | ❌  | |\n|                       | `${#var}`, `${var#pattern}`                     | ✅          | ✅   | ❌  | |\n|                       | `${!var}` indirect expansion                    | ❌          | ✅   | ❌  | |\n|                       | `${var[@]}` / `${var[*]}` array expansion       | ❌          | ✅   | ❌  | |\n| **Command Execution** | Pipelines                                       | ✅          | ✅   | ❌  | |\n|                       | Logical AND / OR (`\u0026\u0026`, ||)                     | ✅          | ✅   | ❌  | |\n|                       | Grouping (`( )`, `{ }`)                         | ✅          | ✅   | ❌  | |\n|                       | Subshell (`( )`)                                | ✅          | ✅   | ❌  | |\n|                       | Coprocesses (`coproc`)                          | ❌          | ✅   | ❌  | |\n| **Builtins**          | `cd`, `echo`, `test`, `read`, `eval`, etc.      | ✅          | ✅   | ✅  | |\n|                       | `shopt`, `declare`, `typeset`                   | ❌          | ✅   | ❌  | Bash-only |\n|                       | `let`, `local`, `export`                        | ✅          | ✅   | ✅  | |\n| **Debugging**         | `set -x`, `set -e`, `trap`                      | ✅          | ✅   | ❌  | |\n|                       | `BASH_SOURCE`, `FUNCNAME` arrays                | ❌          | ✅   | ❌  | |\n| **Miscellaneous**     | Brace expansion (`{1..5}`)                      | ❌          | ✅   | ✅  | |\n|                       | Extended globbing (`extglob`)                   | ❌          | ✅   | ❌  | Requires `shopt` |\n|                       | Bash version variables (`$BASH_VERSION`)        | ❌          | ✅   | ✅  | Note for the default interpreter: it's `$FLASH_VERSION` instead |\n|                       | Source other scripts (`.` or `source`)          | ✅          | ✅   | ✅  | `source` is Bash synonym |\n\n## As shell\n\nAt its base, a shell is simply a macro processor that executes commands. The term macro processor means functionality where text and symbols are expanded to create larger expressions. \n\nA Unix shell is both a command interpreter and a programming language. As a command interpreter, the shell provides the user interface to the rich set of GNU utilities. The programming language features allow these utilities to be combined. Files containing commands can be created, and become commands themselves. These new commands have the same status as system commands in directories such as /bin, allowing users or groups to establish custom environments to automate their common tasks.\n\nShells may be used interactively or non-interactively. In interactive mode, they accept input typed from the keyboard. When executing non-interactively, shells execute commands read from a file.\n\nFlash is largely compatible with sh and bash.\n\n\u003e ⚠️ Flash is still under development. Use it with caution in production environments.\n\n#### Installing it\n\nOption 1:\n\n```bash\ncargo install flash\n```\n\nOption 2:\n\n```bash\ngit clone https://github.com/raphamorim/flash.git\ncd flash \u0026\u0026 cargo install --path .\n```\n\nOption 3:\n\n```bash\ngit clone https://github.com/raphamorim/flash.git\ncd flash\ncargo build --release\n\n# Linux\nsudo cp target/release/flash /bin/\n\n# MacOS/BSD\nsudo cp target/release/flash /usr/local/bin/\n\n# Done\nflash\n```\n\n#### Set as default\n\nOptionally you can also set as default\n\n```bash\n# Add your flash path to:\nvim /etc/shells\n\n# Linux:\nchsh -s /bin/flash\n\n# MacOS/BSD:\nchsh -s /usr/local/bin/flash\n```\n\n## Configuration\n\nFlash supports configuration through a `.flashrc` file in your home directory. This file is executed when the shell starts up.\n\n### Custom Prompt\n\nYou can customize your shell prompt by setting the `PROMPT` variable in your `.flashrc` file:\n\n```bash\n# Simple prompt\nexport PROMPT=\"flash\u003e \"\n\n# Prompt with current directory\nexport PROMPT='flash:$PWD$ '\n\n# Prompt with username and hostname\nexport PROMPT='$USER@$HOSTNAME:$PWD$ '\n```\n\nThe `PROMPT` variable supports variable expansion, so you can use any environment variables in your prompt.\n\n### Example .flashrc\n\n```bash\n# Custom prompt\nexport PROMPT='flash:$PWD$ '\n\n# Environment variables\nexport EDITOR=vim\nexport PAGER=less\n\n# Custom aliases (when alias support is added)\n# alias ll=\"ls -la\"\n# alias grep=\"grep --color=auto\"\n```\n\n--\n\n## As library\n\nFlash can also be used a rust library that can help different purposes: testing purposes, parsing sh/bash, as a backend for your own shell, formatting sh/bash code, and other stuff.\n\n#### As an Interpreter\n\n```rust\nuse flash::interpreter::Interpreter;\nuse std::io;\n\nfn main() -\u003e io::Result\u003c()\u003e {\n    let mut interpreter = Interpreter::new();\n    interpreter.run_interactive()?;\n    Ok(())\n}\n```\n\nNote that `run_interactive` will use flash default evaluator.\n\n```rust\n// Default interactive shell using DefaultEvaluator\npub fn run_interactive(\u0026mut self) -\u003e io::Result\u003c()\u003e {\n    let default_evaluator = DefaultEvaluator;\n    self.run_interactive_with_evaluator(default_evaluator)\n}\n```\n\nYou can actually create your own evaluator using Evaluator trait:\n\n```rust\n// Define the evaluation trait that users can implement\npub trait Evaluator {\n    fn evaluate(\u0026mut self, node: \u0026Node, interpreter: \u0026mut Interpreter) -\u003e Result\u003ci32, io::Error\u003e;\n}\n\n// Default evaluator that implements the standard shell behavior\npub struct DefaultEvaluator;\n\nimpl Evaluator for DefaultEvaluator {\n    fn evaluate(\u0026mut self, node: \u0026Node, interpreter: \u0026mut Interpreter) -\u003e Result\u003ci32, io::Error\u003e {\n        match node {\n            Node::Command {\n                name,\n                args,\n                redirects,\n            } =\u003e self.evaluate_command(name, args, redirects, interpreter),\n            Node::Pipeline { commands } =\u003e self.evaluate_pipeline(commands, interpreter),\n            Node::List {\n                statements,\n                operators,\n            } =\u003e self.evaluate_list(statements, operators, interpreter),\n            Node::Assignment { name, value } =\u003e self.evaluate_assignment(name, value, interpreter),\n            Node::CommandSubstitution { command: _ } =\u003e {\n                Err(io::Error::other(\"Unexpected command substitution node\"))\n            }\n            Node::StringLiteral(_value) =\u003e Ok(0),\n            Node::Subshell { list } =\u003e interpreter.evaluate_with_evaluator(list, self),\n            Node::Comment(_) =\u003e Ok(0),\n            Node::ExtGlobPattern {\n                operator,\n                patterns,\n                suffix,\n            } =\u003e self.evaluate_ext_glob(*operator, patterns, suffix, interpreter),\n            _ =\u003e Err(io::Error::other(\"Unsupported node type\")),\n        }\n    }\n}\n\nimpl DefaultEvaluator {\n    fn evaluate_command(\n        \u0026mut self,\n        name: \u0026str,\n        args: \u0026[String],\n        redirects: \u0026[Redirect],\n        interpreter: \u0026mut Interpreter,\n    ) -\u003e Result\u003ci32, io::Error\u003e {\n        // Handle built-in commands\n        match name {\n            \"cd\" =\u003e {\n                let dir = if args.is_empty() {\n                    env::var(\"HOME\").unwrap_or_else(|_| \".\".to_string())\n                } else {\n                    args[0].clone()\n                };\n\n                match env::set_current_dir(\u0026dir) {\n                    Ok(_) =\u003e {\n                        interpreter.variables.insert(\n                            \"PWD\".to_string(),\n                            env::current_dir()?.to_string_lossy().to_string(),\n                        );\n                        Ok(0)\n                    }\n                    Err(e) =\u003e {\n                        eprintln!(\"cd: {}: {}\", dir, e);\n                        Ok(1)\n                    }\n                }\n            }\n            \"echo\" =\u003e {\n                for (i, arg) in args.iter().enumerate() {\n                    print!(\"{}{}\", if i \u003e 0 { \" \" } else { \"\" }, arg);\n                }\n                println!();\n                Ok(0)\n            }\n            \"export\" =\u003e {\n                for arg in args {\n                    if let Some(pos) = arg.find('=') {\n                        let (key, value) = arg.split_at(pos);\n                        let value = \u0026value[1..];\n                        interpreter\n                            .variables\n                            .insert(key.to_string(), value.to_string());\n                        unsafe {\n                            env::set_var(key, value);\n                        }\n                    } else if let Some(value) = interpreter.variables.get(arg) {\n                        unsafe {\n                            env::set_var(arg, value);\n                        }\n                    }\n                }\n                Ok(0)\n            }\n            \"source\" | \".\" =\u003e {\n                if args.is_empty() {\n                    eprintln!(\"source: filename argument required\");\n                    return Ok(1);\n                }\n\n                let filename = \u0026args[0];\n                match fs::read_to_string(filename) {\n                    Ok(content) =\u003e interpreter.execute(\u0026content),\n                    Err(e) =\u003e {\n                        eprintln!(\"source: {}: {}\", filename, e);\n                        Ok(1)\n                    }\n                }\n            }\n            _ =\u003e {\n                // External command\n                let mut command = Command::new(name);\n                command.args(args);\n\n                // Handle redirections\n                for redirect in redirects {\n                    match redirect.kind {\n                        RedirectKind::Input =\u003e {\n                            let file = fs::File::open(\u0026redirect.file)?;\n                            command.stdin(Stdio::from(file));\n                        }\n                        RedirectKind::Output =\u003e {\n                            let file = fs::File::create(\u0026redirect.file)?;\n                            command.stdout(Stdio::from(file));\n                        }\n                        RedirectKind::Append =\u003e {\n                            let file = fs::OpenOptions::new()\n                                .create(true)\n                                .append(true)\n                                .open(\u0026redirect.file)?;\n                            command.stdout(Stdio::from(file));\n                        }\n                    }\n                }\n\n                // Set environment variables\n                for (key, value) in \u0026interpreter.variables {\n                    command.env(key, value);\n                }\n\n                match command.status() {\n                    Ok(status) =\u003e Ok(status.code().unwrap_or(0)),\n                    Err(_) =\u003e {\n                        eprintln!(\"{}: command not found\", name);\n                        Ok(127)\n                    }\n                }\n            }\n        }\n    }\n\n    fn evaluate_pipeline(\n        \u0026mut self,\n        commands: \u0026[Node],\n        interpreter: \u0026mut Interpreter,\n    ) -\u003e Result\u003ci32, io::Error\u003e {\n        if commands.is_empty() {\n            return Ok(0);\n        }\n\n        if commands.len() == 1 {\n            return interpreter.evaluate_with_evaluator(\u0026commands[0], self);\n        }\n\n        let mut last_exit_code = 0;\n        for command in commands {\n            last_exit_code = interpreter.evaluate_with_evaluator(command, self)?;\n        }\n        Ok(last_exit_code)\n    }\n\n    fn evaluate_list(\n        \u0026mut self,\n        statements: \u0026[Node],\n        operators: \u0026[String],\n        interpreter: \u0026mut Interpreter,\n    ) -\u003e Result\u003ci32, io::Error\u003e {\n        let mut last_exit_code = 0;\n\n        for (i, statement) in statements.iter().enumerate() {\n            last_exit_code = interpreter.evaluate_with_evaluator(statement, self)?;\n\n            if i \u003c operators.len() {\n                match operators[i].as_str() {\n                    \"\u0026\u0026\" =\u003e {\n                        if last_exit_code != 0 {\n                            break;\n                        }\n                    }\n                    \"||\" =\u003e {\n                        if last_exit_code == 0 {\n                            break;\n                        }\n                    }\n                    _ =\u003e {}\n                }\n            }\n        }\n\n        Ok(last_exit_code)\n    }\n\n    fn evaluate_assignment(\n        \u0026mut self,\n        name: \u0026str,\n        value: \u0026Node,\n        interpreter: \u0026mut Interpreter,\n    ) -\u003e Result\u003ci32, io::Error\u003e {\n        match value {\n            Node::StringLiteral(string_value) =\u003e {\n                let expanded_value = interpreter.expand_variables(string_value);\n                interpreter\n                    .variables\n                    .insert(name.to_string(), expanded_value);\n            }\n            Node::CommandSubstitution { command } =\u003e {\n                let output = interpreter.capture_command_output(command, self)?;\n                interpreter.variables.insert(name.to_string(), output);\n            }\n            _ =\u003e {\n                return Err(io::Error::other(\"Unsupported value type for assignment\"));\n            }\n        }\n        Ok(0)\n    }\n\n    fn evaluate_ext_glob(\n        \u0026mut self,\n        operator: char,\n        patterns: \u0026[String],\n        suffix: \u0026str,\n        interpreter: \u0026Interpreter,\n    ) -\u003e Result\u003ci32, io::Error\u003e {\n        let entries = fs::read_dir(\".\")?;\n        let mut matches = Vec::new();\n\n        for entry in entries.flatten() {\n            let file_name = entry.file_name().to_string_lossy().to_string();\n            if interpreter.matches_ext_glob(\u0026file_name, operator, patterns, suffix) {\n                matches.push(file_name);\n            }\n        }\n\n        for m in matches {\n            println!(\"{}\", m);\n        }\n\n        Ok(0)\n    }\n}\n```\n\n#### As a Lexer/Tokenizer\n\n```rust\nfn test_tokens(input: \u0026str, expected_tokens: Vec\u003cTokenKind\u003e) {\n    let mut lexer = Lexer::new(input);\n    for expected in expected_tokens {\n        let token = lexer.next_token();\n        assert_eq!(\n            token.kind, expected,\n            \"Expected {:?} but got {:?} for input: {}\",\n            expected, token.kind, input\n        );\n    }\n\n    // Ensure we've consumed all tokens\n    let final_token = lexer.next_token();\n    assert_eq!(\n        final_token.kind,\n        TokenKind::EOF,\n        \"Expected EOF but got {:?}\",\n        final_token.kind\n    );\n}\n\n#[test]\nfn test_function_declaration() {\n    let input = \"function greet() { echo hello; }\";\n    let expected = vec![\n        TokenKind::Function,\n        TokenKind::Word(\"greet\".to_string()),\n        TokenKind::LParen,\n        TokenKind::RParen,\n        TokenKind::LBrace,\n        TokenKind::Word(\"echo\".to_string()),\n        TokenKind::Word(\"hello\".to_string()),\n        TokenKind::Semicolon,\n        TokenKind::RBrace,\n    ];\n    test_tokens(input, expected);\n}\n```\n\n#### As a Parser\n\n```rust\nuse flash::lexer::Lexer;\nuse flash::parser::Parser;\n\n#[test]\nfn test_simple_command() {\n    let input = \"echo hello world\";\n    let lexer = Lexer::new(input);\n    let mut parser = Parser::new(lexer);\n    let result = parser.parse_script();\n\n    match result {\n        Node::List {\n            statements,\n            operators,\n        } =\u003e {\n            assert_eq!(statements.len(), 1);\n            assert_eq!(operators.len(), 0);\n\n            match \u0026statements[0] {\n                Node::Command {\n                    name,\n                    args,\n                    redirects,\n                } =\u003e {\n                    assert_eq!(name, \"echo\");\n                    assert_eq!(args, \u0026[\"hello\", \"world\"]);\n                    assert_eq!(redirects.len(), 0);\n                }\n                _ =\u003e panic!(\"Expected Command node\"),\n            }\n        }\n        _ =\u003e panic!(\"Expected List node\"),\n    }\n}\n```\n\n#### As Formatter\n\n```rust\nassert_eq!(\n    Formatter::format_str(\"       # This is a comment\"),\n    \"# This is a comment\"\n);\n```\n\nOr by receiving AST\n\n```rust\nlet mut formatter = Formatter::new();\nlet node = Node::Comment(\" This is a comment\".to_string());\n\nassert_eq!(formatter.format(\u0026node), \"# This is a comment\");\n```\n\n## Resources\n\n- https://www.gnu.org/software/bash/manual/bash.html\n- https://www.shellcheck.net/\n- https://stackblitz.com/edit/bash-ast?file=src%2Fapp%2Fapp.component.ts\n\n## License\n\n[GPL-3.0 License](LICENSE) © [Raphael Amorim](https://github.com/raphamorim/)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraphamorim%2Fflash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fraphamorim%2Fflash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraphamorim%2Fflash/lists"}