ArgumentParser   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 382
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 92
dl 0
loc 382
rs 9.2
c 0
b 0
f 0
wmc 40

3 Methods

Rating   Name   Duplication   Size   Complexity  
C parse() 0 154 14
A __construct() 0 5 1
D yieldArgumentsFromRule() 0 163 25

How to fix   Complexity   

Complex Class

Complex classes like ArgumentParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArgumentParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
	
3
/**
4
	* ArgumentParser.php
5
	*/
6
	
7
namespace netfocusinc\argh;
8
9
/**
10
	* Internal class that performs the work of parsing command line arguments.
11
	*
12
	* Uses the provided Language and ParameterCollection to interpret an array of command line arguments.
13
	*
14
	* @author Benjamin Hough
15
	*
16
	* @internal
17
	*
18
	* @since 1.0.0
19
	*/
20
class ArgumentParser
21
{
22
	
23
	//
24
	// PRIVATE PROPERTIES
25
	//
26
	
27
	/** @var Language A set of Rules used to interpret command line arguments */
28
	private $language;
29
	
30
	/** @var ParameterCollection A collection of Parameters used to interpret command line arguments */
31
	private $parameterCollection;
32
	
33
	//
34
	// PUBLIC METHODS
35
	//
36
	
37
	/**
38
		* Constructs a new ArgumentParser
39
		*
40
		* Creates a new ArgumentParser instance with the specified Langugage and ParameterCollection
41
		* The resulting instance is ready for parsing an array of arguments.
42
		*
43
		* @since 1.0.0
44
		*/
45
	public function __construct(Language $language, ParameterCollection $parameterCollection)
46
	{
47
		// Init properties on this instance
48
		$this->language = $language;
49
		$this->parameterCollection = $parameterCollection;
50
	}
51
	
52
		
53
	/**
54
		* Parse an array of command line arguments.
55
		*
56
		* Interprets an array of command line arguments using the Language and ParameterCollection
57
		* that was configured during construction.
58
		* When successful, this results in an array of Arguments (with key,value pairs).
59
		*
60
		* @param array $args A pre-processed $argv array
61
		*
62
		* @return Argument[]
63
		*
64
		* @throws ArgumentException
65
		*/
66
	public function parse(array $args): array
67
	{
68
		// Init an array of Arguments
69
		$arguments = array();
70
			
71
		if(count($args) == 0)
72
		{
73
			// Nothing to parse
74
			return $arguments;
75
		}
76
		
77
		// Get all Rules from Langugage
78
		$rules = $this->language->rules();
79
		
80
		// Get all Parameters from ParameterCollection
81
		$params = $this->parameterCollection->all();
82
		
83
		if( count($rules) == 0 )
84
		{
85
			throw new ArghException(__CLASS__ . ': Language needs at least one rule to parse arguments.');
86
		}
87
		
88
		if( count($params) == 0 )
89
		{
90
			throw new ArghException(__CLASS__ . ': ParameterCollection needs at least one parameter to parse arguments.');
91
		}
92
		
93
		// As parsing progresses, args will be divided into 2-sides (Left-and-Right)
94
		// The Left-Hand-Side will contain args to attempt matching with rules
95
		// The Right-Hand-Side will save args that didn't match in previous iterations (to be checked again later)
96
		
97
		do
98
		{
99
			// Reset temporary variables
100
			$argsL = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $argsL is dead and can be removed.
Loading history...
101
			$argsR = array();
102
			$argsS = "";
0 ignored issues
show
Unused Code introduced by
The assignment to $argsS is dead and can be removed.
Loading history...
103
			
104
			// Copy remaining $args to $argsL (left-hand-side array)
105
			$argsL = array_merge($args);
106
			
107
			do
108
			{
109
				// Combine $argsL elements into a single string, for matching against rules
110
				$argsS = implode(' ', $argsL);
111
				
112
				//
113
				// DEBUG: Show detailed contents of each variable
114
				//
115
						
116
						/*
117
						echo "\n\n";
118
						echo implode(' ', $argsL) . " | " . implode(' ', $argsR) . "\n";
119
						
120
						for($i=0; $i<count($argsL); $i++)
121
						{
122
							for($j=0; $j<strlen($argsL[$i]); $j++)
123
							{
124
								echo $i;
125
							}
126
							echo " ";
127
						}
128
						echo "| ";
129
						for($i=0; $i<count($argsR); $i++)
130
						{
131
							for($j=0; $j<strlen($argsR[$i]); $j++)
132
							{
133
								echo $i;
134
							}
135
							echo " ";
136
						}			
137
						echo "\n\n";
138
						
139
						echo "\nDEBUG: Considering: " . $argsS . " ... \n\n";
140
						*/
141
						
142
				
143
				//
144
				// END DEBUG
145
				//
146
				
147
				foreach($rules as $rule)
148
				{
149
					//echo "DEBUG: Checking for match with rule: " . $rule->name() . " (" . $rule->syntax() . ")" . "\n";
150
					
151
					$tokens = array(); // init array to capture matching tokens from Rule->match()
152
					
153
					if( $rule->match($argsS, $tokens) )
154
					{
155
						// Count the number of arguments that were matched
156
						$count = count($argsL);
157
						
158
						//echo "* MATCHED $count \$argv elements *\n";
159
						
160
						// Empty $argsL; prevent this inner foreach loop from continuing
161
						for($i=0; $i<$count; $i++) array_shift($argsL);
162
						
163
						// Remove (shift) matching elements from $args
164
						// These arguments have been consumed by the parser and are no longer needed
165
						for($i=0; $i<$count; $i++) array_shift($args);
166
 						
167
 						//
168
 						// Try yielding Arguments from this Rule
169
 						// If this Rule does not yield any Arguments, continue checking the next Rule
170
 						//
171
 						
172
 						$yield = $this->yieldArgumentsFromRule($rule, $tokens);
173
						
174
	 					if( count($yield) > 0 )
175
	 					{
176
		 					//? TODO: Validate Arguments before adding them to the Arguments array?
177
		 					
178
		 					// Add the new Arguments yielded from this Rule
179
		 					foreach($yield as $y) $arguments[] = $y;
180
	 						
181
	 						// !IMPORTANT! Stop checking Rules
182
	 						break; 
183
	 						
184
	 					} // END: if(count($argument) > 0)
185
	 					else
186
	 					{
187
		 					// This Rule did not create any Arguments, keep checking with next Rule
188
		 				}
189
						
190
					} // END: if( preg_match($rule->syntax, $args[$i], $matches) )
191
					
192
				} // END: foreach($rules as $rule)
193
				
194
				if( count($tokens) == 0 )
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tokens does not seem to be defined for all execution paths leading up to this point.
Loading history...
195
				{
196
					// $argsS did NOT match any rules
197
					
198
					// Pop last element off of $argsL
199
					$arg = array_pop($argsL);
200
					
201
					// Prepend popped elemented to beginning of $argsR
202
					array_unshift($argsR, $arg);
203
					
204
					if( count($argsL) == 0 )
205
					{
206
						// There was no match, and there are no arguments left to pop from $argsL
207
						throw new ArghException(__METHOD__ . ': Syntax Error: ' . $argsS);
208
					}
209
					
210
				} // END: if( count($tokens) == 0 )
211
				
212
			} // END do
213
			while( count($argsL) > 0 );
214
			
215
		} // END: do
216
		while( count($args) > 0 );
217
		
218
		// Return Arguments array
219
		return $arguments;
220
		
221
	} // END: public static function parse()
222
	
223
	/**
224
		* Attempts to create Arguments given a Rule and matching tokens (from the command line arguments string)
225
		*
226
		* When a set of matching tokens (from a command line argument string) is matched with a Rule (by the parse method)
227
		* This function is used to determine if the matching tokens can yield an Argument from the matched Rule.
228
		* This requires checking that the SEMANTICS of the Rule correspond with the matched tokens
229
		* For example, if -xvf matches the Rule (hypenated multi flag), each character (of xvf) must also correspond to the flag of a 
230
		* Parameter that was defined by the client. When no Argument is yielded, the parser can then attempt to match the tokens with another Rule.
231
		*
232
		* @internal
233
		*
234
		* @param $rule Rule
235
		* @param $tokens string[]
236
		*
237
		* @return Argument[] An array of Arguments
238
		*/
239
	public function yieldArgumentsFromRule(Rule $rule, array $tokens): array
240
	{
241
		
242
		// Create an array of new Argument
243
		// In most cases, a single Rule will create a single Arugment
244
		// Unless the Rule contains an ARGH_SEMANTICS_FLAGS, which creates an Argument for each flag
245
		
246
		$argument = array();
247
248
		// Loop through $tokens and define Argument(s) based on the current rules semantics
249
		$count_tokens = count($tokens);
250
		
251
		for($i=1; $i<$count_tokens; $i++)
252
		{
253
			$token = $tokens[$i];
254
			$semantics = $rule->semantics()[$i-1];
255
256
			//echo __METHOD__ . ": token: $token (" . Rule::semanticsToString($semantics) . ")\n";
257
258
			switch( $semantics )
259
			{ 
260
				case ARGH_SEMANTICS_FLAG:
261
				
262
					if( $this->parameterCollection->exists($token) )
263
					{	
264
						// This Rule will create a single Argument
265
						if(count($argument)==0) $argument[0] = new Argument($token);								
266
					}
267
					else
268
					{
269
						// This token does NOT match the flag of any defined parameter
270
						// This Rule will NOT yield any arguments
271
						break 2; // Break from this switch and for loop
272
					}
273
					
274
					break;
275
					
276
				case ARGH_SEMANTICS_FLAGS:
277
					
278
					// Check every character of this $token for a matching parameter 'flag'
279
					for($j=0; $j<strlen($token); $j++)
280
					{
281
						if( $this->parameterCollection->exists( $token{$j} ) )
282
						{
283
							// This Rule can only apply to ARGH_TYPE_BOOLEAN Parameters
284
							if( ARGH_TYPE_BOOLEAN == $this->parameterCollection->get($token{$j})->getParameterType() )
285
							{
286
								// Create new Argument for each flag
287
								if( !array_key_exists($j, $argument) ) $argument[$j] = new Argument($token{$j});
288
							}
289
						}
290
						else
291
						{
292
							// A character in $token, does not match a defined Parameter flag
293
							// This Rule will NOT yield any arguments
294
							// Undo the creation of new Arguments under this Rule
295
							$argument = array();
296
							
297
							break; // Break from this for loop
298
						}		
299
						
300
					} // END: for($j=0; $j<strlen($token); $j++)
301
					
302
					break;
303
					
304
				case ARGH_SEMANTICS_NAME:
305
			
306
					if( $this->parameterCollection->exists($token) )
307
					{
308
						// This Rule will create a single Argument
309
						if(count($argument)==0) $argument[0] = new Argument($token); 								
310
					}
311
					else
312
					{
313
						// This token does NOT match the flag of any defined parameter
314
						// This Rule will NOT yield any arguments
315
						break 2; // Break from this switch and for loop
316
					}
317
			
318
					break;			
319
					
320
				case ARGH_SEMANTICS_VALUE:
321
				
322
					// Usually, the Argument will have already been created by another token in this Rule
323
				
324
					// If no new Argument created by this Rule yet, create one now
325
					if(count($argument)==0) $argument[0] = new Argument();
326
					
327
					// The new Argument's 'key' should be set by another token in this Rule
328
					$argument[0]->setValue($token);
329
					
330
					break;
331
					
332
				case ARGH_SEMANTICS_LIST:
333
				
334
					// Usually, the Argument will have already been created by another token in this Rule
335
				
336
					// If no new Argument created by this Rule yet, create one now
337
					if(count($argument)==0) $argument[0] = new Argument();
338
				
339
					// Trim brackets from the $token (list)
340
					$token = trim($token, "[]");
341
					
342
					// Explode comma seperated list into elements
343
					$elements = explode(',', $token);
344
					
345
					// Use the $elements array as the 'value' for all new Argument created by this Rule
346
					// Usually, this will only apply to a single Argument, unless this Rule contains ARGH_SEMANTICS_FLAGS
347
					foreach($argument as &$a) $a->setValue($elements);
348
				
349
					break;
350
					
351
				case ARGH_SEMANTICS_COMMAND:
352
				
353
					// Check if ParameterCollection contains any commands
354
					if($this->parameterCollection->hasCommand())
355
					{
356
						// Retrieve all ARGH_TYPE_COMMAND Parameters
357
						$commands = $this->parameterCollection->getCommands();
358
							
359
						foreach($commands as $p)
360
						{
361
							if($p->hasOptions())
362
							{
363
								if( in_array($token, $p->getOptions()) )
364
								{
365
									// $token matches an option of this ARGH_TYPE_COMMAND Parameter	
366
									
367
				 					// If no new Argument created by this Rule yet, create one now
368
				 					if(count($argument)==0) $argument[0] = new Argument($p->getName(), $token);
369
									
370
									// Stop searching this Parameters options
371
									break;
372
									
373
								} // END: if( in_array($token, $p->options()) )
374
							} // END: if($p->hasOptions())
375
						} // END: foreach($commands as $p)
376
							
377
378
					} // END: if($this->parameterCollection->hasCommand())
379
				
380
					break;
381
					
382
				case ARGH_SEMANTICS_VARIABLE:
383
					
384
					// Create a new Argument to hold values
385
					$argument[0] = new Argument(Parameter::ARGH_NAME_VARIABLE, $token);
386
					
387
					break;
388
					
389
				default:
390
				
391
					throw new ArghException(__CLASS__ . ': Token has unknown semantic meaning.');
392
			}
393
			
394
		} // END: for($j=1; $j<count($matches); $j++)
395
		
396
		
397
		//echo 'Yielded Arguments:' . "\n";
398
		//print_r($argument);
399
		
400
		// Return an array of Arguments yielded by this Rule
401
		return $argument;
402
		
403
	} // END: yieldArgumentsFromRule(Rule $rule, array $tokens): array
404
	
405
}