Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cli Parser Laravel Package

sebastian/cli-parser

sebastian/cli-parser is a lightweight PHP library for parsing $_SERVER['argv'], extracted from PHPUnit. It gives command-line tools a simple way to read and validate arguments and options without a full CLI framework.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (declare it, don't free-ride). PHPUnit already pulls this in transitively in most Laravel apps, so vendor/sebastian/cli-parser may already exist before you ever run:

composer require sebastian/cli-parser
# or, if only bin/ scripts and local tooling use it:
composer require --dev sebastian/cli-parser

Do not rely on the transitive copy — PHPUnit's constraint can change under you on a composer update.

Where to look first. The README is badges only; there is no usage documentation. Your real documentation is:

  1. vendor/sebastian/cli-parser/src/Parser.php — one small file, the entire public surface (Parser::parse()).
  2. vendor/sebastian/cli-parser/src/Exceptions/ — UnknownOptionException, AmbiguousOptionException, RequiredOptionArgumentMissingException, OptionDoesNotAcceptArgumentException, all under the SebastianBergmann\CliParser\Exception hierarchy.
  3. phpunit/phpunit's src/TextUI/Application.php — the battle-tested, real-world call site. If you are unsure how a spec string behaves, copy PHPUnit's.

First use case — a bin/ script that must not boot Laravel. The whole point of this package in a Laravel codebase is startup cost: it lets you interpret arguments before (or entirely without) the framework, the container, or Symfony Console.

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

require __DIR__ . '/../vendor/autoload.php';

use SebastianBergmann\CliParser\Exception;
use SebastianBergmann\CliParser\Parser;

try {
    $options = (new Parser)->parse(
        $_SERVER['argv'],
        [
            'env=',    // --env=staging  (trailing '=' => requires a value)
            'force',   // --force        (no '=' => boolean flag)
            'dry-run',
            'path=',
        ],
        'f'            // short options string; '-' prefixed results come back as '-f'
    );
} catch (Exception $e) {
    fwrite(STDERR, $e->getMessage() . PHP_EOL);
    exit(1);
}

// [['--env', 'staging'], ['--force', null], ['--path', '/tmp']]
$opts = collect($options)->mapWithKeys(fn (array $o) => [ltrim($o[0], '-'), $o[1]]);

$env    = $opts->get('env', 'production');
$dryRun = $opts->has('force') ? false : (bool) $opts->has('dry-run');

That is the entire mental model: a spec of long options plus a short-option string in, a list of [option-with-dashes, value-or-null] tuples out.

Sanity-check the spec syntax once, in your own codebase. The =/: suffix conventions are the whole contract and they are not documented in the README. Write one throwaway test per option spec (--flag, --opt=value, --opt value, -f, bundled shorts) and assert the tuples. Five minutes here prevents a silent mis-parse in a deploy script.

Implementation Patterns

1. Isolate it behind a small adapter, not sprinkled through your code

Parser is final and has no interface, so mocking it is impossible. Wrap it once:

namespace App\Support\Cli;

use SebastianBergmann\CliParser\Parser;

final class RawArgs
{
    /** @param list<string> $argv */
    public static function parse(array $argv, array $long, string $short = ''): array
    {
        return (new Parser)->parse($argv, $long, $short);
    }

    /** @param list<array{0: string, 1: string|null}> $tuples */
    public static function toMap(array $tuples): array
    {
        $map = [];
        foreach ($tuples as [$name, $value]) {
            $map[ltrim($name, '-')] = $value ?? true;
        }
        return $map;
    }
}

Bind it in a service provider and your application code only ever deals with array{name: string|true} — swappable later for Symfony's ArgvInput if the tool grows up.

2. Pre-bootstrap flag sniffing (the strongest Laravel use case)

Deciding which environment or config to load happens before the kernel exists, so Artisan and Symfony Console are not available yet. Put this in a bootstrap file or a bin/ entrypoint:

// bootstrap/cli-context.php
$flags = RawArgs::toMap(RawArgs::parse($_SERVER['argv'], ['env=', 'no-cache']));

$envFile = match ($flags['env'] ?? null) {
    'staging' => '.env.staging',
    'local'   => '.env.local',
    default   => '.env',
};

Dotenv::createImmutable(base_path(), $envFile)->safeLoad();

Keep the option list tiny and stable — you are now maintaining an argument grammar outside the framework's validation and --help.

3. Artisan command with a passthrough segment

Useful when your command is a thin wrapper that forwards flags to another binary (PHPUnit, Pest, a node script). Symfony Console owns the outer grammar; cli-parser owns the tail after --:

public function handle(): int
{
    $argv = $_SERVER['argv'];
    $sep  = array_search('--', $argv, true);
    $tail = $sep === false ? [] : array_slice($argv, $sep);   // keep '--' as argv[0]

    $forwarded = RawArgs::parse($tail, ['filter=', 'testsuite=', 'stop-on-failure'], 'd');

    $this->line('Forwarding: ' . json_encode($forwarded));

    // exec/shell_exec the underlying binary with the reconstructed argv...
    return self::SUCCESS;
}

Draw the boundary explicitly and document it. Two parsers in one command is a debugging hazard the moment someone adds --filter to the Artisan signature as well.

4. Fixture-driven testing

Never read $_SERVER['argv'] in tests. Feed fabricated argv:

it('parses the deploy spec', function () {
    $tuples = RawArgs::parse(
        ['deploy', '--env=staging', '--force', '--path', '/tmp'],
        ['env=', 'force', 'path=']
    );

    expect(RawArgs::toMap($tuples))->toBe(['env' => 'staging', 'force' => true, 'path' => '/tmp']);
});

This is the cheapest way to pin down the spec syntax, and it doubles as the documentation the upstream README does not provide.

5. Error handling mapped to Laravel exit codes

try {
    $opts = RawArgs::toMap(RawArgs::parse($argv, $long, $short));
} catch (\SebastianBergmann\CliParser\UnknownOptionException $e) {
    $this->error($e->getMessage());   // "Unknown option --envr" + a "Did you mean?" hint
    return self::FAILURE;
} catch (\SebastianBergmann\CliParser\Exception $e) {
    $this->error($e->getMessage());
    return self::INVALID;
}

Since 4.1/4.2 the exceptions suggest the most similar known option, which is genuinely good UX for internal tooling — surface the message verbatim rather than replacing it.

6. Dependency placement

  • bin/ scripts shipped with the app or a package → require.
  • Scripts used only by developers/CI (seeding, linting, one-off backfills) → require-dev.
  • Inside application classes → generally a smell; you are re-implementing Artisan.

Gotchas and Tips

  • Do not use this inside Artisan commands for normal input. You lose --help generation, InputOption validation, prompts, autocompletion, and $this->option() ergonomics. If you catch yourself writing a spec array inside a Command, stop and use the signature string.
  • The spec syntax is undocumented and unforgiving. A long option without the trailing = silently becomes a boolean flag; a short option without : silently refuses a value (you'll get OptionDoesNotAcceptArgumentException). There is no config file, no schema, no validation of the spec itself.
  • Verify the spec convention against src/Parser.php for your installed version before you build on it. The long-option = suffix and the short-option string are the de facto contract used by PHPUnit, but the README never states it.
  • PHP version floors bite. 4.0 dropped PHP 8.2 (so 4.x needs 8.3+), 5.0 dropped PHP 8.3 (so 5.x needs 8.4+). A Laravel 10/11 app on PHP 8.2 must stay on 3.x. If you support a range of PHP versions across projects, pin the parser accordingly in composer.json rather than letting the PHPUnit constraint float.
  • Catch SebastianBergmann\CliParser\Exception, not Symfony\Component\Console\Exception. They are unrelated hierarchies; an uncaught parser exception in a bin/ script produces a fatal stack trace instead of a clean error line.
  • Results carry their dashes. $option[0] is --env or -f, not env. Normalize once (see the adapter) and never ltrim at call sites.
  • Positional arguments are not collected for you. The parser returns options only; bare words (deploy staging) must be picked out of $_SERVER['argv'] yourself. If your tool needs real positionals, that is a strong signal to use Symfony Console.
  • Pass the raw $_SERVER['argv']. The parser skips argv[0] itself. Slicing first shifts everything by one and produces mystifying UnknownOptionExceptions on your script name.
  • Guard on SAPI. Under php-fpm/Octane $_SERVER['argv'] may be absent or meaningless. if (PHP_SAPI !== 'cli') { ... } before touching it, or scope the code to CLI-only entrypoints.
  • Everything is a string or null. No integer casting, no arrays (--tag=a --tag=b keeps only the last), no --opt=value escaping beyond what the shell already did. Cast at the boundary and document that repeated options are unsupported.
  • Spaces in values are a shell problem, not a parser problem. --path=/tmp/My Dir breaks before PHP sees it; quote it or use --path "/tmp/My Dir".
  • No extension points. Final class, no events, no middleware, no plugins. The only seam you get is the one you build (the adapter above).
  • Debugging recipe: when parsing misbehaves, var_dump($_SERVER['argv']) and the raw tuple list before mapping. Nine times out of ten the spec array is wrong, not the input.
  • --help is yours to write. There is no help generation and no usage text; hand-write it next to the spec so the two cannot drift, and consider a single constant holding both.
  • Prefer it over getopt() for new internal scripts — the exception messages are better, it handles --opt value and --opt=value, and it is maintained. But prefer Symfony Console over both the moment the tool leaves your team.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode