Search by

Command-line plumbing for PHP apps: argument parsing with typed reads, aligned tables and help listings, returned as strings. Zero dependencies.

Package info

github.com/SugarCommerce/cli

Homepage

Issues

pkg:composer/sugarcommerce/cli

Statistics

Installs: 14

Dependents: 0

Suggesters: 0

Stars: 0

1.1.0 2026-10-23 15:26 UTC

This package is auto-updated.

Last update: 2026-10-23 15:26:54 UTC


README

Command-line plumbing for PHP apps: argument parsing with typed reads, aligned tables and help listings — returned as strings, never printed. Zero dependencies.

Why

Every app with a bin/ script grows the same three things: a loop that reads --name=value, a function that pads columns into a table, and a help screen. Each copy handles --name value slightly differently, and each pads with strlen(), so the first customer called Zoë pushes the table out of line.

getopt() is not the answer: it stops at the first positional word, so order:delete 42 --confirm never sees --confirm.

Arguments

use SugarCommerce\Cli\Args;

$args = Args::parse( array_slice( $argv, 1 ) );

$args->positional( 0 );            // 'order:delete'
$args->int( 'days', 7 );           // --days=30 or --days 30 → 30; --days=7d → 7 (the default)
$args->string( 'reason' );         // '' when absent or given bare
$args->flag( 'confirm' );          // --confirm, or --confirm=yes; not --confirm=no
$args->has( 'files' );             // given at all

--name=value and --name value are values; --name followed by another --option, or by nothing, is a flag. -- ends the options. A name given twice keeps the last value. to_array() gives the [ '_' => positionals, name => value|true ] shape hand-written parsers return, for code moving over from one.

Refuse an option you do not read rather than ignore it — ignored, --dryrun for --dry-run runs the thing for real:

if ( $typos = $args->unknown( array( 'dry-run', 'account' ) ) ) {
    fwrite( STDERR, 'Unknown option: --' . implode( ', --', $typos ) . "\n" . $usage );
    exit( 2 );
}

unknown() returns the names given that are not in the list, without --, in the order typed, matched exactly. It checks -- options only: a single-dash -h is a positional word, so a script that takes no positionals should refuse a non-empty positionals() too.

Tables

use SugarCommerce\Cli\Table;

echo Table::render( array( 'ID', 'Name' ), array( array( 7, 'Zoë Ångström' ), array( 12, '漢字テスト' ) ) );
+----+--------------+
| ID | Name         |
+----+--------------+
| 7  | Zoë Ångström |
| 12 | 漢字テスト   |
+----+--------------+

Widths are terminal columns (mb_strwidth), so accents and CJK line up. A line break, tab or escape sequence in a cell becomes a space — a value from the database cannot break the table or recolour the terminal — and bytes that are not UTF-8 become U+FFFD.

echo Table::listing( array(
    'order:delete <id>' => 'Delete one order (--confirm)',
    'lookups'           => 'Recent lookups [--limit=20]',
) );

The help screen: usage on the left, descriptions lined up on the right.

What it does not do

Print, read input, or exit. Everything is returned, so it is testable and the caller decides where it goes — STDOUT, STDERR, a log.

Testing

composer install
composer test

19 tests — parsing pinned to the hand-written parser it replaces across nine argument lines, typed reads and their defaults, --, unknown options named for refusal, column widths in three scripts, and cells that try to break the table or the terminal.

License

GPL-2.0-or-later