Skip to content

Commit 261a355

Browse files
committed
interactive arguments reader subcommand added
1 parent 81130f4 commit 261a355

12 files changed

+485
-49
lines changed

README.md

+80
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ A library to build command line applications using PHP. This is part of the [Tar
2020

2121
- [Defining Arguments and Options](#defining-arguments-and-options)
2222

23+
- [Reading Arguments and Options Interactively](#reading-arguments-and-options-interactively) **New on version 1.1.0**
2324
- [Handeling The Filesystem](#handeling-the-filesystem)
2425

2526
- [Rendering Templates](#rendering-templates)
@@ -237,6 +238,83 @@ In the second example, the `count` argument takes autmatically its default value
237238

238239
![Parse error example](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/repeat-args-missing.png)
239240

241+
# Reading Arguments and Options Interactively
242+
243+
Some commands can have long and complicated list of arguments. Defining the syntax of such command is easy thanks to [Syntax](https://github.com/tarsana/syntax) but typing the arguments in the command line becomes challenging.
244+
245+
Let's take the following command for example:
246+
247+
```php
248+
class ClassGenerator extends Command {
249+
protected function init()
250+
{
251+
$this->name('Class Generator')
252+
->version('1.0.0')
253+
->description('Generates basic code for a class.')
254+
->syntax('
255+
language: string,
256+
name: string,
257+
parents: ([string]:[]),
258+
interfaces: ([string]:[]),
259+
attrs: [{
260+
name,
261+
type,
262+
hasGetter: (boolean:true),
263+
hasSetter: (boolean:true),
264+
isStatic: (boolean:false)
265+
}],
266+
methods: ([{
267+
name: string,
268+
type: string,
269+
args: [{ name, type, default: (string:null) |.}],
270+
isStatic: (boolean:false)
271+
}]:[])
272+
')
273+
->descriptions([
274+
'language' => 'The programming language in which the code will be generated.',
275+
'name' => 'The name of the class.',
276+
'parents' => 'List of parent classes names.',
277+
'interfaces' => 'List of implemented interfaces.',
278+
'attrs' => 'List of attributes of the class.',
279+
'attrs.name' => 'The name of the attribute.',
280+
'attrs.type' => 'The type of the attribute.',
281+
'attrs.hasGetter' => 'Generate a getter for the attribute.',
282+
'attrs.hasSetter' => 'Generate a setter for the attribute.',
283+
'attrs.isStatic' => 'The attribute is static.',
284+
'methods' => 'List of methods of the class.',
285+
'methods.name' => 'The method name.',
286+
'methods.type' => 'The method return type.',
287+
'methods.args' => 'List of arguments of the method.',
288+
'methods.isStatic' => 'This method is static.'
289+
]);
290+
}
291+
292+
protected function execute()
293+
{
294+
$this->console->line("Generate code for the class {$this->args->name} in {$this->args->language}...");
295+
296+
}
297+
}
298+
```
299+
300+
if you run the command using the `-i` flag, it will let you enter the arguments interactively:
301+
302+
![Interactive Arguments Reader](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/interactive-args.gif)
303+
304+
After reading all args, the command will show the command line version of the entered args:
305+
306+
```
307+
> PHP User Serializable name:string:true:true:false
308+
```
309+
310+
which means that running
311+
312+
```
313+
$ php class.php PHP User Serializable name:string:true:true:false
314+
```
315+
316+
would produce the same result.
317+
240318
# Handling The Filesystem
241319

242320
The `fs` attribute is an instance of `Tarsana\IO\Filesystem` that you can use to handle files and directories. [Read the documentation](https://github.com/tarsana/io#handeling-files-and-directories) for the full API.
@@ -510,6 +588,8 @@ Please take a look at the examples in the `examples` directory, and try using th
510588

511589
# Development Notes
512590

591+
- **Version 1.1.0** The flag `-i` added to commands to enable interactive reading of arguments and options.
592+
513593
- **Version 1.0.1** Fixed a bug of subcommands having different instances of `fs` and `templatesLoader` from their parent.
514594

515595
- **Version 1.0.0** The first version is finally out; have fun!

docs/screenshots/interactive-args.gif

76.3 KB
Loading

examples/tests/RepeatCommandTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,18 @@ public function test_it_repeats_word_n_times_uppercase()
4545
->printsExactly("BARBARBARBARBAR<br>");
4646
}
4747

48+
public function test_it_runs_interatively()
49+
{
50+
$this->withStdin("Yo\n\n\n")
51+
->command(new RepeatCommand, ['-i'])
52+
->argsEqual((object) [
53+
'word' => 'Yo',
54+
'count' => 3
55+
])
56+
->optionsEqual([
57+
'--upper' => false
58+
])
59+
->prints("YoYoYo<br>");
60+
}
61+
4862
}

src/Command.php

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php namespace Tarsana\Command;
22

33
use Tarsana\Command\Commands\HelpCommand;
4+
use Tarsana\Command\Commands\InteractiveCommand;
45
use Tarsana\Command\Commands\VersionCommand;
56
use Tarsana\Command\Console\Console;
67
use Tarsana\Command\Console\ExceptionPrinter;
@@ -298,7 +299,8 @@ public function hasCommand(string $name) : bool
298299
protected function setupSubCommands()
299300
{
300301
return $this->command('--help', new HelpCommand($this))
301-
->command('--version', new VersionCommand($this));
302+
->command('--version', new VersionCommand($this))
303+
->command('-i', new InteractiveCommand($this));
302304
}
303305

304306
public function describe(string $name, string $description = null)
@@ -347,24 +349,28 @@ public function run(array $args = null, array $options = [], bool $rawArgs = tru
347349
}
348350
}
349351

350-
if (null === $this->action)
351-
$this->execute();
352-
else
353-
($this->action)($this);
352+
return $this->fire();
354353
} catch (\Exception $e) {
355354
$this->handleError($e);
356355
}
357356
}
358357

359-
public function clear()
358+
protected function fire()
359+
{
360+
return (null === $this->action)
361+
? $this->execute()
362+
: ($this->action)($this);
363+
}
364+
365+
protected function clear()
360366
{
361367
$this->args = null;
362368
foreach($this->options as $name => $value) {
363369
$this->options[$name] = false;
364370
}
365371
}
366372

367-
public function parseArguments(array $args)
373+
protected function parseArguments(array $args)
368374
{
369375
if (null === $this->syntax) {
370376
$this->args = null;

src/Commands/HelpCommand.php

+54-40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php namespace Tarsana\Command\Commands;
22

3+
use Tarsana\Command\Helpers\SyntaxHelper;
34
use Tarsana\Command\SubCommand;
45
use Tarsana\Syntax\ArraySyntax;
56
use Tarsana\Syntax\ObjectSyntax;
@@ -8,67 +9,78 @@
89

910
class HelpCommand extends SubCommand {
1011

12+
protected $helper;
13+
1114
protected function init()
1215
{
1316
$this->name('Help')
14-
->description('Shows the help message');
17+
->description('Shows the help message.');
18+
$this->helper = SyntaxHelper::instance();
1519
}
1620

1721
protected function execute()
1822
{
19-
$c = $this->console();
20-
$c->line("<info>{$this->parent()->name()}</info> version <info>{$this->parent()->version()}</info>");
21-
$c->line("<br>{$this->parent()->description()}<br>");
23+
$parent = $this->parent;
24+
25+
$text = "<info>{$parent->name}</info> version <info>{$parent->version}</info>"
26+
. "<br><br>{$parent->description}<br><br>"
27+
. $this->syntaxHelp()
28+
. $this->optionsHelp()
29+
. $this->subCommandsHelp();
30+
31+
$this->console()->out($text);
32+
}
33+
34+
protected function syntaxHelp() : string
35+
{
36+
$syntax = $this->parent->syntax();
37+
$helper = $this->helper;
38+
$text = '';
2239

23-
$syntax = $this->parent()->syntax();
2440
if ($syntax) {
25-
$c->line("Syntax: <success>[options] " . $this->formatSyntax($syntax) . "</success>");
26-
$c->line("Arguments:");
41+
$string = $helper->asString($syntax);
42+
$text .= "Syntax: <success>[options] {$string}</success><br>"
43+
. "Arguments:<br>";
2744
foreach ($syntax->fields() as $name => $s) {
28-
$this->printField($name, $s);
45+
$text .= $this->fieldHelp($name, $s);
2946
}
3047
}
3148

32-
$options = array_keys($this->parent()->options());
49+
return $text;
50+
}
51+
52+
protected function optionsHelp() : string
53+
{
54+
$options = array_keys($this->parent->options());
55+
$text = '';
3356
if (!empty($options)) {
34-
$c->line("Options:");
57+
$text .= 'Options:<br>';
3558
foreach ($options as $name) {
3659
$description = $this->parent()->describe($name);
37-
$c->line("<tab><warn>{$name}</warn> {$description}");
60+
$text .= "<tab><warn>{$name}</warn> {$description}<br>";
3861
}
3962
}
4063

41-
$subCommands = $this->parent()->commands();
64+
return $text;
65+
}
66+
67+
protected function subCommandsHelp() : string
68+
{
69+
$subCommands = $this->parent->commands();
70+
$text = '';
4271
if (!empty($subCommands)) {
43-
$c->line("SubCommands:");
72+
$text .= 'SubCommands:<br>';
4473
foreach ($subCommands as $name => $cmd) {
45-
$c->line("<tab><warn>{$name}</warn> {$cmd->description()}");
74+
$text .= "<tab><warn>{$name}</warn> {$cmd->description()}<br>";
4675
}
4776
}
48-
}
4977

50-
protected function formatSyntax(Syntax $s) : string
51-
{
52-
if ($s instanceof ObjectSyntax)
53-
return implode($s->separator(), array_keys($s->fields()));
54-
if ($s instanceof ArraySyntax)
55-
return $this->formatSyntax($s->syntax()) . $s->separator() . '...';
56-
if ($s instanceof OptionalSyntax)
57-
return $this->formatSyntax($s->syntax());
58-
59-
return (string) $s;
78+
return $text;
6079
}
6180

62-
protected function getFields(Syntax $s) : array
63-
{
64-
if ($s instanceof ObjectSyntax)
65-
return $s->fields();
66-
if ($s instanceof ArraySyntax || $s instanceof OptionalSyntax)
67-
return $this->getFields($s->syntax());
68-
return [];
69-
}
70-
71-
protected function printField(string $name, Syntax $s, string $prefix = '', int $level = 1)
81+
protected function fieldHelp(
82+
string $name, Syntax $s, string $prefix = '', int $level = 1
83+
) : string
7284
{
7385
$tabs = str_repeat('<tab>', $level);
7486
$optional = ($s instanceof OptionalSyntax);
@@ -77,14 +89,16 @@ protected function printField(string $name, Syntax $s, string $prefix = '', int
7789
else
7890
$default = 'required';
7991
$description = $this->parent()->describe($prefix.$name);
80-
$syntax = $this->formatSyntax($s);
81-
$this->console()->line("{$tabs}<warn>{$name}</warn> <success>{$syntax}</success> {$description} <info>({$default})</info>");
82-
92+
$syntax = $this->helper->asString($s);
93+
$text = "{$tabs}<warn>{$name}</warn> <success>{$syntax}</success>"
94+
. " {$description} <info>({$default})</info><br>";
8395
$level ++;
8496
$prefix .= $name . '.';
85-
foreach ($this->getFields($s) as $field => $syntax) {
86-
$this->printField($field, $syntax, $prefix, $level);
97+
foreach ($this->helper->fields($s) as $field => $syntax) {
98+
$text .= $this->fieldHelp($field, $syntax, $prefix, $level);
8799
}
100+
101+
return $text;
88102
}
89103

90104
}

0 commit comments

Comments
 (0)