I made an abstract syntax tree parser for Bash in C#
Blazor WASM visualizer of the AST w/ Mermaid
I'm working on a .NET agents project (https://netclaw.dev) and one of its features allows them to execute shell commands.
In order to make agent execution comply with some rules (determined by the end-user + security policy) I needed some way of reasoning about "what is this command really doing and which directory is it doing it in?" so I could surface that information in an approval prompt to the end-user.
There are some native libraries that do this, but I'm hoping to make my project AOT-compatible in the future so I had Claude grok a corpus from thousands of commands my agent has attempted to run and built a C# program that could create an abstract syntax tree representation that could definitely determine:
- What are the real command verb + noun pairs the agent is requesting to execute?
- Which directory(ies) are they being executed in - this requires doing things like tracking the implicit flow of the current working directory.
I've dogfooded this over the course of the past week or so with several thousands more commands and it works great. The Blazor WASM sample I have on screen is just a visualizer of what the AST yields and serves no practical purpose other than being fun.
The library doesn't support things like bash functions and evaluating shell file references because that's kind of out of scope for what I need (evaluating inline CLI commands) - so if you try pasting a `.sh` file it'll choke on those.
For instance though:
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}");
}
}
Will produce (shows the propagation of the current working directory):
None cd
path: /repo
AndIf rm
↳ cwd: /repo
path: /etc/passwd
I'm working on adding a PowerShell flavor to this library next so I can do the same types of things on Windows shells.
Repo is here: https://github.com/Aaronontheweb/ShellSyntaxTree