Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,14 @@ $app
})
;

// Maybe it could be named `handle()` or `run()`, but again we keep legacy of `commander.js`
// Parse only parses input but doesnt invoke action
$app->parse(['git', 'add', 'path1', 'path2', 'path3', '-f']);

// Handle will do both parse and invoke action.
$app->handle(['git', 'add', 'path1', 'path2', 'path3', '-f']);
// Will produce: Add path1, path2, path3 with force

$app->parse(['git', 'co', '-b', 'master-2', '-f']);
$app->handle(['git', 'co', '-b', 'master-2', '-f']);
// Will produce: Checkout to new master-2 with force
```

Expand Down
217 changes: 199 additions & 18 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Ahc\Cli\Helper\OutputHelper;
use Ahc\Cli\Input\Command;
use Ahc\Cli\Output\Writer;
use Ahc\Cli\IO\Interactor;

/**
* A cli application.
Expand All @@ -31,45 +31,128 @@ class Application
/** @var string App version */
protected $version = '0.0.1';

/** @var string Ascii art logo */
protected $logo = '';

protected $default = '__default__';

/** @var Interactor */
protected $io;

public function __construct(string $name, string $version = '', callable $onExit = null)
{
$this->name = $name;
$this->version = $version;

// @codeCoverageIgnoreStart
$this->onExit = $onExit ?? function () {
exit(0);
$this->onExit = $onExit ?? function ($exitCode = 0) {
exit($exitCode);
};
// @codeCoverageIgnoreEnd

$this->command = $this->command('__default__', 'Default command', '', true)
->on([$this, 'showHelp'], 'help');

unset($this->commands['__default__']);
$this->command('__default__', 'Default command', '', true)->on([$this, 'showHelp'], 'help');
}

/**
* Get the name.
*
* @return string
*/
public function name(): string
{
return $this->name;
}

/**
* Get the version.
*
* @return string
*/
public function version(): string
{
return $this->version;
}

/**
* Get the commands.
*
* @return Command[]
*/
public function commands(): array
{
return $this->commands;
$commands = $this->commands;

unset($commands['__default__']);

return $commands;
}

/**
* Get the raw argv.
*
* @return array
*/
public function argv(): array
{
return $this->argv;
}

public function command(string $name, string $desc = '', string $alias = '', bool $allowUnknown = false): Command
/**
* Sets or gets the ASCII art logo.
*
* @param string|null $logo
*
* @return string|self
*/
public function logo(string $logo = null)
{
if (\func_num_args() === 0) {
return $this->logo;
}

$this->logo = $logo;

return $this;
}

/**
* Add a command by its name desc alias etc.
*
* @param string $name
* @param string $desc
* @param string $alias
* @param bool $allowUnknown
* @param bool $default
*
* @return Command
*/
public function command(
string $name,
string $desc = '',
string $alias = '',
bool $allowUnknown = false,
bool $default = false
): Command {
$command = new Command($name, $desc, $allowUnknown, $this);

$this->add($command, $alias, $default);

return $command;
}

/**
* Add a prepred command.
*
* @param Command $command
* @param string $alias
* @param bool $default
*
* @return self
*/
public function add(Command $command, string $alias = '', bool $default = false): self
{
$name = $command->name();

if ($this->commands[$name] ?? $this->aliases[$name] ?? $this->commands[$alias] ?? $this->aliases[$alias] ?? null) {
throw new \InvalidArgumentException(\sprintf('Command "%s" already added', $name));
}
Expand All @@ -78,11 +161,22 @@ public function command(string $name, string $desc = '', string $alias = '', boo
$this->aliases[$alias] = $name;
}

$command = (new Command($name, $desc, $allowUnknown, $this))->version($this->version)->onExit($this->onExit);
if ($default) {
$this->default = $name;
}

$this->commands[$name] = $command->version($this->version)->onExit($this->onExit)->bind($this);

return $this->commands[$name] = $command;
return $this;
}

/**
* Gets matching command for given argv.
*
* @param array $argv
*
* @return Command
*/
public function commandFor(array $argv): Command
{
$argv += [null, null, null];
Expand All @@ -93,9 +187,36 @@ public function commandFor(array $argv): Command
// cmd alias
?? $this->commands[$this->aliases[$argv[1]] ?? null]
// default.
?? $this->command;
?? $this->commands[$this->default];
}

/**
* Gets or sets io.
*
* @param Interactor|null $io
*
* @return Interactor|self
*/
public function io(Interactor $io = null)
{
if ($io || !$this->io) {
$this->io = $io ?? new Interactor;
}

if (\func_num_args() === 0) {
return $this->io;
}

return $this;
}

/**
* Parse the arguments via the matching command but dont execute action..
*
* @param array $argv Cli arguments/options.
*
* @return Command The matched and parsed command (or default)
*/
public function parse(array $argv): Command
{
$this->argv = $argv;
Expand All @@ -114,13 +235,41 @@ public function parse(array $argv): Command
}
}

$command->parse($argv);
return $command->parse($argv);
}

$this->doAction($command);
/**
* Handle the request, invoke action and call exit handler.
*
* @param array $argv
*
* @return mixed
*/
public function handle(array $argv)
{
$io = $this->io();

return $command;
try {
$exitCode = 0;
$command = $this->parse($argv);

$this->doAction($command);
} catch (\Throwable $e) {
$exitCode = 255;
$location = 'At file ' . $e->getFile() . '#' . $e->getLine();
$io->error($e->getMessage(), true)->bgRed($location, true);
}

return ($this->onExit)($exitCode);
}

/**
* Get aliases for given command.
*
* @param Command $command
*
* @return array
*/
protected function aliasesFor(Command $command): array
{
$aliases = [$name = $command->name()];
Expand All @@ -135,28 +284,60 @@ protected function aliasesFor(Command $command): array
return $aliases;
}

public function showHelp(Writer $writer = null)
/**
* Show help of all commands.
*
* @return mixed
*/
public function showHelp()
{
$writer = $this->io()->writer();
$helper = new OutputHelper($writer);

$header = "{$this->name}, version {$this->version}";
$footer = 'Run `<command> --help` for specific help';

(new OutputHelper($writer))->showCommandsHelp($this->commands, $header, $footer);
if ($this->logo) {
$writer->write($this->logo, true);
}

$helper->showCommandsHelp($this->commands(), $header, $footer);

return ($this->onExit)();
}

/**
* Invoke command action.
*
* @param Command $command
*
* @return mixed
*/
protected function doAction(Command $command)
{
if (null === $action = $command->action()) {
return;
}

// Let the command collect more data (if mising or needs confirmation)
$command->interact($this->io());

$params = [];
$values = $command->values();
foreach ((new \ReflectionFunction($action))->getParameters() as $param) {

foreach ($this->getActionParameters($action) as $param) {
$params[] = $values[$param->getName()] ?? null;
}

return $action(...$params);
}

protected function getActionParameters(callable $action): array
{
$reflex = \is_array($action)
? (new \ReflectionClass($action[0]))->getMethod($action[1])
: new \ReflectionFunction($action);

return $reflex->getParameters();
}
}
7 changes: 7 additions & 0 deletions src/Helper/InflectsString.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
*/
trait InflectsString
{
/**
* Convert a string to camel case.
*
* @param string $string
*
* @return string
*/
public function toCamelCase(string $string): string
{
$words = \str_replace(['-', '_'], ' ', $string);
Expand Down
7 changes: 6 additions & 1 deletion src/Helper/OutputHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ protected function showHelp(string $for, array $items, int $space, string $heade
*/
protected function sortItems(array $items, &$max = 0): array
{
$max = 0;
$first = reset($items);
$max = \strlen($first->name());

\uasort($items, function ($a, $b) use (&$max) {
$max = \max(\strlen($a->name()), \strlen($b->name()), $max);
Expand All @@ -106,6 +107,10 @@ protected function sortItems(array $items, &$max = 0): array

/**
* Prepare name for different items.
*
* @param Parameter|Command $item
*
* @return string
*/
protected function getName($item): string
{
Expand Down
Loading