Reading scripts
editThe 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:
| Property | Holds |
|---|---|
| Name, NameValue | The word of the command name, and its value |
| Arguments | The words after the name |
| Assignments | The NAME=value in front of the command |
| Redirections | The >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:
- PosixArithmeticExpansionSyntax.Expression for
$(( )); - PosixDelimitedExpressionStatementSyntax.Expression for
(( ))and[[ ]]; - PowerShellExpressionStatementSyntax.Expression for a PowerShell expression statement.
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);
}
}