6 use InvalidArgumentException;
9 * Forms a front controller for a console app, handling and validating arguments (options)
11 * Instantiate, add arguments, then call validate(). Afterwards, the user's valid arguments
12 * and their values will be available in $cli->values.
14 * You may also specify that some arguments be used to provide input/output. By communicating
15 * solely through the file pointers provided by openInput()/openOutput(), you can make your
16 * app more flexible to end users.
18 * @author Steve Clay <steve@mrclay.org>
19 * @license http://www.opensource.org/licenses/mit-license.php MIT License
24 * @var array validation errors
26 public $errors = array();
29 * @var array option values available after validation.
32 * 'a' => false // option was missing
33 * ,'b' => true // option was present
34 * ,'c' => "Hello" // option had value
35 * ,'f' => "/home/user/file" // file path from root
36 * ,'f.raw' => "~/file" // file path as given to option
39 public $values = array();
44 public $moreArgs = array();
49 public $debug = array();
52 * @var bool The user wants help info
54 public $isHelpRequest = false;
59 protected $_args = array();
64 protected $_stdin = null;
69 protected $_stdout = null;
72 * @param bool $exitIfNoStdin (default true) Exit() if STDIN is not defined
74 public function __construct($exitIfNoStdin = true)
76 if ($exitIfNoStdin && ! defined('STDIN')) {
77 exit('This script is for command-line use only.');
79 if (isset($GLOBALS['argv'][1])
80 && ($GLOBALS['argv'][1] === '-?' || $GLOBALS['argv'][1] === '--help')) {
81 $this->isHelpRequest = true;
86 * @param Arg|string $letter
89 public function addOptionalArg($letter)
91 return $this->addArgument($letter, false);
95 * @param Arg|string $letter
98 public function addRequiredArg($letter)
100 return $this->addArgument($letter, true);
104 * @param string $letter
105 * @param bool $required
106 * @param Arg|null $arg
108 * @throws InvalidArgumentException
110 public function addArgument($letter, $required, Arg $arg = null)
112 if (! preg_match('/^[a-zA-Z]$/', $letter)) {
113 throw new InvalidArgumentException('$letter must be in [a-zA-Z]');
116 $arg = new Arg($required);
118 $this->_args[$letter] = $arg;
123 * @param string $letter
126 public function getArgument($letter)
128 return isset($this->_args[$letter]) ? $this->_args[$letter] : null;
132 * Read and validate options
134 * @return bool true if all options are valid
136 public function validate()
139 $this->errors = array();
140 $this->values = array();
141 $this->_stdin = null;
143 if ($this->isHelpRequest) {
148 foreach ($this->_args as $letter => $arg) {
151 $lettersUsed .= $letter;
153 if ($arg->mayHaveValue || $arg->mustHaveValue) {
154 $options .= ($arg->mustHaveValue ? ':' : '::');
158 $this->debug['argv'] = $GLOBALS['argv'];
159 $argvCopy = array_slice($GLOBALS['argv'], 1);
160 $o = getopt($options);
161 $this->debug['getopt_options'] = $options;
162 $this->debug['getopt_return'] = $o;
164 foreach ($this->_args as $letter => $arg) {
166 $this->values[$letter] = false;
167 if (isset($o[$letter])) {
168 if (is_bool($o[$letter])) {
170 // remove from argv copy
171 $k = array_search("-$letter", $argvCopy);
173 array_splice($argvCopy, $k, 1);
176 if ($arg->mustHaveValue) {
177 $this->addError($letter, "Missing value");
179 $this->values[$letter] = true;
183 $this->values[$letter] = $o[$letter];
184 $v =& $this->values[$letter];
186 // remove from argv copy
187 // first look for -ovalue or -o=value
188 $pattern = "/^-{$letter}=?" . preg_quote($v, '/') . "$/";
189 $foundInArgv = false;
190 foreach ($argvCopy as $k => $argV) {
191 if (preg_match($pattern, $argV)) {
192 array_splice($argvCopy, $k, 1);
197 if (! $foundInArgv) {
199 $k = array_search("-$letter", $argvCopy);
201 array_splice($argvCopy, $k, 2);
205 // check that value isn't really another option
206 if (strlen($lettersUsed) > 1) {
207 $pattern = "/^-[" . str_replace($letter, '', $lettersUsed) . "]/i";
208 if (preg_match($pattern, $v)) {
209 $this->addError($letter, "Value was read as another option: %s", $v);
213 if ($arg->assertFile || $arg->assertDir) {
214 if ($v[0] !== '/' && $v[0] !== '~') {
215 $this->values["$letter.raw"] = $v;
216 $v = getcwd() . "/$v";
219 if ($arg->assertFile) {
220 if ($arg->useAsInfile) {
222 } elseif ($arg->useAsOutfile) {
225 if ($arg->assertReadable && ! is_readable($v)) {
226 $this->addError($letter, "File not readable: %s", $v);
229 if ($arg->assertWritable) {
231 if (! is_writable($v)) {
232 $this->addError($letter, "File not writable: %s", $v);
235 if (! is_writable(dirname($v))) {
236 $this->addError($letter, "Directory not writable: %s", dirname($v));
240 } elseif ($arg->assertDir && $arg->assertWritable && ! is_writable($v)) {
241 $this->addError($letter, "Directory not readable: %s", $v);
245 if ($arg->isRequired()) {
246 $this->addError($letter, "Missing");
250 $this->moreArgs = $argvCopy;
251 reset($this->moreArgs);
252 return empty($this->errors);
256 * Get the full paths of file(s) passed in as unspecified arguments
260 public function getPathArgs()
262 $r = $this->moreArgs;
263 foreach ($r as $k => $v) {
264 if ($v[0] !== '/' && $v[0] !== '~') {
265 $v = getcwd() . "/$v";
266 $v = str_replace('/./', '/', $v);
268 $v = preg_replace('@/[^/]+/\\.\\./@', '/', $v, 1, $changed);
277 * Get a short list of errors with options
281 public function getErrorReport()
283 if (empty($this->errors)) {
286 $r = "Some arguments did not pass validation:\n";
287 foreach ($this->errors as $letter => $arr) {
288 $r .= " $letter : " . implode(', ', $arr) . "\n";
297 public function getArgumentsListing()
300 foreach ($this->_args as $letter => $arg) {
302 $desc = $arg->getDescription();
303 $flag = " -$letter ";
304 if ($arg->mayHaveValue) {
306 } elseif ($arg->mustHaveValue) {
309 if ($arg->assertFile) {
310 $flag = str_replace('VAL', 'FILE', $flag);
311 } elseif ($arg->assertDir) {
312 $flag = str_replace('VAL', 'DIR', $flag);
314 if ($arg->isRequired()) {
315 $desc = "(required) $desc";
317 $flag = str_pad($flag, 12, " ", STR_PAD_RIGHT);
318 $desc = wordwrap($desc, 70);
319 $r .= $flag . str_replace("\n", "\n ", $desc) . "\n\n";
325 * Get resource of open input stream. May be STDIN or a file pointer
326 * to the file specified by an option with 'STDIN'.
330 public function openInput()
332 if (null === $this->_stdin) {
335 $this->_stdin = fopen($this->_stdin, 'rb');
336 return $this->_stdin;
340 public function closeInput()
342 if (null !== $this->_stdin) {
343 fclose($this->_stdin);
348 * Get resource of open output stream. May be STDOUT or a file pointer
349 * to the file specified by an option with 'STDOUT'. The file will be
350 * truncated to 0 bytes on opening.
354 public function openOutput()
356 if (null === $this->_stdout) {
359 $this->_stdout = fopen($this->_stdout, 'wb');
360 return $this->_stdout;
364 public function closeOutput()
366 if (null !== $this->_stdout) {
367 fclose($this->_stdout);
372 * @param string $letter
374 * @param string $value
376 protected function addError($letter, $msg, $value = null)
378 if ($value !== null) {
379 $value = var_export($value, 1);
381 $this->errors[$letter][] = sprintf($msg, $value);