jbzoo/cli

The framework helps create CLI tools and provides new tools for symfony/console, symfony/process.

7.2.1 2024-03-28 20:21 UTC

README

CI Coverage Status Psalm Coverage Psalm Level CodeFactor
Stable Version Total Downloads Dependents GitHub License

Why?

The library greatly extends the functionality of CLI App and helps make creating new console utilities in PHP quicker and easier. Here's a summary of why this library is essential:

  • Enhanced Functionality

    • The library supercharges Symfony/Console, facilitating a more streamlined development of console utilities.
  • Progress Bar Improvements

    • Developers gain a refined progress bar suited for loop actions and enhanced with debugging information. This makes tracking task progress and diagnosing issues a breeze. See DemoProgressBar.php and see Live Demo.
    • $this->_($messages, $level, $context) as part of CliCommand instead of Symfony/Console $output->writeln().
    • cli($messages, $level, $context) as alias for different classes.
    • $this->progressBar(iterable|int $listOrMax, \Closure $callback, string $title = '') as part of CliCommand instead of Symfony/Console ProgressBar.
  • Strict Type Conversion

  • Styling and Output Customization

    • With built-in styles and color schemes, developers can make their console outputs more readable and visually appealing. See DemoStyles.php.
  • Message Aliases

    • The library introduces powerful aliases for message outputs, allowing for concise and consistent command calls. This is especially helpful in maintaining clean code.
  • Advanced Options

    • Features such as profiling for performance, timestamping, error muting, and specialized output modes (like cron and logstash modes) empower developers to refine their console outputs and diagnostics according to their specific needs.
    • Display timing and memory usage information with --profile option.
    • Show timestamp at the beginning of each message with --timestamp option.
    • Mute any sort of errors. So exit code will be always 0 (if it's possible) with --mute-errors.
    • None-zero exit code on any StdErr message with --non-zero-on-error option.
    • For any errors messages application will use StdOut instead of StdErr --stdout-only option (It's on your own risk!).
    • Disable progress bar animation for logs with --no-progress option.
  • Versatile Output Modes

    • The library provides different output formats catering to various use cases. Whether you're focusing on user-friendly text, logs, or integration with tools like ELK Stack, there's an output mode tailored for you.
    • --output-mode=text. By default, text output format. Userfriendly and easy to read.
    • --output-mode=cron. It's basically focused on logs output. It's combination of --timestamp --profile --stdout-only --no-progress -vv --no-ansi.
    • --output-mode=logstash. It's basically focused on Logstash format for ELK Stack. Also, it means --stdout-only --no-progress -vv.
  • Bonuses

    • There is a multiprocess mode (please don't confuse it with multithreading) to speed up work with a monotonous dataset.
    • Helper functions for user input in interactive mode.

Live Demo

Output regular messages

asciicast

Progress Bar Demo

asciicast

Quck Start - Build your first CLI App

Installing

composer require jbzoo/cli

The simplest CLI application has the following file structure. See the Demo App for more details.

File Structure

/path/to/app/
    my-app                      # Binrary file (See below)
    composer.json               # Composer file
    /Commands/                  # Commands directory
        Simple.php              # One of the commands (See below)
    /vendor/
        autoload.php            # Composer autoload

Composer file

./demo/composer.json

See Details
{
    "name"        : "vendor/my-app",
    "type"        : "project",
    "description" : "Example of CLI App based on JBZoo/CLI",
    "license"     : "MIT",
    "keywords"    : ["cli", "application", "example"],

    "require"     : {
        "php"       : ">=8.1",
        "jbzoo/cli" : "^7.1.0"
    },

    "autoload"    : {
        "psr-4" : {"DemoApp\\" : ""}
    },

    "bin"         : ["my-app"]
}

Binary file

Binary file: demo/my-app

See Details
#!/usr/bin/env php
<?php declare(strict_types=1);

namespace DemoApp;

use JBZoo\Cli\CliApplication;

// Init composer autoloader
require_once __DIR__ . '/vendor/autoload.php';

// Optional. Set your application name and version.
$application = new CliApplication('My Console Application', 'v1.0.0');

// Optional. Looks at the online generator of ASCII logos
// https://patorjk.com/software/taag/#p=testall&f=Epic&t=My%20Console%20App
$application->setLogo(
  <<<'EOF'
        __  __          _____                      _
       |  \/  |        / ____|                    | |          /\
       | \  / |_   _  | |     ___  _ __  ___  ___ | | ___     /  \   _ __  _ __
       | |\/| | | | | | |    / _ \| '_ \/ __|/ _ \| |/ _ \   / /\ \ | '_ \| '_ \
       | |  | | |_| | | |___| (_) | | | \__ \ (_) | |  __/  / ____ \| |_) | |_) |
       |_|  |_|\__, |  \_____\___/|_| |_|___/\___/|_|\___| /_/    \_\ .__/| .__/
                __/ |                                               | |   | |
               |___/                                                |_|   |_|
      EOF,
);

// Scan directory to find commands.
//  * It doesn't work recursively!
//  * They must be inherited from the class \JBZoo\Cli\CliCommand
$application->registerCommandsByPath(__DIR__ . '/Commands', __NAMESPACE__);

// Optional. Action name by default (if there is no arguments)
$application->setDefaultCommand('list');

// Run application
$application->run();

Simple CLI Action

The simplest CLI action: ./demo/Commands/DemoSimple.php

See Details
<?php declare(strict_types=1);

namespace DemoApp\Commands;

use JBZoo\Cli\CliCommand;
use JBZoo\Cli\Codes;

class Simple extends CliCommand
{
    protected function configure(): void
    {
        // Action name. It will be used in command line.
        // Example: `./my-app simple`
        $this->setName('simple');

        // Defined inhereted CLI options. See ./src/CliCommand.php for details.
        parent::configure();
    }

    protected function executeAction(): int
    {
        // Your code here
        $this->_('Hello world!');

        // Exit code. 0 - success, 1 - error.
        return self::SUCCESS;
    }
}

Built-in Functionality

Sanitize input variables

As live-demo take a look at demo application - ./demo/Commands/DemoOptionsStrictTypes.php.

Try to launch ./my-app options-strict-types.

// If the option has `InputOption::VALUE_NONE` it returns true/false.
// --option-name
$value = $this->getOpt('option-name'); // `$value === true` 

// --option-name="    123.6   "
$value = $this->getOpt('option-name'); // Returns the value AS-IS. `$value ===  "   123.6   "`

// --option-name="    123.6   "
$value = $this->getOptBool('option-name'); // Converts an input variable to boolean. `$value === true`

// --option-name="    123.6   "
$value = $this->getOptInt('option-name'); // Converts an input variable to integer. `$value === 123`
$value = $this->getOptInt('option-name', 42, [1, 2, 42]); // Strict comparing with allowed values

// --option-name="    123.6   "
$value = $this->getOptFloat('option-name'); // Converts an input variable to float. `$value === 123.6`
$value = $this->getOptFloat('option-name', 1.0, [1.0, 2.0, 3.0]); // Strict comparing with allowed values

// --option-name="    123.6   "
$value = $this->getOptString('option-name'); // Converts an input variable to trimmed string. `$value === "123.6"`
$value = $this->getOptString('option-name', 'default', ['default', 'mini', 'full']); // Strict comparing with allowed values

// --option-name=123.6
$value = $this->getOptArray('option-name'); // Converts an input variable to trimmed string. `$value === ["123.6"]`

// --option-name="15 July 2021 13:48:00"
$value = $this->getOptDatetime('option-name'); // Converts an input variable to \DateTimeImmutable object.

// Use standard input as input variable.
// Example. `echo " Qwerty 123 " | php ./my-app agruments`
$value = self::getStdIn(); // Reads StdIn as string value. `$value === " Qwerty 123 \n"`

Rendering text in different colors and styles

output-styles

There are list of predefined colors

<black>  Text in Black color  </black>
<red>    Text in Red Color    </red>
<green>  Text in Green Color  </green>
<yellow> Text in Yellow Color </yellow>
<blue>   Text in Blue Color   </blue>
<magenta>Text in Magenta Color</magenta>
<cyan>   Text in Cyan Color   </cyan>
<white>  Text in White Color  </white>

<!-- Usually default color is white. It depends on terminal settings. -->
<!-- You should use it only to overwrite nested tags. -->
<default>Text in Default Color</default>

There are list of predefined styles

<bl>Blinked Text</bl>
<b>Bold Text</b>
<u>Underlined Text</u>
<r>Reverse Color/Backgroud</r>
<bg>Change Background Only</bg>

Also, you can combine colors ans styles.

<magenta-bl>Blinked text in magenta color</magenta-bl>
<magenta-b>Bold text in magenta color</magenta-b>
<magenta-u>Underlined text in magenta color</magenta-u>
<magenta-r>Reverse text in magenta color</magenta-r>
<magenta-bg>Reverse only background of text in magenta color</magenta-bg>

And predefined shortcuts for standard styles of Symfony Console

<i> alias for <info>
<c> alias for <commnet>
<q> alias for <question>
<e> alias for <error>

Verbosity Levels

Console commands have different verbosity levels, which determine the messages displayed in their output.

As live-demo take a look at demo application - ./demo/Commands/ExamplesOutput.php. You can see Demo video.

Example of usage of verbosity levels

output-full-example

// There two strictly(!) recommended output ways:

/**
 * Prints a message to the output in the command class which inherits from the class \JBZoo\Cli\CliCommand
 * 
 * @param string|string[] $messages     Output message(s). Can be an array of strings or a string. Array of strings will be imploded with new line.
 * @param string          $verboseLevel is one of value form the class \JBZoo\Cli\OutLvl::*
 * @param string          $context      is array of extra info. Will be serialized to JSON and displayed in the end of the message.
 */
$this->_($messages, $verboseLevel, $context);

/**
 * This is global alias function of `$this->_(...)`.
 * It's nice to have it if you want to display a text from not CliCommand class.
 */
JBZoo\Cli\cli($messages, $verboseLevel, $context);
 
# Do not output any message
./my-app output -q
./my-app output --quiet

# Normal behavior, no option required. Only the most useful messages.
./my-app output 

# Increase verbosity of messages
./my-app output -v

# Display also the informative non essential messages
./my-app output -vv

# Display all messages (useful to debug errors)
./my-app output -vvv

Memory and time profiling

As live-demo take a look at demo application - ./demo/Commands/DemoProfile.php.

Try to launch ./my-app profile --profile.

profiling

Progress Bar

As live-demo take a look at demo application - ./demo/Commands/DemoProgressBar.php and Live Demo.

You can consider this as a substitute for the long cycles you want to profile.

Keep in mind that there is an additional overhead for memory and runtime to calculate all the extra debugging information in --verbose mode.

Simple example

progress-default-example

$this->progressBar(5, function (): void {
    // Some code in loop
});

Advanced usage

progress-full-example

$this->progressBar($arrayOfSomething, function ($value, $key, $step) {
    // Some code in loop

    if ($step === 3) {
        throw new ExceptionBreak("Something went wrong with \$value={$value}. Stop the loop!");
    }

    return "<c>Callback Args</c> \$value=<i>{$value}</i>, \$key=<i>{$key}</i>, \$step=<i>{$step}</i>";
}, 'Custom messages based on callback arguments', $throwBatchException);

Helper Functions

As live-demo take a look at demo application - ./demo/Commands/DemoHelpers.php.

Try to launch ./my-app helpers.

JBZoo/Cli uses Symfony Question Helper as base for aliases.

helpers

Regualar question

Ask any custom question and wait for a user's input. There is an option to set a default value.

$yourName = $this->ask("What's your name?", 'Default Noname');
$this->_("Your name is \"{$yourName}\"");

Ask user's password

Ask a question and hide the response. This is particularly convenient for passwords. There is an option to set a random value as default value.

$yourSecret = $this->askPassword("New password?", true);
$this->_("Your secret is \"{$yourSecret}\"");

Ask user to select the option

If you have a predefined set of answers the user can choose from, you could use a method askOption which makes sure that the user can only enter a valid string from a predefined list. There is an option to set a default option (index or string).

$selectedColor = $this->askOption("What's your favorite color?", ['Red', 'Blue', 'Yellow'], 'Blue');
$this->_("Selected color is {$selectedColor}");

Represent a yes/no question

Suppose you want to confirm an action before actually executing it. Add the following to your command.

$isConfirmed = $this->confirmation('Are you ready to execute the script?');
$this->_("Is confirmed: " . ($isConfirmed ? 'Yes' : 'No'));

Rendering key=>value list

If you need to show an aligned list, use the following code.

use JBZoo\Cli\CliRender;

$this->_(CliRender::list([
    "It's like a title",
    'Option Name' => 'Option Value',
    'Key' => 'Value',
    'Another Key #2' => 'Qwerty',
], '*')); // It's bullet character
 * It's like a title
 * Option Name   : Option Value
 * Key           : Value
 * Another Key #2: Qwerty

Easy logging

Simple log

./my-app output --timestamp >> /path/to/crontab/logs/$(date +%Y-%m-%d).log 2>&1

logs-simple

Crontab

Just add the --output-mode=cron flag and save the output to a file. Especially, this is very handy for saving logs for Crontab.

./my-app output --output-mode=cron >> /path/to/crontab/logs/$(date +%Y-%m-%d).log 2>&1

logs-cron

Elatcisearch / Logstash (ELK)

Just add the --output-mode=logstash flag and save the output to a file. Especially, this is very handy for saving logs for ELK Stack.

./my-app output --output-mode=logstash >> /path/to/logstash/logs/$(date +%Y-%m-%d).log 2>&1

logs-logstash-exception

Multi processing

There is a multiprocess mode (please don't confuse it with multithreading) to speed up work with a monotonous dataset. Basically, JBZoo\Cli will start a separate child process (not a thread!) for each dataset and wait for all of them to execute (like a Promise). This is how you get acceleration, which will depend on the power of your server and the data processing algorithm.

You will see a simple progress bar, but you won't be able to profile and log nicely, as it works for normal mode.

You can find examples here

Notes:

  • Pay attention on the method executeOneProcess() and getListOfChildIds() which are used to manage child processes. They are inherited from CliCommandMultiProc class.
  • Optimal number of child processes is Number of CPU cores - 1 . You can override this value by setting cli options. See them here ./src/CliCommandMultiProc.php.
  • Be really careful with concurrency. It's not easy to debug. Try to use -vvv option to see all errors and warnings.

Tips & Tricks

  • Use class \JBZoo\Cli\Codes to get all available exit codes.
  • You can add extra context to any message. It will be serialized to JSON and displayed in the end of the message. Just use CliHelper::getInstance()->appendExtraContext(['section' => ['var' => 'value']]);
  • You can define constant \JBZOO_CLI_TIMESTAMP_REAL=true to add timestamp_real as exta context. Sometimes it's useful for logstash if default value @timestamp doesn't work for you.

Contributing

# Fork the repo and build project
make update

# Make your local changes

# Run all tests and check code style
make test-all

# Create your pull request and check all tests on GithubActions page

Useful projects and links

License

MIT

See Also

  • CI-Report-Converter - Converting different error reports for deep compatibility with popular CI systems.
  • Composer-Diff - See what packages have changed after composer update.
  • Composer-Graph - Dependency graph visualization of composer.json based on mermaid-js.
  • Mermaid-PHP - Generate diagrams and flowcharts with the help of the mermaid script language.
  • Utils - Collection of useful PHP functions, mini-classes, and snippets for every day.
  • Image - Package provides object-oriented way to manipulate with images as simple as possible.
  • Data - Extended implementation of ArrayObject. Use files as config/array.
  • Retry - Tiny PHP library providing retry/backoff functionality with multiple backoff strategies and jitter support.
  • SimpleTypes - Converting any values and measures - money, weight, exchange rates, length, ...