Reading scripts

edit

The shape of a script

A ShellScriptSyntax holds a ShellStatementListSyntax, whose Statements are the statements of the script. A statement is a command, a pipeline, a command list (a && b), or a compound statement such as if, for or a function definition, which holds statement lists of its own.

The separators belong to the lists: a statement list, a pipeline and a command list are separated lists, where Count counts the statements and SeparatorCount the ;, | or && between them. See Lists.

var tree = ShellSyntaxTree.ParseText("ls -l", ShellDialect.Bash);

var kinds = new List<SyntaxKind>();
foreach (var node in tree.GetRoot().DescendantNodes())
{
    kinds.Add(node.Kind());
}

Assert.Equal([SyntaxKind.StatementList, SyntaxKind.Command, SyntaxKind.Word, SyntaxKind.LiteralWordPart, SyntaxKind.Word, SyntaxKind.LiteralWordPart], kinds);

Commands and words

A ShellCommandSyntax is a simple command. Its Elements are its parts in the order of the text, and named properties sort them:

PropertyHolds
Name, NameValueThe word of the command name, and its value
ArgumentsThe words after the name
AssignmentsThe NAME=value in front of the command
RedirectionsThe >file, 2>&1, <<EOF of the command

A ShellWordSyntax is made of parts: literal text, quoted strings, variable references, command substitutions, globs. Its Value is the word with its quotes and escapes resolved, or null when it depends on an expansion, such as a variable:

var command = (ShellCommandSyntax)ShellSyntaxTree.ParseCommand("git commit -m 'wip'", ShellDialect.Bash);

Assert.Equal("git", command.NameValue);
Assert.Equal("wip", command.Arguments[2].Value);

To find every command, filter the descendants by type or by kind:

var tree = ShellSyntaxTree.ParseText("ls -l | wc -l && echo done", ShellDialect.Bash);

var commandCount = 0;
foreach (var node in tree.GetRoot().DescendantNodes())
{
    if (node.Kind() == SyntaxKind.Command)
    {
        // handle a simple command
        commandCount++;
    }
}

Assert.Equal(3, commandCount);

Expressions

What an expression is differs between shells, so there is no entry point to parse one. Reach them through the nodes that contain them:

Precedence is in the tree: a binary expression holds its operands, which can be binary expressions themselves.

var arithmetic = ShellSyntaxTree.ParseText("echo $((1 + 2 * 3))", ShellDialect.Bash)
    .GetRoot().DescendantNodes().OfType<PosixArithmeticExpansionSyntax>().Single();

// 1 + (2 * 3): precedence is in the tree, not left to the caller.
var binary = (ShellBinaryExpressionSyntax)arithmetic.Expression;
Assert.Equal("+", binary.OperatorText);
Assert.IsType<ShellBinaryExpressionSyntax>(binary.Right);

Comments

Comments are trivia: they do not change the structure, and DescendantComments() returns them:

var tree = ShellSyntaxTree.ParseText("# build\nmake # all targets\n", ShellDialect.Bash);

var comments = new List<string>();
foreach (var comment in tree.GetRoot().DescendantComments())
{
    comments.Add($"{comment.Span.Start}: {comment}");
}

Assert.Equal(["0: # build", "13: # all targets"], comments);

Walk a script

ShellSyntaxWalker has a method per node type, such as VisitCommand:

private sealed class SimpleCommandCounter : ShellSyntaxWalker
{
    public int Count { get; private set; }

    public override void VisitCommand(ShellCommandSyntax node)
    {
        Count++;
        base.VisitCommand(node);
    }
}