Skip to content

cuteboy0323/ShellSyntaxTree

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ShellSyntaxTree

NuGet

A focused .NET library that parses bash and PowerShell command strings into a structured AST. Purpose-built for tools that need to reason about shell commands without running them β€” approval gates for LLM-emitted commands, CI/CD script auditors, sandbox policy generators, audit-log analytics.

Hand-rolled, AOT-trim friendly, zero native dependencies. Multi-targets netstandard2.0 and net8.0.

dotnet add package ShellSyntaxTree --version 0.2.0-alpha

What you get

For an input like cd /repo && rm /etc/passwd, ShellSyntaxTree produces:

flowchart TD
    classDef bad fill:#fee,stroke:#b00,stroke-width:2px
    A[cd /repo<br/>πŸ“ /repo] -- "&&" --> B[rm<br/>πŸ“ /etc/passwd<br/>cwd: /repo]
    class B bad
Loading

A two-clause AST where the second clause's Args includes a synthetic /repo attribution arg (so consumers can see "this rm is implicitly operating in /repo") and /etc/passwd is resolved and marked IsPath = true. Hard-deny rules over /etc/* fire immediately; no substring matching, no shelling out, no false positives.

Things you can do with it

  • Approval gates for AI agents β€” given a command emitted by an LLM, decide ALLOW / PROMPT / DENY before invoking the shell.
  • CI/CD pipeline audits β€” scan shell steps in GitHub Actions / Jenkinsfile / Azure Pipelines for writes outside the workspace, curl | bash from non-allowlisted hosts, hardcoded credential echoes.
  • Sandbox / container policy β€” derive the minimum-viable volume mount set or AppArmor profile from a build script.
  • Pre-commit linters β€” flag dangerous patterns (rm -rf /, chmod 777 /etc/*) in shell scripts at commit time.
  • Shell history / audit-log analytics β€” ingest ~/.bash_history or auditd records into structured form for SIEM-style insights.
  • Documentation / explainers β€” convert complex one-liners into readable structure for tutorials and runbooks.

The original consumer is Netclaw's approval policy; the library is built to be reusable beyond that.

Quick start

using ShellSyntaxTree;

var parser = new BashParser();
var parsed = parser.Parse("cd /repo && rm /etc/passwd");

if (parsed.IsUnparseable)
{
    // Safe-fail: prompt the user, deny the command, etc.
    Console.WriteLine($"can't model: {parsed.UnparseableReason}");
    return;
}

foreach (var clause in parsed.Clauses)
{
    Console.WriteLine($"{clause.Operator} {clause.Verb.Joined}");

    foreach (var arg in clause.Args.Where(a => a.IsPath))
    {
        var marker = arg.IsCwdAttribution ? "↳ cwd" : "  path";
        Console.WriteLine($"    {marker}: {arg.Resolved}");
    }

    foreach (var redirect in clause.Redirects.Where(r => !r.IsDynamicSkip))
    {
        Console.WriteLine($"    {redirect.Direction}: {redirect.Target}");
    }
}

Run that against the example input and you get:

None cd
      path: /repo
AndIf rm
    ↳ cwd: /repo
      path: /etc/passwd

Public API surface

namespace ShellSyntaxTree;

public interface IShellParser { ParsedCommand Parse(string command); }
public sealed class BashParser : IShellParser { /* … */ }
public sealed class PwshParser : IShellParser { /* … */ }   // v0.2.0

public abstract record ShellParserOptions { /* HomeDirectory, WorkingDirectory */ }
public sealed record BashParserOptions : ShellParserOptions;
public sealed record PwshParserOptions : ShellParserOptions;

public sealed record ParsedCommand { /* Source, Clauses, IsUnparseable, … */ }
public sealed record Clause        { /* Operator, Verb, Args, Redirects, IsSubshell, IsCommandStringWrapped */ }
public sealed record VerbChain     { /* Tokens, Joined, CanonicalVerb, IsDynamic */ }
public sealed record Arg           { /* Raw, Resolved, Kind, IsPath, IsCwdAttribution, IsFlag */ }
public sealed record Redirect      { /* Direction, Target, IsDynamicSkip */ }

public enum ArgKind            { Literal, EnvVar, Glob, Tilde, DynamicSkip }
public enum RedirectDirection  { In, Out, Append, ErrOut, ErrAppend }
public enum CompoundOperator   { None, AndIf, OrIf, Sequence, Pipe }

Both parsers emit the same ParsedCommand AST β€” a consumer walks a PowerShell parse exactly as it walks a bash one. A Windows cmd parser remains deferred.

Behavioral contract: SPEC.md (bash + shared surface) and SPEC.POWERSHELL.md (PowerShell).

Samples

Two runnable samples live under samples/.

ShellSyntaxTree.Cli.Sample β€” terminal explainer + audit policy

dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- explain "cd /repo && rm /etc/passwd"
dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- audit "cd /repo && rm /etc/passwd"

# --shell pwsh routes the same explain / audit logic through PwshParser:
dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- explain --shell pwsh "gci C:\logs | rm"

explain pretty-prints the AST with [flag] / [path] / [cwd-attr] / [dyn-skip] / [glob] markers per arg. audit runs a small built-in policy ("deny writes in /etc, /usr, /bin, /sbin, /lib", "warn on curl | bash", "warn on dynamic args in path slots") and exits 0 / 1 / 2 by severity. See samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs for the policy code β€” ~50 lines.

ShellSyntaxTree.Web.Sample β€” Blazor WebAssembly Mermaid visualizer

Paste a bash or PowerShell script, watch the parsed AST render as a Mermaid flowchart in your browser. Everything runs client-side β€” pasted scripts never leave your machine. Useful for "what does this script actually do?" moments and for understanding how the library models constructs like subshells and bash -c / pwsh -Command recursion.

dotnet run --project samples/ShellSyntaxTree.Web.Sample
# β†’ http://localhost:5239

Build script preset

A shell selector switches between the bash and PowerShell parsers; each ships preset scripts demonstrating compound commands, subshell isolation, command-string recursion, alias resolution, dynamic-cwd attribution, and unparseable inputs. Each preset shows what the library produces in a single click.

Building from source

dotnet tool restore
dotnet build -c Release
dotnet test  -c Release
dotnet pack  -c Release -o ./bin/nuget

global.json pins the SDK; you need .NET 10 SDK or later for the .slnx solution format.

Versioning

Tags are bare SemVer version numbers β€” no v prefix. The release workflow asserts this and fails fast on misformatted tags.

  • 0.1.x β€” bash parser. Additive after 0.1.0 (more verb table entries, more corpus, bug fixes).
  • 0.2.0 β€” first PowerShell parser (PwshParser). Adds the shared ShellParserOptions base and the additive VerbChain.CanonicalVerb / VerbChain.IsDynamic fields; renames Clause.IsBashCWrapped β†’ IsCommandStringWrapped (breaking β€” see RELEASE_NOTES.md).
  • 1.0.0 β€” when an external consumer beyond Netclaw ships against it without finding API gaps.

License

Apache-2.0. Copyright Β© 2026 Aaron Stannard.


Repository layout β€” for contributors and curious agents:

Path What
src/ShellSyntaxTree/ The library (bash + PowerShell parsers)
tests/ShellSyntaxTree.Tests/ xUnit unit tests + corpus runner
tests/ShellSyntaxTree.Tests/Corpus/<shell>/*.json Corpus entries β€” the acceptance contract (bash + powershell)
samples/ShellSyntaxTree.Cli.Sample/ Console explainer + audit policy
samples/ShellSyntaxTree.Web.Sample/ Blazor WASM Mermaid visualizer
tools/PwshCorpusTool/ PowerShell corpus authoring aid
SPEC.md, SPEC.POWERSHELL.md The behavioral contract
openspec/ Change-proposal history (rationale for design decisions)
PROJECT_CONTEXT.md, TOOLING.md, AGENTS.md Repo governance β€” for autonomous agents
IMPLEMENTATION_PLAN.md NOW / NEXT / LATER work tracker

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors