Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F6009137
shell.pegphp
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
14 KB
Referenced Files
None
Subscribers
None
shell.pegphp
View Options
/*
* Recognise a shell command per the POSIX Shell Command Language specification
* and return an AST.
*
* Tree construction may be overridden in a subclass by overriding the node()
* function.
*
* In general, AST nodes have a name and a contents array. For a binary
* operator, the first element of the contents array will be the left
* operand, and the second element will be the right operand. In more complex
* cases, operands should generally be named by enclosing them within a named
* node.
*
* Each element of the contents array may either be a string, a node, or an
* array of nodes.
*/
/*
* File initializer
*/
{
use Wikimedia\WikiPEG\InternalError;
// @phan-file-suppress PhanUnusedGotoLabel
// @phan-file-suppress PhanNoopSwitchCases
}
/*
* Class initializer
*/
{
/**
* Overridable tree node constructor
*
* @stable to override
* @param string $type
* @param array|Node|string $contents
* @return Node
*/
protected function node( $type, $contents ) {
return new Node( $type, $contents );
}
/**
* Combine arrays and non-array items into a single flat array.
*
* @param array|Node|string|null ...$items
* @return array
* @phan-return array<Node|string>
*/
private function merge( ...$items ) {
if ( !$items ) {
return [];
}
$mergeArgs = [];
foreach ( $items as $item ) {
if ( $item !== null ) {
if ( !is_array( $item ) ) {
$mergeArgs[] = [ $item ];
} else {
$mergeArgs[] = $item;
}
}
}
return array_merge( ...$mergeArgs );
}
}
program =
linebreak commands:complete_commands linebreak
{
return $this->node( 'program', $commands );
}
/ linebreak
{
return $this->node( 'program', [] );
}
complete_commands =
(
c:complete_command newline_list
{
return $c;
}
/ complete_command
)*
complete_command =
OWS
list: (
nodes: (
item: and_or
separator: $separator_op
{
if ( $separator && $separator[0] === '&' ) {
return $this->node( 'background', $item );
} else {
return $item;
}
}
)+
last: and_or?
{
if ( $last ) {
$nodes[] = $last;
}
if ( count( $nodes ) > 1 ) {
return $this->node( 'list', $nodes );
} else {
return $nodes[0];
}
}
/
and_or
)
{
return $this->node( 'complete_command', $list );
}
and_or =
first: pipeline
rest: (
AND_IF linebreak pipeline:pipeline
{
return $this->node( 'and_if', $pipeline );
}
/ OR_IF linebreak pipeline:pipeline
{
return $this->node( 'or_if', $pipeline );
}
/ pipeline
)*
{
return $this->merge( $first, $rest );
}
pipeline =
bang: Bang?
pipeline: pipe_sequence
{
if ( $bang !== null ) {
return $this->node( 'bang', $pipeline );
} else {
return $pipeline;
}
}
pipe_sequence =
first: command
rest: (
PIPE linebreak command:command
{
return $command;
}
)*
{
if ( count( $rest ) ) {
return $this->node( 'pipeline', $this->merge( $first, $rest ) );
} else {
return $first;
}
}
command =
function_definition
/ simple_command
/ c:compound_command r:redirect_list?
{
if ( $r !== null ) {
return $this->merge( $c, $r );
} else {
return $c;
}
}
compound_command =
brace_group
/ subshell
/ for_clause
/ case_clause
/ if_clause
/ while_clause
/ until_clause
subshell =
LPAREN list:compound_list RPAREN
{
return $this->node( 'subshell', $list );
}
compound_list =
linebreak
terms:(
term: and_or
separator: $separator
{
if ( $separator && $separator[0] === '&' ) {
return $this->node( 'background', $term );
} else {
return $term;
}
}
)+
last: and_or?
{
if ( $last ) {
$terms[] = $last;
}
return $terms;
}
/
linebreak
term: and_or
{
if ( $term === null ) {
// Phan is convinced $term may be null, not sure how
return [];
} else {
return $term;
}
}
for_clause =
(
For name:for_name linebreak for_case_in wordlist:wordlist? sequential_sep do_group:do_group
{
return $this->node( 'for', [ $name, $this->node( 'in', $wordlist ?: [] ), $do_group ] );
}
)
/
(
For name:for_name sequential_sep? do_group:do_group
{
return $this->node( 'for', [ $name, $do_group ] );
}
)
// Spec note: Apply rule 5.
// This and other lexer rules in the spec are mostly workarounds for loose
// integration between the lexer and the parser implied by the spec. We know
// that we want a NAME here, so we can just ask for a NAME. With a separate
// lexer it would be necessary for the lexer to guess whether the parser will
// want a NAME or a WORD, and for the parser to somehow downgrade an
// inappropriate NAME token to a WORD if the guess was wrong.
for_name = name:NAME OWS
{
return $name;
}
// Spec note: Apply rule 6
for_case_in = In OWS
wordlist = WORD+
case_clause =
Case word:WORD linebreak for_case_in linebreak list_esac:( case_list $Esac / case_list_ns $Esac / $Esac )
{
if ( is_array( $list_esac ) ) {
$list = $list_esac[0];
} else {
$list = [];
}
return $this->node( 'case', [ $word, $this->node( 'in', $list ) ] );
}
// ns means no semicolon
case_list_ns =
list:case_list item:case_item_ns
{
$list[] = $item;
return $list;
}
/ item:case_item_ns
{
return [ $item ];
}
case_list =
case_item+
// ns means no semicolon
case_item_ns =
LPAREN? pattern:pattern RPAREN list:compound_list
{
return $this->node( 'case_item', [
$pattern,
$this->node( 'case_consequent', $list )
] );
}
/ LPAREN? pattern:pattern RPAREN linebreak
{
return $this->node( 'case_item', [ $pattern ] );
}
case_item =
LPAREN? pattern:pattern RPAREN list:compound_list DSEMI linebreak
{
return $this->node( 'case_item', [
$pattern,
$this->node( 'case_consequent', $list )
] );
}
/ LPAREN? pattern:pattern RPAREN linebreak DSEMI linebreak
{
return $this->node( 'case_item', $pattern );
}
// Spec note: rule 4
pattern =
first:WORD rest:( PIPE WORD )*
{
$patterns = [ $first ];
foreach ( $rest as $pattern ) {
$patterns[] = $pattern[1];
}
return $this->node( 'case_pattern', $patterns );
}
if_clause =
If
condition: compound_list
Then
consequent: compound_list
else_part: else_part?
Fi
{
$contents = [
$this->node( 'condition', $condition ),
$this->node( 'consequent', $consequent )
];
if ( $else_part !== null ) {
$contents = $this->merge( $contents, $else_part );
}
return $this->node( 'if', $contents );
}
else_part =
Elif
condition: compound_list
Then
consequent: compound_list
else_part: else_part?
{
$contents = [
$this->node( 'elif_condition', $condition ),
$this->node( 'elif_consequent', $consequent )
];
if ( $else_part !== null ) {
$contents = $this->merge( $contents, $else_part );
}
return $contents;
}
/ Else alternative:compound_list
{
return [ $this->node( 'else', $alternative ) ];
}
while_clause =
While list:compound_list body:do_group
{
return $this->node( 'while', [
$this->node( 'condition', $list ),
$body
] );
}
until_clause =
Until list:compound_list body:do_group
{
return $this->node( 'until', [
$this->node( 'condition', $list ),
$body
] );
}
function_definition =
fname:fname LPAREN RPAREN linebreak body:function_body
{
return $this->node( 'function_definition',
$this->merge( $this->node( 'function_name', $fname ), $body ) );
}
// Spec note: Apply rule 9
function_body =
c:compound_command r:redirect_list?
{
if ( $r !== null ) {
return $this->merge( $c, $r );
} else {
return $c;
}
}
fname =
NAME // Apply rule 8
brace_group =
Lbrace list:compound_list Rbrace
{
return $this->node( 'brace_group', $list );
}
do_group =
Do list:compound_list Done // Apply rule 6
{
return $this->node( 'do', $list );
}
simple_command =
prefix:cmd_prefix word:cmd_word suffix:cmd_suffix?
{
$contents = [ $prefix, $word ];
if ( $suffix !== null ) {
$contents = array_merge( $contents, $suffix );
}
return $this->node( 'simple_command', $contents );
}
/ prefix:cmd_prefix
{
return $this->node( 'simple_command', [ $prefix ] );
}
/ name:cmd_name suffix:cmd_suffix?
{
$contents = [ $name ];
if ( $suffix ) {
$contents = array_merge( $contents, $suffix );
}
return $this->node( 'simple_command', $contents );
}
// Spec note: Apply rule 7a: Guess whether ASSIGNMENT_WORD is needed. That
// doesn't need any special handling, but we need to avoid allowing reserved
// words here with a negative assertion.
cmd_name =
!reserved word:WORD
{
return $word;
}
// Apply rule 7b
cmd_word =
WORD
cmd_prefix =
contents: (
io_redirect
/ ASSIGNMENT_WORD
)+
{
return $this->node( 'cmd_prefix', $contents );
}
cmd_suffix =
(
io_redirect
/ WORD
)+
redirect_list =
io_redirect+
io_redirect =
number:IO_NUMBER? file_or_here:(io_file / io_here)
{
$contents = [];
if ( $number !== null ) {
$contents[] = $this->node( 'io_subject', $number );
}
$contents[] = $file_or_here;
return $this->node( 'io_redirect', $contents );
}
// Options reordered to put the longer substrings first. Otherwise we would
// need assertions to protect longer operators from being partly consumed by
// smaller operators.
io_file =
LESSAND filename:filename
{
return $this->node( 'duplicate_input', $filename );
}
/ LESSGREAT filename:filename
{
return $this->node( 'read_and_write', $filename );
}
/ LESS filename:filename
{
return $this->node( 'input', $filename );
}
/ GREATAND filename:filename
{
return $this->node( 'duplicate_output', $filename );
}
/ DGREAT filename:filename
{
return $this->node( 'append_output', $filename );
}
/ CLOBBER filename:filename
{
return $this->node( 'clobber', $filename );
}
/ GREAT filename:filename
{
return $this->node( 'output', $filename );
}
filename =
WORD // Apply rule 2
io_here =
op: (
DLESSDASH { return 'io_here_strip'; }
/ DLESS { return 'io_here'; }
)
end: here_end
{ // @phan-suppress-previous-line PhanPluginNeverReturnMethod
// TODO: this is quite complicated to implement, especially given the way
// the parser is structured.
throw new UnimplementedError( 'heredoc is not implemented' );
// For phan
// @phan-suppress-next-line PhanPluginUnreachableCode
return $this->node( 'io_here', '' );
}
here_end =
$WORD // Apply rule 3
newline_list =
NEWLINE+
linebreak =
newline_list?
separator_op =
AND
/ SEMI
separator =
separator_op linebreak
/ newline_list
sequential_sep =
SEMI linebreak
/ newline_list
// Operator tokens per spec
AND_IF = '&&' OWS
OR_IF = '||' OWS
DSEMI = ';;' OWS
DLESS = '<<' OWS
DGREAT = '>>' OWS
LESSAND = '<&' OWS
GREATAND = '>&' OWS
LESSGREAT = '<>' OWS
DLESSDASH = '<<-' OWS
CLOBBER = '>|' OWS
// Reserved word tokens per spec
If = 'if' DELIM
Then = 'then' DELIM
Else = 'else' DELIM
Elif = 'elif' DELIM
Fi = 'fi' DELIM
Do = 'do' DELIM
Done = 'done' DELIM
Case = 'case' DELIM
Esac = 'esac' DELIM
While = 'while' DELIM
Until = 'until' DELIM
For = 'for' DELIM
Lbrace = '{' DELIM
Rbrace = '}' DELIM
Bang = '!' DELIM
In = 'in' DELIM
reserved =
If
/ Then
/ Else
/ Elif
/ Fi
/ Do
/ Done
/ Case
/ Esac
/ While
/ Until
/ For
/ Lbrace
/ Rbrace
/ Bang
/ In
// Single-character tokens with disambiguating assertions
AND = !AND_IF '&' OWS
SEMI = !DSEMI ';' OWS
LESS = !DLESS !LESSAND !LESSGREAT !DLESSDASH '<' OWS
GREAT = !DGREAT !GREATAND !CLOBBER '>' OWS
PIPE = !OR_IF '|' OWS
LPAREN = '(' OWS
RPAREN = ')' OWS
// Optional whitespace
// Consume whitespace and comments after each token
OWS = [ \t\v\r\f]* ("#" [^\n]*)?
// Delimiter assertion for reserved word termination
DELIM =
[ \t\v\r\f]+
/ !. // EOF
/ & "\n"
WORD =
parts: word_part+
OWS
{
return $this->node( 'word', $parts );
}
word_part =
single_quoted_part
/ double_quoted_part
/ bare_escape_sequence
/ backquote_expansion
/ dollar_expansion
/ plain_part
single_quoted_part =
"'" contents:$[^']* "'"
{
return $this->node( 'single_quote', $contents );
}
double_quoted_part =
'"'
contents: (
dquoted_escape
/ backquote_expansion
/ dollar_expansion
/ "\\"
/ $[^"`$\\]+
)*
'"'
{
return $this->node( 'double_quote', $contents );
}
dquoted_escape =
"\\" contents:[$`"\\\n]
{
return $this->node( 'dquoted_escape', $contents );
}
bare_escape_sequence =
"\\" contents:[^\n]
{
return $this->node( 'bare_escape', $contents );
}
backquote_expansion =
"`"
parts: (
backquoted_escape
/ dollar_expansion
/ double_backquote_expansion
/ "$"
/ "\\"
/ $[^`$\\]+
)*
"`"
{
return $this->node( 'backquote', $parts );
}
backquoted_escape =
"\\" contents:[$\\]
{
return $this->node( 'backquoted_escape', $contents );
}
double_backquote_expansion =
"\\`"
parts: (
backquoted_escape
/ dollar_expansion
/ "$"
/ "\\" !"`"
/ $[^`$\\]+
)
"\\`"
{
return $this->node( 'double_backquote', $parts );
}
dollar_expansion =
"$"
contents: (
special_parameter
/ short_positional_parameter
/ brace_expansion
/ arithmetic_expansion
/ command_expansion
/ named_parameter
)
{
return $contents;
}
special_parameter =
contents: [@*#?\-$!0]
{
return $this->node( 'special_parameter', $contents );
}
short_positional_parameter =
contents: [1-9]
{
return $this->node( 'positional_parameter', $contents );
}
brace_expansion =
"{"
contents: (
binary_expansion
/ string_length
/ braced_parameter_expansion
)
"}"
{
return $contents;
}
binary_expansion =
parameter:parameter
operator:(
$( ":" [\-=?+] )
/ $( "%%" / "##" / [%#\-=?+] )
)
OWS
word:WORD<no_rbrace>?
{
$names = [
':-' => 'use_default',
'-' => 'use_default_unset',
':=' => 'assign_default',
'=' => 'assign_default_unset',
':?' => 'indicate_error',
'?' => 'indicate_error_unset',
':+' => 'use_alternative',
'+' => 'use_alternative_unset',
'%' => 'remove_smallest_suffix',
'%%' => 'remove_largest_suffix',
'#' => 'remove_smallest_prefix',
'##' => 'remove_largest_prefix'
];
if ( !isset( $names[$operator] ) ) {
throw new InternalError( "Unable to find operator \"$operator\"" );
}
return $this->node( $names[$operator], [ $parameter, $word ?? '' ] );
}
string_length =
"#" parameter:parameter
{
return $this->node( 'string_length', $parameter );
}
arithmetic_expansion =
"(("
OWS
words:WORD+
"))"
{
return $this->node( 'arithmetic_expansion', $words );
}
command_expansion =
"("
command: complete_command
")"
{
return $this->node( 'command_expansion', $command );
}
braced_parameter_expansion =
parameter:parameter
{
return $this->node( 'braced_parameter_expansion', $parameter );
}
parameter =
special_parameter /
long_positional_parameter /
named_parameter
long_positional_parameter =
parameter: $[0-9]+
{
return $this->node( 'positional_parameter', $parameter );
}
named_parameter =
name:NAME
{
return $this->node( 'named_parameter', $name );
}
plain_part =
plain: (
$(&<no_rbrace> [^'"\\`$ \t\v\r\f\n&|;<>(){}]+)
/ $(!<no_rbrace> [^'"\\`$ \t\v\r\f\n&|;<>()]+)
)
{
return $this->node( 'unquoted_literal', $plain );
}
ASSIGNMENT_WORD =
name:NAME "=" word:WORD
{
return $this->node( 'assignment', [ $this->node( 'name', $name ), $word ] );
}
NAME = $( [_a-zA-Z] [_a-zA-Z0-9]* )
NEWLINE = "\n" OWS
IO_NUMBER = $[0-9]+
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Oct 4 2026, 09:20 (5 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
0d/5f/4206995a8d87e5b352fd24fbabd2
Default Alt Text
shell.pegphp (14 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment