adhocore/cli
Framework agnostic Command Line Interface utilities and helpers for PHP. Build Console App with ease, fun and love.

- Command line application made easy
- Inspired by nodejs commander (thanks tj)
- Zero dependency.
- For PHP7, PHP8 and for good

What's included
Core: Argv parser · Cli application · Shell
IO: Colorizer · Cursor manipulator · Progress bar · Stream writer · Stream reader
Other: Autocompletion
Installation
composer require adhocore/cli:^v1.0.0
composer require adhocore/cli:^v0.9.0
Usage
Argv parser
$command = new Ahc\Cli\Input\Command('rmdir', 'Remove dirs');
$command
->version('0.0.1-dev')
->arguments('<dir> [dirs...]')
->option('-s --with-subdir', 'Also delete subdirs (`with` means false by default)')
->option('-e,--no-empty', 'Delete empty (`no` means true by default)')
->option('-d|--depth [nestlevel]', 'How deep to process subdirs', 'intval', 5)
->parse(['thisfile.php', '-sev', 'dir', 'dir1', 'dir2', '-vv'])
;
print_r($command->values());
print_r($command->values(false));
$command->dir;
$command->dirs;
$command->depth;
Command help
It can be triggered manually with $command->showHelp() or automatic when -h or --help option is passed to $command->parse().
For above example, the output would be:

Command version
It can be triggered manually with $command->showVersion() or automatic when -V or --version option is passed to $command->parse().
For above example, the output would be:
0.0.1-dev
Console app
Definitely check adhocore/phint - a real world console application made using adhocore/cli.
Here we simulate a git app with limited functionality of add, and checkout.
You will see how intuitive, fluent and cheese building a console app is!
Git app
$app = new Ahc\Cli\Application('git', '0.0.1');
$app
->command('add', 'Stage changed files', 'a')
->arguments('<path> [paths...]')
->option('-f --force', 'Force add ignored file', 'boolval', false)
->option('-N --intent-to-add', 'Add content later but index now', 'boolval', false)
->action(function ($path, $paths, $force, $intentToAdd) {
array_unshift($paths, $path);
echo ($intentToAdd ? 'Intent to add ' : 'Add ')
. implode(', ', $paths)
. ($force ? ' with force' : '');
})
->tap()
->command('checkout', 'Switch branches', 'co')
->arguments('<branch>')
->option('-b --new-branch', 'Create a new branch and switch to it', false)
->option('-f --force', 'Checkout even if index differs', 'boolval', false)
->action(function ($branch, $newBranch, $force) {
echo 'Checkout to '
. ($newBranch ? 'new ' . $branch : $branch)
. ($force ? ' with force' : '');
})
;
$app->parse(['git', 'add', 'path1', 'path2', 'path3', '-f']);
$app->handle(['git', 'add', 'path1', 'path2', 'path3', '-f']);
$app->handle(['git', 'co', '-b', 'master-2', '-f']);
Organized app
Instead of inline commands/actions, we define and add our own commands (having interact() and execute()) to the app:
class InitCommand extends Ahc\Cli\Input\Command
{
public function __construct()
{
parent::__construct('init', 'Init something');
$help = '<cyan>Custom help screen</end>';
$writer = new Ahc\Cli\Output\Writer();
$this
->argument('<arrg>', 'The Arrg')
->argument('[arg2]', 'The Arg2')
->option('-a --apple', 'The Apple')
->option('-b --ball', 'The ball')
->help($writer->colorizer()->colors($help))
->usage(
// append details or explanation of given example with ` ## ` so they will be uniformly aligned when shown
'<bold> init</end> <comment>--apple applet --ball ballon <arggg></end> ## details 1<eol/>' .
// $0 will be interpolated to actual command name
'<bold> $0</end> <comment>-a applet -b ballon <arggg> [arg2]</end> ## details 2<eol/>'
)
->logo('Ascii art logo of your command');
}
public function interact(Ahc\Cli\IO\Interactor $io) : void
{
if (!$this->apple) {
$this->set('apple', $io->prompt('Enter apple'));
}
if (!$this->ball) {
$this->set('ball', $io->prompt('Enter ball'));
}
}
public function execute($ball, $apple)
{
$io = $this->app()->io();
$io->write('Apple ' . $apple, true);
$io->write('Ball ' . $ball, true);
}
}
class OtherCommand extends Ahc\Cli\Input\Command
{
public function __construct()
{
parent::__construct('other', 'Other something');
}
public function execute()
{
$io = $this->app()->io();
$io->write('Other command');
}
}
$app = new Ahc\Cli\Application('App', 'v0.0.1');
$app->add(new InitCommand, 'i');
$app->add(new OtherCommand, 'o');
$app->logo('Ascii art logo of your app');
$app->help('Custom help screen (if omitted one will be generated)');
$app->handle($_SERVER['argv']);
Grouping commands
Grouped commands are listed together in commands list. Explicit grouping a command is optional.
By default if a command name has a colon : then the part before it is taken as a group,
else * is taken as a group.
Example: command name app:env has a default group app, command name appenv has group *.
$app->group('Configuration', function ($app) {
$app->add(new ConfigSetCommand);
$app->add(new ConfigListCommand);
});
$app->add((new ConfigSetCommand)->inGroup('Config'));
$app->add((new ConfigListCommand)->inGroup('Config'));
...
Default command
By default, running your CLI app without any arguments will show the help screen. However you can set the default action to run one of your commands either by setting the third parameter of the add function to true or by using the defaultCommand function.
$app->add(new InitCommand, 'i', true);
$app->command('init', 'Init something', 'i');
$app->defaultCommand('init');
$default_command = $app->getDefaultCommand();
Exception handler
Set a custom exception handler as callback. The callback receives exception & exit code. The callback may rethrow exception or may exit the program or just log exception and do nothing else.
$app = new Ahc\Cli\Application('App', 'v0.0.1');
$app->add(...);
$app->onException(function (Throwable $e, int $exitCode) {
exit($exitCode);
throw $e;
})->handle($argv);
App help
It can be triggered manually with $app->showHelp() or automatic when -h or --help option is passed to $app->parse().
Note If you pass something like ['app', cmd', '-h'] to $app->parse() it will automatically and instantly show you help of that cmd and not the $app.
For above example, the output would be:

App version
Same version number is passed to all attached Commands. So you can trigger version on any of the commands.
Shell
Very thin shell wrapper that provides convenience methods around proc_open().
Basic usage
$shell = new Ahc\Cli\Helper\Shell($command = 'php -v', $rawInput = null);
$shell->execute($async = false);
echo $shell->getOutput();
Advanced usage
$shell = new Ahc\Cli\Helper\Shell('php /some/long/running/scipt.php');
$shell->setOptions($workDir = '/home', $envVars = [])
->execute($async = true)
->isRunning();
$shell->stop();
$shell->kill();
Timeout
$shell = new Ahc\Cli\Helper\Shell('php /some/long/running/scipt.php');
$shell->setOptions($workDir, $envVars, $timeout = 10.5)->execute();
echo $shell->getOutput();
echo $shell->getErrorOutput();
Cli Interaction
You can perform user interaction like printing colored output, reading user input programatically and moving the cursors around with provided Ahc\Cli\IO\Interactor.
$interactor = new Ahc\Cli\IO\Interactor;
$interactor = new Ahc\Cli\IO\Interactor($inputPath, $outputPath);
Confirm
$confirm = $interactor->confirm('Are you happy?', 'n');
$confirm
? $interactor->greenBold('You are happy :)', true)
: $interactor->redBold('You are sad :(', true);
Single choice
$fruits = ['a' => 'apple', 'b' => 'banana'];
$choice = $interactor->choice('Select a fruit', $fruits, 'b');
$interactor->greenBold("You selected: {$fruits[$choice]}", true);
Multiple choices
$fruits = ['a' => 'apple', 'b' => 'banana', 'c' => 'cherry'];
$choices = $interactor->choices('Select fruit(s)', $fruits, ['b', 'c']);
$choices = \array_map(function ($c) use ($fruits) { return $fruits[$c]; }, $choices);
$interactor->greenBold('You selected: ' . implode(', ', $choices), true);
Prompt free input
$any = $interactor->prompt('Anything', rand(1, 100));
$interactor->greenBold("Anything is: $any", true);
Prompt with validation
$nameValidator = function ($value) {
if (\strlen($value) < 5) {
throw new \InvalidArgumentException('Name should be atleast 5 chars');
}
return $value;
};
$name = $interactor->prompt('Name', null, $nameValidator, 5);
$interactor->greenBold("The name is: $name", true);
Prompt hidden
On windows platform, it may change the fontface which can be fixed.
$passValidator = function ($pass) {
if (\strlen($pass) < 6) {
throw new \InvalidArgumentException('Password too short');
}
return $pass;
};
$pass = $interactor->promptHidden('Password', $passValidator, 2);

IO Components
The interactor is composed of Ahc\Cli\Input\Reader and Ahc\Cli\Output\Writer while the Writer itself is composed of Ahc\Cli\Output\Color. All these components can be used standalone.
Color
Color looks cool!
$color = new Ahc\Cli\Output\Color;
Simple usage
echo $color->warn('This is warning');
echo $color->info('This is info');
echo $color->error('This is error');
echo $color->comment('This is comment');
echo $color->ok('This is ok msg');
Custom style
Ahc\Cli\Output\Color::style('mystyle', [
'bg' => Ahc\Cli\Output\Color::CYAN,
'fg' => Ahc\Cli\Output\Color::fg256(57), // 256 colors can be used as well
'bold' => 1, // You can experiment with 0, 1, 2, 3 ... as well
]);
echo $color->mystyle('My text');
Built-in styles
There are a number of pre-defined built-in styles that allows you granular customization to different output conditions such as help and prompts:
- answer
- choice
- comment
- error
- help_category
- help_description_even
- help_description_odd
- help_example
- help_footer
- help_group
- help_header
- help_item_even
- help_item_odd
- help_summary
- help_text
- info
- logo
- ok
- question
- version
- warn
Overriding a built-in style works the same way as defining a new style:
Ahc\Cli\Output\Color::style('error', [
'fg' => Ahc\Cli\Output\Color::RED,
'bold' => 1,
]);
Disable colors
Ahc\Cli\Output\Color::$enabled = false;
Colors will be automatically disabled if the NO_COLOR environment variable is set to true.
Cursor
Move cursor around, erase line up or down, clear screen.
$cursor = new Ahc\Cli\Output\Cursor;
echo $cursor->up(1)
. $cursor->down(2)
. $cursor->right(3)
. $cursor->left(4)
. $cursor->next(0)
. $cursor->prev(2);
. $cursor->eraseLine()
. $cursor->clear()
. $cursor->clearUp()
. $cursor->clearDown()
. $cursor->moveTo(5, 8);
Progress Bar
Easily add a progress bar to your output:
$progress = new Ahc\Cli\Output\ProgressBar(100);
for ($i = 0; $i <= 100; $i++) {
$progress->current($i);
usleep(80000);
}
You can also manually advance the bar:
$progress = new Ahc\Cli\Output\ProgressBar(100);
$progress->advance();
$progress->advance(10);
$progress->advance(5, 'Still going.');
You can override the progress bar options to customize it to your liking:
$progress = new Ahc\Cli\Output\ProgressBar(100);
$progress->option('pointer', '>>');
$progress->option('loader', '▩');
$progress->option('pointer', '>>')->option('loader', '▩');
$progress->option([
'pointer' => '>>',
'loader' => '▩'
]);
+---------------+------------------------------------------------------+---------------+
| Option | Description | Default value |
+===============+======================================================+===============+
| pointer | The progress bar head symbol | > |
| loader | The loader symbol | = |
| color | The color of progress bar | white |
| labelColor | The text color of the label | white |
| labelPosition | The position of the label (top, bottom, left, right) | bottom |
+---------------+------------------------------------------------------+---------------+
Writer
Write anything in style.
$writer = new Ahc\Cli\Output\Writer;
$writer->errorBold('This is error');
Output formatting
You can call methods composed of any combinations:
'<colorName>', 'bold', 'bg', 'fg', 'warn', 'info', 'error', 'ok', 'comment'
... in any order (eg: bgRedFgBlaock, boldRed, greenBold, commentBgPurple and so on ...)
$writer->bold->green->write('It is bold green');
$writer->boldGreen('It is bold green');
$writer->comment('This is grayish comment', true);
$writer->bgPurpleBold('This is white on purple background');
Free style
Many colors with one single call: wrap text with tags <method> and </end>
For NL/EOL just use <eol> or </eol> or <eol/>.
Great for writing long colorful texts for example command usage info.
$writer->colors('<red>This is red</end><eol><bgGreen>This has bg Green</end>');
Raw output
$writer->raw('Enter name: ');
Tables
Just pass array of assoc arrays. The keys of first array will be taken as heading.
Heading is auto inflected to human readable capitalized words (ucwords).
$writer->table([
['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
]);
Gives something like:
+--------+------+------+
| A | B C | C D |
+--------+------+------+
| apple | ball | cat |
| applet | bee | cute |
+--------+------+------+
Designing table look and feel
Just pass 2nd param $styles:
$writer->table([
['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
], [
// for => styleName (anything that you would call in $writer instance)
'head' => 'boldGreen', // For the table heading
'odd' => 'bold', // For the odd rows (1st row is odd, then 3, 5 etc)
'even' => 'comment', // For the even rows (2nd row is even, then 4, 6 etc)
'1:1' => 'red', // For cell in row 1 col 1 (1 based count, 'apple' in this example)
'2:*' => '',
'*:2' => '',
'b-c' => '',
'*:*' => 'blue',
]);
You can define the style of a cell dynamically using a callback. You could then apply one style or another depending on a value.
$rows = [
['name' => 'John Doe', 'age' => '30'],
['name' => 'Jane Smith', 'age' => '25'],
['name' => 'Bob Johnson', 'age' => '40'],
];
$styles = [
'*:2' => function ($val, $row) {
return $row['age'] >= 30 ? 'boldRed' : '';
},
];
$writer->table($rows, $styles);
The example above only processes the cells in the second column of the table. If you want to process any cell, you can use the *:* key. You could then customise each cell in the table
$rows = [
['name' => 'John Doe', 'age' => '30'],
['name' => 'Jane Smith', 'age' => '25'],
['name' => 'Alice Bob', 'age' => '10'],
['name' => 'Big Johnson', 'age' => '40'],
['name' => 'Jane X', 'age' => '50'],
['name' => 'John Smith', 'age' => '20'],
['name' => 'Bob John', 'age' => '28'],
];
$styles = [
'*:*' => function ($val, $row) {
if ($val === 'Jane X') {
return 'yellow';
}
if ($val == 10 || $val == 20) {
return 'boldPurple';
}
if (str_contains($val, 'Bob')) {
return 'blue';
}
return $row['age'] >= 30 ? 'boldRed' : '';
},
];
$writer->table($rows, $styles);
Note: Priority in increasing order:
odd or even
2:* (row)
*:2 or b-c <-> column name (col)
*:* any cell in table
1:1 (cell) = highest priority
Justify content (Display setting)
If you want to display certain configurations (from your .env file for example) a bit like Laravel does (via the php artisan about command) you can use the justify method.
$writer->justify('Environment');
$writer->justify('PHP Version', PHP_VERSION);
$writer->justify('App Version', '1.0.0');
$writer->justify('Locale', 'en');
Gives something like:
Environment ........................................
PHP Version .................................. 8.1.4
App Version .................................. 1.0.0
Locale .......................................... en
You can use the sep parameter to define the separator to use.
$writer->justify('Environment', '', ['sep' => '-']);
$writer->justify('PHP Version', PHP_VERSION);
Gives something like:
Environment ----------------------------------------
PHP Version .................................. 8.1.4
In addition, the text color, the background color and the thickness of the two texts can be defined via the 3rd argument of this method.
$writer->justify('Cache Enable', 'true', [
'first' => ['fg' => Ahc\Cli\Output\Color::CYAN], // style of the key
'second' => ['fg' => Ahc\Cli\Output\Color::GREEN], // style of the value
]);
$writer->justify('Debug Mode', 'false', [
'first' => ['fg' => Ahc\Cli\Output\Color::CYAN], // style of the key
'second' => ['fg' => Ahc\Cli\Output\Color::RED], // style of the value
]);
For more details regarding the different color options, see Custom style
Reader
Read and pre process user input.
$reader = new Ahc\Cli\Input\Reader;
$reader->read(null, 'ucwords');
$reader->read('abc', 'trim');
$reader->read('', 'trim', 5);
$reader->readHidden($default, $callback);
$reader->readPiped();
$reader->readPiped(function ($reader) {
return $reader->read();
return $reader->readAll();
});
Exceptions
Whenever an exception is caught by Application::handle(), it will show a beautiful stack trace and exit with non 0 status code.

I18n Support
adhocore/cli also supports internationalisation. This is particularly useful if you are not very comfortable with English or if you are creating a framework or CLI application that could be used by people from a variety of backgrounds.
By default, all the texts generated by our system are in English. But you can easily modify them by defining your translations as follows
\Ahc\Application::addLocale('fr', [
'Only last argument can be variadic' => 'Seul le dernier argument peut être variadique',
], true);
You can also change the default English text to make the description more explicit if you wish.
\Ahc\Application::addLocale('en', [
'Show help' => 'Shows helpful information about a command',
]);
you can find all the translation keys supported by the package in this gist : https://gist.github.com/dimtrovich/1597c16d5c74334e68eef15a4e7ba3fd
Autocompletion
Any console applications that are built on top of adhocore/cli can entertain autocomplete of commands and options in zsh shell with oh-my-zsh.
All you have to do is add one line to the end of ~/.oh-my-zsh/custom/plugins/ahccli/ahccli.plugin.zsh:
compdef _ahccli <appname>
Example: compdef _ahccli phint for phint.
That is cumbersome to perform manually, here's a complete command you can copy/paste/run:
One time setup
mkdir -p ~/.oh-my-zsh/custom/plugins/ahccli && cd ~/.oh-my-zsh/custom/plugins/ahccli
[ -f ./ahccli.plugin.zsh ] || curl -sSLo ./ahccli.plugin.zsh https://raw.githubusercontent.com/adhocore/php-cli/master/ahccli.plugin.zsh
chmod 760 ./ahccli.plugin.zsh && cd -
Load ahccli plugin
This is also one time setup.
nano ~/.zshrc
plugins=(git ... ... ahccli)
Registering app
echo compdef _ahccli appname >> ~/.oh-my-zsh/custom/plugins/ahccli/ahccli.plugin.zsh
Of course you can add multiple apps, just change appname in above command
Then either restart the shell or source the plugin like so:
source ~/.oh-my-zsh/custom/plugins/ahccli/ahccli.plugin.zsh
Trigger autocomplete
appname <tab>
appname subcommand <tab>
Related
Contributors
License
© 2017-2020, Jitendra Adhikari | MIT
Credits
This project is release managed by please.