1<?php 2 3namespace Shellbox\ShellParser; 4 5class SyntaxInfo { 6 /** @var Node */ 7 private $root; 8 9 /** @var string[]|null */ 10 private $featureList; 11 12 /** @var string[]|null */ 13 private $literalArgv; 14 15 public const LIST = 'list'; 16 public const BACKGROUND = 'background'; 17 public const PIPELINE = 'pipeline'; 18 public const COMPOUND = 'compound'; 19 public const REDIRECT = 'redirect'; 20 public const COMMAND_EXPANSION = 'command_expansion'; 21 public const PARAMETER = 'parameter'; 22 public const EXOTIC_EXPANSION = 'exotic_expansion'; 23 public const ASSIGNMENT = 'assignment'; 24 25 /** 26 * @internal Use SyntaxTree::getInfo() 27 * 28 * @param Node $root 29 */ 30 public function __construct( $root ) { 31 $this->root = $root; 32 } 33 34 /** 35 * @var array Node types used to identify features. Note that features do 36 * not need to be mutually exclusive. 37 */ 38 private static $nodeTypesByFeature = [ 39 'list' => [ 'list', 'and_if', 'or_if' ], 40 'background' => [ 'background' ], 41 'pipeline' => [ 'pipeline' ], 42 'compound' => [ 43 'subshell', 44 'for', 45 'case', 46 'if', 47 'while', 48 'until', 49 'function_definition', 50 'brace_group' 51 ], 52 'redirect' => [ 'io_redirect' ], 53 'command_expansion' => [ 'backquote', 'command_expansion' ], 54 'parameter' => [ 'special_parameter', 'positional_parameter', 'named_parameter' ], 55 'exotic_expansion' => [ 56 'use_default', 57 'use_default_unset', 58 'assign_default', 59 'assign_default_unset', 60 'indicate_error', 61 'indicate_error_unset', 62 'use_alternative', 63 'use_alternative_unset', 64 'remove_smallest_suffix', 65 'remove_largest_suffix', 66 'remove_smallest_prefix', 67 'remove_largest_prefix', 68 'string_length', 69 'arithmetic_expansion', 70 'braced_parameter_expansion' 71 ], 72 'assignment' => [ 'assignment' ], 73 ]; 74 75 /** 76 * @var array Features by node type, compiled with compileFeaturesByNodeType(). 77 */ 78 private static $featuresByNodeType = [ 79 'and_if' => [ 'list' ], 80 'arithmetic_expansion' => [ 'exotic_expansion' ], 81 'assign_default' => [ 'exotic_expansion' ], 82 'assign_default_unset' => [ 'exotic_expansion' ], 83 'assignment' => [ 'assignment' ], 84 'background' => [ 'background' ], 85 'backquote' => [ 'command_expansion' ], 86 'brace_group' => [ 'compound' ], 87 'braced_parameter_expansion' => [ 'exotic_expansion' ], 88 'case' => [ 'compound' ], 89 'command_expansion' => [ 'command_expansion' ], 90 'for' => [ 'compound' ], 91 'function_definition' => [ 'compound' ], 92 'if' => [ 'compound' ], 93 'indicate_error' => [ 'exotic_expansion' ], 94 'indicate_error_unset' => [ 'exotic_expansion' ], 95 'io_redirect' => [ 'redirect' ], 96 'list' => [ 'list' ], 97 'named_parameter' => [ 'parameter' ], 98 'or_if' => [ 'list' ], 99 'pipeline' => [ 'pipeline' ], 100 'positional_parameter' => [ 'parameter' ], 101 'remove_largest_prefix' => [ 'exotic_expansion' ], 102 'remove_largest_suffix' => [ 'exotic_expansion' ], 103 'remove_smallest_prefix' => [ 'exotic_expansion' ], 104 'remove_smallest_suffix' => [ 'exotic_expansion' ], 105 'special_parameter' => [ 'parameter' ], 106 'string_length' => [ 'exotic_expansion' ], 107 'subshell' => [ 'compound' ], 108 'until' => [ 'compound' ], 109 'use_alternative' => [ 'exotic_expansion' ], 110 'use_alternative_unset' => [ 'exotic_expansion' ], 111 'use_default' => [ 'exotic_expansion' ], 112 'use_default_unset' => [ 'exotic_expansion' ], 113 'while' => [ 'compound' ], 114 ]; 115 116 /** 117 * A function for use from a PHP CLI which inverts the $nodeTypesByFeature 118 * array to produce $featuresByNodeType. 119 * 120 * @return string 121 */ 122 public static function compileFeaturesByNodeType() { 123 $featuresByNodeType = []; 124 foreach ( self::$nodeTypesByFeature as $feature => $types ) { 125 foreach ( $types as $type ) { 126 $featuresByNodeType[$type][] = $feature; 127 } 128 } 129 $s = ''; 130 ksort( $featuresByNodeType ); 131 foreach ( $featuresByNodeType as $type => $features ) { 132 $s .= "\t'$type' => [ '" . implode( "', '", $features ) . "' ],\n"; 133 } 134 return "private static \$featuresByNodeType = [\n$s];\n"; 135 } 136 137 /** 138 * Get the features used in this shell program 139 * 140 * @return array 141 */ 142 public function getFeatureList() { 143 if ( $this->featureList === null ) { 144 $features = []; 145 $this->root->traverse( 146 function ( $node ) use ( &$features ) { 147 if ( $node instanceof Node ) { 148 $newFeatures = self::$featuresByNodeType[$node->type] ?? []; 149 foreach ( $newFeatures as $feature ) { 150 $features[$feature] = true; 151 } 152 } 153 } 154 ); 155 $this->featureList = array_keys( $features ); 156 } 157 return $this->featureList; 158 } 159 160 /** 161 * If the program is a single command and all of its arguments can be 162 * represented as string literals, return the unquoted literals. Otherwise, 163 * return null. 164 * 165 * @return string[]|null 166 */ 167 public function getLiteralArgv() { 168 if ( $this->literalArgv !== null ) { 169 return $this->literalArgv; 170 } 171 172 $argv = []; 173 $node = $this->root; 174 if ( $this->getNodeType( $node ) !== 'program' || $this->getChildCount( $node ) !== 1 ) { 175 return null; 176 } 177 $node = $node->contents[0]; 178 if ( $this->getNodeType( $node ) !== 'complete_command' || $this->getChildCount( $node ) !== 1 ) { 179 return null; 180 } 181 $node = $node->contents[0]; 182 if ( $this->getNodeType( $node ) !== 'simple_command' ) { 183 return null; 184 } 185 foreach ( $this->getChildren( $node ) as $child ) { 186 $type = $this->getNodeType( $child ); 187 if ( $type !== 'word' ) { 188 continue; 189 } 190 $unquotedWord = $this->unquoteWord( $child ); 191 if ( $unquotedWord === null ) { 192 return null; 193 } 194 $argv[] = $unquotedWord; 195 } 196 $this->literalArgv = $argv; 197 return $argv; 198 } 199 200 /** 201 * @param string|array|Node $node 202 * @return string 203 */ 204 private function getNodeType( $node ) { 205 if ( $node instanceof Node ) { 206 return $node->type; 207 } elseif ( is_array( $node ) ) { 208 return 'array'; 209 } else { 210 return 'string'; 211 } 212 } 213 214 /** 215 * @param string|array|Node $node 216 * @return array 217 */ 218 private function getChildren( $node ) { 219 if ( $node instanceof Node ) { 220 return $node->contents; 221 } else { 222 return []; 223 } 224 } 225 226 /** 227 * @param string|array|Node $node 228 * @return int 229 */ 230 private function getChildCount( $node ) { 231 return count( $this->getChildren( $node ) ); 232 } 233 234 /** 235 * Remove quotes from a word node. If the word cannot be converted to a 236 * literal string, return null. 237 * 238 * @param Node $word 239 * @return string|null 240 */ 241 private function unquoteWord( Node $word ) { 242 $unquotedWord = ''; 243 foreach ( $this->getChildren( $word ) as $part ) { 244 $type = $this->getNodeType( $part ); 245 if ( $type === 'single_quote' 246 || $type === 'unquoted_literal' 247 || $type === 'bare_escape' 248 ) { 249 if ( $this->getChildCount( $part ) !== 1 ) { 250 return null; 251 } 252 $literalPart = $this->getChildren( $part )[0]; 253 if ( !is_string( $literalPart ) ) { 254 return null; 255 } 256 $unquotedWord .= $literalPart; 257 } elseif ( $type === 'double_quote' ) { 258 $literalPart = $this->unquoteDoubleQuote( $part ); 259 if ( !is_string( $literalPart ) ) { 260 return null; 261 } 262 $unquotedWord .= $literalPart; 263 } else { 264 return null; 265 } 266 } 267 return $unquotedWord; 268 } 269 270 /** 271 * Remove quotes from a double-quote node. If it contains expansions, null 272 * will be returned. 273 * 274 * @param Node $dquote 275 * @return string|null 276 */ 277 private function unquoteDoubleQuote( Node $dquote ) { 278 $unquoted = ''; 279 foreach ( $this->getChildren( $dquote ) as $part ) { 280 $type = $this->getNodeType( $part ); 281 if ( $type === 'string' ) { 282 $unquoted .= $part; 283 } elseif ( $type === 'dquoted_escape' ) { 284 if ( $this->getChildCount( $part ) !== 1 ) { 285 return null; 286 } 287 $literalPart = $this->getChildren( $part )[0]; 288 if ( !is_string( $literalPart ) ) { 289 return null; 290 } 291 $unquoted .= $literalPart; 292 } else { 293 return null; 294 } 295 } 296 return $unquoted; 297 } 298} 299