master f66c1a90f802 cached
27 files
93.2 KB
21.8k tokens
143 symbols
1 requests
Download .txt
Repository: stecman/symfony-console-completion
Branch: master
Commit: f66c1a90f802
Files: 27
Total size: 93.2 KB

Directory structure:
gitextract_ezfj5dnr/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── phpunit.yml
├── .gitignore
├── LICENCE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src/
│   ├── Completion/
│   │   ├── CompletionAwareInterface.php
│   │   ├── CompletionInterface.php
│   │   └── ShellPathCompletion.php
│   ├── Completion.php
│   ├── CompletionCommand.php
│   ├── CompletionContext.php
│   ├── CompletionHandler.php
│   ├── EnvironmentCompletionContext.php
│   └── HookFactory.php
└── tests/
    ├── Stecman/
    │   └── Component/
    │       └── Symfony/
    │           └── Console/
    │               └── BashCompletion/
    │                   ├── Common/
    │                   │   └── CompletionHandlerTestCase.php
    │                   ├── CompletionCommandTest.php
    │                   ├── CompletionContextTest.php
    │                   ├── CompletionHandlerTest.php
    │                   ├── CompletionTest.php
    │                   ├── Fixtures/
    │                   │   ├── CompletionAwareCommand.php
    │                   │   ├── HiddenCommand.php
    │                   │   ├── TestBasicCommand.php
    │                   │   └── TestSymfonyStyleCommand.php
    │                   └── HookFactoryTest.php
    └── bootstrap.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "composer"
    directory: "/"
    schedule:
      interval: "weekly"
    versioning-strategy: widen
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"


================================================
FILE: .github/workflows/phpunit.yml
================================================
name: PHPUnit

on: pull_request

permissions:
  contents: read

concurrency:
  group: phpunit-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  phpunit:
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        php-versions: ['8.1', '8.2', '8.3', '8.4', '8.5']

    name: PHP ${{ matrix.php-versions }}

    steps:
      - name: Checkout
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Set up php ${{ matrix.php-versions }}
        uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1
        with:
          php-version: ${{ matrix.php-versions }}
          coverage: none
          ini-file: development
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up dependencies
        run: composer i

      - name: PHPUnit with ZSH
        run: SHELL=zsh vendor/bin/phpunit

      - name: PHPUnit with BASH
        run: SHELL=bash vendor/bin/phpunit

  summary:
    permissions:
      contents: none
    runs-on: ubuntu-latest
    needs: [phpunit]

    if: always()

    name: phpunit-summary

    steps:
      - name: Summary status
        run: if ${{ needs.phpunit.result != 'success' }}; then exit 1; fi


================================================
FILE: .gitignore
================================================
vendor
.idea
/build/
phpunit.xml
/composer.lock
/.phpunit.result.cache


================================================
FILE: LICENCE
================================================
The MIT License (MIT)

Copyright (c) 2014 Stephen Holdaway

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# BASH/ZSH auto-complete for Symfony Console applications

[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/stecman/symfony-console-completion/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/stecman/symfony-console-completion/?branch=master)

[![Latest Stable Version](https://poser.pugx.org/stecman/symfony-console-completion/v)](https://packagist.org/packages/stecman/symfony-console-completion)
[![Total Downloads](https://poser.pugx.org/stecman/symfony-console-completion/downloads)](https://packagist.org/packages/stecman/symfony-console-completion)
[![License](https://poser.pugx.org/stecman/symfony-console-completion/license)](https://packagist.org/packages/stecman/symfony-console-completion)
[![PHP Version Require](https://poser.pugx.org/stecman/symfony-console-completion/require/php)](https://packagist.org/packages/stecman/symfony-console-completion)

This package provides automatic (tab) completion in BASH and ZSH for Symfony Console Component based applications. With zero configuration, this package allows completion of available command names and the options they provide. User code can define custom completion behaviour for argument and option values.

Example of zero-config use with Composer:

![Composer BASH completion](https://i.imgur.com/MoDWkby.gif)

## Zero-config use

If you don't need any custom completion behaviour, you can simply add the completion command to your application:

1. Install `stecman/symfony-console-completion` using [composer](https://getcomposer.org/) by running:
   ```
   $ composer require stecman/symfony-console-completion
   ```

2. For standalone Symfony Console applications, add an instance of `CompletionCommand` to your application's `Application::getDefaultCommands()` method:

   ```php
   protected function getDefaultCommands()
   {
      //...
       $commands[] = new \Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand();
      //...
   }
   ```

   For Symfony Framework applications, register the `CompletionCommand` as a service in `app/config/services.yml`:

   ```yaml
   services:
   #...
       console.completion_command:
         class: Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand
         tags:
             -  { name: console.command }
   #...
   ```

3. Register completion for your application by running one of the following in a terminal, replacing `[program]` with the command you use to run your application (eg. 'composer'):

   ```bash
   # BASH ~4.x, ZSH
   source <([program] _completion --generate-hook)

   # BASH ~3.x, ZSH
   [program] _completion --generate-hook | source /dev/stdin

   # BASH (any version)
   eval $([program] _completion --generate-hook)
   ```

   By default this registers completion for the absolute path to you application, which will work if the program is accessible on your PATH. You can specify a program name to complete for instead using the `--program` option, which is required if you're using an alias to run the program.

4. If you want the completion to apply automatically for all new shell sessions, add the command from step 3 to your shell's profile (eg. `~/.bash_profile` or `~/.zshrc`)

Note: The type of shell (ZSH/BASH) is automatically detected using the `SHELL` environment variable at run time. In some circumstances, you may need to explicitly specify the shell type with the `--shell-type` option.

The current version supports Symfony 6 and PHP 8.x only, due to backwards compatibility breaks in Symfony 6. For older versions of Symfony and PHP, use [version 0.11.0](https://github.com/stecman/symfony-console-completion/releases/tag/0.11.0).


## How it works

The `--generate-hook` option of `CompletionCommand` generates a small shell script that registers a function with your shell's completion system to act as a bridge between the shell and the completion command in your application. When you request completion for your program (by pressing tab with your program name as the first word on the command line), the bridge function is run; passing the current command line contents and cursor position to `[program] _completion`, and feeding the resulting output back to the shell.


## Defining value completions

By default, no completion results will be returned for option and argument values. There are two ways of defining custom completion values for values: extend `CompletionCommand`, or implement `CompletionAwareInterface`.

### Implementing `CompletionAwareInterface`

`CompletionAwareInterface` allows a command to be responsible for completing its own option and argument values. When completion is run with a command name specified (eg. `myapp mycommand ...`) and the named command implements this interface, the appropriate interface method is called automatically:

```php
class MyCommand extends Command implements CompletionAwareInterface
{
    ...

    public function completeOptionValues($optionName, CompletionContext $context)
    {
        if ($optionName == 'some-option') {
            return ['myvalue', 'other-value', 'word'];
        }
    }

    public function completeArgumentValues($argumentName, CompletionContext $context)
    {
        if ($argumentName == 'package') {
            return $this->getPackageNamesFromDatabase($context->getCurrentWord());
        }
    }
}
```

This method of generating completions doesn't support use of `CompletionInterface` implementations at the moment, which make it easy to share completion behaviour between commands. To use this functionality, you'll need write your value completions by extending `CompletionCommand`.


### Extending `CompletionCommand`

Argument and option value completions can also be defined by extending `CompletionCommand` and overriding the `configureCompletion` method:

```php
class MyCompletionCommand extends CompletionCommand
{
    protected function configureCompletion(CompletionHandler $handler)
    {
        $handler->addHandlers([
            // Instances of Completion go here.
            // See below for examples.
        ]);
    }
}
```

#### The `Completion` class

The following snippets demonstrate how the `Completion` class works with `CompletionHandler`, and some possible configurations. The examples are for an application with the signature:

    `myapp (walk|run) [-w|--weather=""] direction`


##### Command-specific argument completion with an array

```php
$handler->addHandler(
    new Completion(
        'walk',                    // match command name
        'direction',               // match argument/option name
        Completion::TYPE_ARGUMENT, // match definition type (option/argument)
        [                     // array or callback for results
            'north',
            'east',
            'south',
            'west'
        ]
    )
);
```

This will complete the `direction` argument for this:

```bash
$ myapp walk [tab]
```

but not this:

```bash
$ myapp run [tab]
```

##### Non-command-specific (global) argument completion with a function

```php
$handler->addHandler(
    new Completion(
        Completion::ALL_COMMANDS,
        'direction',
        Completion::TYPE_ARGUMENT,
        function() {
            return range(1, 10);
        }
    )
);
```

This will complete the `direction` argument for both commands:

```bash
$ myapp walk [tab]
$ myapp run [tab]
```

##### Option completion

Option handlers work the same way as argument handlers, except you use `Completion::TYPE_OPTION` for the type.

```php
$handler->addHandler(
    new Completion(
        Completion::ALL_COMMANDS,
        'weather',
        Completion::TYPE_OPTION,
        [
            'raining',
            'sunny',
            'everything is on fire!'
        ]
    )
);
```

##### Completing the for both arguments and options

To have a completion run for both options and arguments matching the specified name, you can use the type `Completion::ALL_TYPES`. Combining this with `Completion::ALL_COMMANDS` and consistent option/argument naming throughout your application, it's easy to share completion behaviour between commands, options and arguments:

```php
$handler->addHandler(
    new Completion(
        Completion::ALL_COMMANDS,
        'package',
        Completion::ALL_TYPES,
        function() {
            // ...
        }
    )
);
```

## Example completions

### Completing references from a Git repository

```php
new Completion(
    Completion::ALL_COMMANDS,
    'ref',
    Completion::TYPE_OPTION,
    function () {
        $raw = shell_exec('git show-ref --abbr');
        if (preg_match_all('/refs\/(?:heads|tags)?\/?(.*)/', $raw, $matches)) {
            return $matches[1];
        }
    }
)
```

### Completing filesystem paths

This library provides the completion implementation `ShellPathCompletion` which defers path completion to the shell's built-in path completion behaviour rather than implementing it in PHP, so that users get the path completion behaviour they expect from their shell.

```php
new Completion\ShellPathCompletion(
    Completion::ALL_COMMANDS,
    'path',
    Completion::TYPE_OPTION
)

```

## Behaviour notes

* Option shortcuts are not offered as completion options, however requesting completion (ie. pressing tab) on a valid option shortcut will complete.
* Completion is not implemented for the `--option="value"` style of passing a value to an option, however `--option value` and `--option "value"` work and are functionally identical.
* Value completion is always run for options marked as `InputOption::VALUE_OPTIONAL` since there is currently no way to determine the desired behaviour from the command line contents (ie. skip the optional value or complete for it)


================================================
FILE: composer.json
================================================
{
    "name": "stecman/symfony-console-completion",
    "description": "Automatic BASH completion for Symfony Console Component based applications.",
    "license": "MIT",
    "authors": [
        {
            "name": "Stephen Holdaway",
            "email": "stephen@stecman.co.nz"
        }
    ],
    "require": {
        "php": ">=8.1",
        "symfony/console": "~6.4 || ^7.1 || ^8.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5",
        "dms/phpunit-arraysubset-asserts": "^0.4.0"
    },
    "autoload": {
        "psr-4": {
            "Stecman\\Component\\Symfony\\Console\\BashCompletion\\": "src/"
        }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "0.14.x-dev"
        }
    }
}


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
         backupGlobals="false"
         backupStaticAttributes="false"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         bootstrap="tests/bootstrap.php"
>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </coverage>
    <testsuites>
        <testsuite name="Symfony console completion">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>


================================================
FILE: src/Completion/CompletionAwareInterface.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;

use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;

interface CompletionAwareInterface
{

    /**
     * Return possible values for the named option
     *
     * @param string $optionName
     * @param CompletionContext $context
     * @return array
     */
    public function completeOptionValues($optionName, CompletionContext $context);

    /**
     * Return possible values for the named argument
     *
     * @param string $argumentName
     * @param CompletionContext $context
     * @return array
     */
    public function completeArgumentValues($argumentName, CompletionContext $context);
}


================================================
FILE: src/Completion/CompletionInterface.php
================================================
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;

interface CompletionInterface
{
    // Sugar for indicating that a Completion should run for all command names and for all types
    // Intended to avoid meaningless null parameters in the constructors of implementing classes
    const ALL_COMMANDS = null;
    const ALL_TYPES = null;

    const TYPE_OPTION = 'option';
    const TYPE_ARGUMENT = 'argument';

    /**
     * Return the type of input (option/argument) completion should be run for
     *
     * @see \Symfony\Component\Console\Command\Command::addArgument
     * @see \Symfony\Component\Console\Command\Command::addOption
     * @return string - one of the CompletionInterface::TYPE_* constants
     */
    public function getType();

    /**
     * Return the name of the command completion should be run for
     * If the return value is CompletionInterface::ALL_COMMANDS, the completion will be run for any command name
     *
     * @see \Symfony\Component\Console\Command\Command::setName
     * @return string|null
     */
    public function getCommandName();

    /**
     * Return the option/argument name the completion should be run for
     * CompletionInterface::getType determines whether the target name refers to an option or an argument
     *
     * @return string
     */
    public function getTargetName();

    /**
     * Execute the completion
     *
     * @return string[] - an array of possible completion values
     */
    public function run();
}


================================================
FILE: src/Completion/ShellPathCompletion.php
================================================
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;

/**
 * Shell Path Completion
 *
 * Defers completion to the calling shell's built-in path completion functionality.
 */
class ShellPathCompletion implements CompletionInterface
{
    /**
     * Exit code set up to trigger path completion in the completion hooks
     * @see Stecman\Component\Symfony\Console\BashCompletion\HookFactory
     */
    const PATH_COMPLETION_EXIT_CODE = 200;

    protected $type;

    protected $commandName;

    protected $targetName;

    public function __construct($commandName, $targetName, $type)
    {
        $this->commandName = $commandName;
        $this->targetName = $targetName;
        $this->type = $type;
    }

    /**
     * @inheritdoc
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @inheritdoc
     */
    public function getCommandName()
    {
        return $this->commandName;
    }

    /**
     * @inheritdoc
     */
    public function getTargetName()
    {
        return $this->targetName;
    }

    /**
     * Exit with a status code configured to defer completion to the shell
     *
     * @see \Stecman\Component\Symfony\Console\BashCompletion\HookFactory::$hooks
     */
    public function run()
    {
        exit(self::PATH_COMPLETION_EXIT_CODE);
    }
}


================================================
FILE: src/Completion.php
================================================
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion;

use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;

class Completion implements CompletionInterface
{
    /**
     * The type of input (option/argument) the completion should be run for
     *
     * @see CompletionInterface::ALL_TYPES
     * @var string
     */
    protected $type;

    /**
     * The command name the completion should be run for
     *
     * @see CompletionInterface::ALL_COMMANDS
     * @var string|null
     */
    protected $commandName;

    /**
     * The option/argument name the completion should be run for
     *
     * @var string
     */
    protected $targetName;

    /**
     * Array of values to return, or a callback to generate completion results with
     * The callback can be in any form accepted by call_user_func.
     *
     * @var callable|array
     */
    protected $completion;

    /**
     * Create a Completion with the command name set to CompletionInterface::ALL_COMMANDS
     *
     * @deprecated - This will be removed in 1.0.0 as it is redundant and isn't any more concise than what it implements.
     *
     * @param string $targetName
     * @param string $type
     * @param array|callable $completion
     * @return Completion
     */
    public static function makeGlobalHandler($targetName, $type, $completion)
    {
        return new Completion(CompletionInterface::ALL_COMMANDS, $targetName, $type, $completion);
    }

    /**
     * @param string $commandName
     * @param string $targetName
     * @param string $type
     * @param array|callable $completion
     */
    public function __construct($commandName, $targetName, $type, $completion)
    {
        $this->commandName = $commandName;
        $this->targetName = $targetName;
        $this->type = $type;
        $this->completion = $completion;
    }

    /**
     * Return the stored completion, or the results returned from the completion callback
     *
     * @return array
     */
    public function run()
    {
        if ($this->isCallable()) {
            return call_user_func($this->completion);
        }

        return $this->completion;
    }

    /**
     * Get type of input (option/argument) the completion should be run for
     *
     * @see CompletionInterface::ALL_TYPES
     * @return string|null
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Set type of input (option/argument) the completion should be run for
     *
     * @see CompletionInterface::ALL_TYPES
     * @param string|null $type
     */
    public function setType($type)
    {
        $this->type = $type;
    }

    /**
     * Get the command name the completion should be run for
     *
     * @see CompletionInterface::ALL_COMMANDS
     * @return string|null
     */
    public function getCommandName()
    {
        return $this->commandName;
    }

    /**
     * Set the command name the completion should be run for
     *
     * @see CompletionInterface::ALL_COMMANDS
     * @param string|null $commandName
     */
    public function setCommandName($commandName)
    {
        $this->commandName = $commandName;
    }

    /**
     * Set the option/argument name the completion should be run for
     *
     * @see setType()
     * @return string
     */
    public function getTargetName()
    {
        return $this->targetName;
    }

    /**
     * Get the option/argument name the completion should be run for
     *
     * @see getType()
     * @param string $targetName
     */
    public function setTargetName($targetName)
    {
        $this->targetName = $targetName;
    }

    /**
     * Return the array or callback configured for for the Completion
     *
     * @return array|callable
     */
    public function getCompletion()
    {
        return $this->completion;
    }

    /**
     * Set the array or callback to return/run when Completion is run
     *
     * @see run()
     * @param array|callable $completion
     */
    public function setCompletion($completion)
    {
        $this->completion = $completion;
    }

    /**
     * Check if the configured completion value is a callback function
     *
     * @return bool
     */
    public function isCallable()
    {
        return is_callable($this->completion);
    }
}


================================================
FILE: src/CompletionCommand.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion;

use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class CompletionCommand extends SymfonyCommand
{
    /**
     * @var CompletionHandler
     */
    protected $handler;

    protected function configure(): void
    {
        $this
            ->setName('_completion')
            ->setDefinition($this->createDefinition())
            ->setDescription('BASH completion hook.')
            ->setHelp(<<<END
To enable BASH completion, run:

    <comment>eval `[program] _completion -g`</comment>.

Or for an alias:

    <comment>eval `[program] _completion -g -p [alias]`</comment>.

END
            );

        // Hide this command from listing if supported
        $this->setHidden(true);
    }

    /**
     * {@inheritdoc}
     */
    public function getNativeDefinition(): InputDefinition
    {
        return $this->createDefinition();
    }

    /**
     * Ignore user-defined global options
     *
     * Any global options defined by user-code are meaningless to this command.
     * Options outside of the core defaults are ignored to avoid name and shortcut conflicts.
     */
    public function mergeApplicationDefinition(bool $mergeArgs = true): void
    {
        // Get current application options
        $appDefinition = $this->getApplication()->getDefinition();
        $originalOptions = $appDefinition->getOptions();

        // Temporarily replace application options with a filtered list
        $appDefinition->setOptions(
            $this->filterApplicationOptions($originalOptions)
        );

        parent::mergeApplicationDefinition($mergeArgs);

        // Restore original application options
        $appDefinition->setOptions($originalOptions);
    }

    /**
     * Reduce the passed list of options to the core defaults (if they exist)
     *
     * @param InputOption[] $appOptions
     * @return InputOption[]
     */
    protected function filterApplicationOptions(array $appOptions)
    {
        return array_filter($appOptions, function(InputOption $option) {
            static $coreOptions = array(
                'help' => true,
                'quiet' => true,
                'verbose' => true,
                'version' => true,
                'ansi' => true,
                'no-ansi' => true,
                'no-interaction' => true,
            );

            return isset($coreOptions[$option->getName()]);
        });
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->handler = new CompletionHandler($this->getApplication());
        $handler = $this->handler;

        if ($input->getOption('generate-hook')) {
            global $argv;
            $program = $argv[0];

            $factory = new HookFactory();
            $alias = $input->getOption('program');
            $multiple = (bool)$input->getOption('multiple');

            if (!$alias) {
                $alias = basename($program);
            }

            $hook = $factory->generateHook(
                $input->getOption('shell-type') ?: $this->getShellType(),
                $program,
                $alias,
                $multiple
            );

            $output->write($hook, true);
        } else {
            $handler->setContext(new EnvironmentCompletionContext());

            // Get completion results
            $results = $this->runCompletion();

            // Escape results for the current shell
            $shellType = $input->getOption('shell-type') ?: $this->getShellType();

            foreach ($results as &$result) {
                $result = $this->escapeForShell($result, $shellType);
            }

            $output->write($results, true);
        }

        return SymfonyCommand::SUCCESS;
    }

    /**
     * Escape each completion result for the specified shell
     *
     * @param string $result - Completion results that should appear in the shell
     * @param string $shellType - Valid shell type from HookFactory
     * @return string
     */
    protected function escapeForShell($result, $shellType)
    {
        switch ($shellType) {
            // BASH requires special escaping for multi-word and special character results
            // This emulates registering completion with`-o filenames`, without side-effects like dir name slashes
            case 'bash':
                $context = $this->handler->getContext();
                $wordStart = substr($context->getRawCurrentWord(), 0, 1);

                if ($wordStart == "'") {
                    // If the current word is single-quoted, escape any single quotes in the result
                    $result = str_replace("'", "\\'", $result);
                } else if ($wordStart == '"') {
                    // If the current word is double-quoted, escape any double quotes in the result
                    $result = str_replace('"', '\\"', $result);
                } else {
                    // Otherwise assume the string is unquoted and word breaks should be escaped
                    $result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result);
                }

                // Escape output to prevent special characters being lost when passing results to compgen
                return escapeshellarg($result);

            // No transformation by default
            default:
                return $result;
        }
    }

    /**
     * Run the completion handler and return a filtered list of results
     *
     * @deprecated - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion
     *
     * @return string[]
     */
    protected function runCompletion()
    {
        $this->configureCompletion($this->handler);
        return $this->handler->runCompletion();
    }

    /**
     * Configure the CompletionHandler instance before it is run
     *
     * @param CompletionHandler $handler
     */
    protected function configureCompletion(CompletionHandler $handler)
    {
        // Override this method to configure custom value completions
    }

    /**
     * Determine the shell type for use with HookFactory
     *
     * @return string
     */
    protected function getShellType()
    {
        if (!getenv('SHELL')) {
            throw new \RuntimeException('Could not read SHELL environment variable. Please specify your shell type using the --shell-type option.');
        }

        return basename(getenv('SHELL'));
    }

    protected function createDefinition()
    {
        return new InputDefinition(array(
            new InputOption(
                'generate-hook',
                'g',
                InputOption::VALUE_NONE,
                'Generate BASH code that sets up completion for this application.'
            ),
            new InputOption(
                'program',
                'p',
                InputOption::VALUE_REQUIRED,
                "Program name that should trigger completion\n<comment>(defaults to the absolute application path)</comment>."
            ),
            new InputOption(
                'multiple',
                'm',
                InputOption::VALUE_NONE,
                "Generated hook can be used for multiple applications."
            ),
            new InputOption(
                'shell-type',
                null,
                InputOption::VALUE_OPTIONAL,
                'Set the shell type (zsh or bash). Otherwise this is determined automatically.'
            ),
        ));
    }
}


================================================
FILE: src/CompletionContext.php
================================================
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion;

/**
 * Command line context for completion
 *
 * Represents the current state of the command line that is being completed
 */
class CompletionContext
{
    /**
     * The current contents of the command line as a single string
     *
     * Bash equivalent: COMP_LINE
     *
     * @var string
     */
    protected $commandLine;

    /**
     * The index of the user's cursor relative to the start of the command line.
     *
     * If the current cursor position is at the end of the current command,
     * the value of this variable is equal to the length of $this->commandLine
     *
     * Bash equivalent: COMP_POINT
     *
     * @var int
     */
    protected $charIndex = 0;

    /**
     * An array of the individual words in the current command line.
     *
     * This is not set until $this->splitCommand() is called, when it is populated by
     * $commandLine exploded by $wordBreaks
     *
     * Bash equivalent: COMP_WORDS
     *
     * @var string[]|null
     */
    protected $words = null;

    /**
     * Words from the currently command-line before quotes and escaping is processed
     *
     * This is indexed the same as $this->words, but in their raw input terms are in their input form, including
     * quotes and escaping.
     *
     * @var string[]|null
     */
    protected $rawWords = null;

    /**
     * The index in $this->words containing the word at the current cursor position.
     *
     * This is not set until $this->splitCommand() is called.
     *
     * Bash equivalent: COMP_CWORD
     *
     * @var int|null
     */
    protected $wordIndex = null;

    /**
     * Characters that $this->commandLine should be split on to get a list of individual words
     *
     * Bash equivalent: COMP_WORDBREAKS
     *
     * @var string
     */
    protected $wordBreaks = "= \t\n";

    /**
     * Set the whole contents of the command line as a string
     *
     * @param string $commandLine
     */
    public function setCommandLine($commandLine)
    {
        $this->commandLine = $commandLine;
        $this->reset();
    }

    /**
     * Return the current command line verbatim as a string
     *
     * @return string
     */
    public function getCommandLine()
    {
        return $this->commandLine;
    }

    /**
     * Return the word from the command line that the cursor is currently in
     *
     * Most of the time this will be a partial word. If the cursor has a space before it,
     * this will return an empty string, indicating a new word.
     *
     * @return string
     */
    public function getCurrentWord()
    {
        if (isset($this->words[$this->wordIndex])) {
            return $this->words[$this->wordIndex];
        }

        return '';
    }

    /**
     * Return the unprocessed string for the word under the cursor
     *
     * This preserves any quotes and escaping that are present in the input command line.
     *
     * @return string
     */
    public function getRawCurrentWord()
    {
        if (isset($this->rawWords[$this->wordIndex])) {
            return $this->rawWords[$this->wordIndex];
        }

        return '';
    }

    /**
     * Return a word by index from the command line
     *
     * @see $words, $wordBreaks
     * @param int $index
     * @return string
     */
    public function getWordAtIndex($index)
    {
        if (isset($this->words[$index])) {
            return $this->words[$index];
        }

        return '';
    }

    /**
     * Get the contents of the command line, exploded into words based on the configured word break characters
     *
     * @see $wordBreaks, setWordBreaks
     * @return array
     */
    public function getWords()
    {
        if ($this->words === null) {
            $this->splitCommand();
        }

        return $this->words;
    }

    /**
     * Get the unprocessed/literal words from the command line
     *
     * This is indexed the same as getWords(), but preserves any quoting and escaping from the command line
     *
     * @return string[]
     */
    public function getRawWords()
    {
        if ($this->rawWords === null) {
            $this->splitCommand();
        }

        return $this->rawWords;
    }

    /**
     * Get the index of the word the cursor is currently in
     *
     * @see getWords, getCurrentWord
     * @return int
     */
    public function getWordIndex()
    {
        if ($this->wordIndex === null) {
            $this->splitCommand();
        }

        return $this->wordIndex;
    }

    /**
     * Get the character index of the user's cursor on the command line
     *
     * This is in the context of the full command line string, so includes word break characters.
     * Note that some shells can only provide an approximation for character index. Under ZSH for
     * example, this will always be the character at the start of the current word.
     *
     * @return int
     */
    public function getCharIndex()
    {
        return $this->charIndex;
    }

    /**
     * Set the cursor position as a character index relative to the start of the command line
     *
     * @param int $index
     */
    public function setCharIndex($index)
    {
        $this->charIndex = $index;
        $this->reset();
    }

    /**
     * Set characters to use as split points when breaking the command line into words
     *
     * This defaults to a sane value based on BASH's word break characters and shouldn't
     * need to be changed unless your completions contain the default word break characters.
     *
     * @deprecated This is becoming an internal setting that doesn't make sense to expose publicly.
     *
     * @see wordBreaks
     * @param string $charList - a single string containing all of the characters to break words on
     */
    public function setWordBreaks($charList)
    {
        // Drop quotes from break characters - strings are handled separately to word breaks now
        $this->wordBreaks = str_replace(array('"', '\''), '', $charList);;
        $this->reset();
    }

    /**
     * Split the command line into words using the configured word break characters
     *
     * @return string[]
     */
    protected function splitCommand()
    {
        $tokens = $this->tokenizeString($this->commandLine);

        foreach ($tokens as $token) {
            if ($token['type'] != 'break') {
                $this->words[] = $this->getTokenValue($token);
                $this->rawWords[] = $token['value'];
            }

            // Determine which word index the cursor is inside once we reach it's offset
            if ($this->wordIndex === null && $this->charIndex <= $token['offsetEnd']) {
                $this->wordIndex = count($this->words) - 1;

                if ($token['type'] == 'break') {
                    // Cursor is in the break-space after a word
                    // Push an empty word at the cursor to allow completion of new terms at the cursor, ignoring words ahead
                    $this->wordIndex++;
                    $this->words[] = '';
                    $this->rawWords[] = '';
                    continue;
                }

                if ($this->charIndex < $token['offsetEnd']) {
                    // Cursor is inside the current word - truncate the word at the cursor to complete on
                    // This emulates BASH completion's behaviour with COMP_CWORD

                    // Create a copy of the token with its value truncated
                    $truncatedToken = $token;
                    $relativeOffset = $this->charIndex - $token['offset'];
                    $truncatedToken['value'] = substr($token['value'], 0, $relativeOffset);

                    // Replace the current word with the truncated value
                    $this->words[$this->wordIndex] = $this->getTokenValue($truncatedToken);
                    $this->rawWords[$this->wordIndex] = $truncatedToken['value'];
                }
            }
        }

        // Cursor position is past the end of the command line string - consider it a new word
        if ($this->wordIndex === null) {
            $this->wordIndex = count($this->words);
            $this->words[] = '';
            $this->rawWords[] = '';
        }
    }

    /**
     * Return a token's value with escaping and quotes removed
     *
     * @see self::tokenizeString()
     * @param array $token
     * @return string
     */
    protected function getTokenValue($token)
    {
        $value = $token['value'];

        // Remove outer quote characters (or first quote if unclosed)
        if ($token['type'] == 'quoted') {
            $value = preg_replace('/^(?:[\'"])(.*?)(?:[\'"])?$/', '$1', $value);
        }

        // Remove escape characters
        $value = preg_replace('/\\\\(.)/', '$1', $value);

        return $value;
    }

    /**
     * Break a string into words, quoted strings and non-words (breaks)
     *
     * Returns an array of unmodified segments of $string with offset and type information.
     *
     * @param string $string
     * @return array as [ [type => string, value => string, offset => int], ... ]
     */
    protected function tokenizeString($string)
    {
        // Map capture groups to returned token type
        $typeMap = array(
            'double_quote_string' => 'quoted',
            'single_quote_string' => 'quoted',
            'word' => 'word',
            'break' => 'break',
        );

        // Escape every word break character including whitespace
        // preg_quote won't work here as it doesn't understand the ignore whitespace flag ("x")
        $breaks = preg_replace('/(.)/', '\\\$1', $this->wordBreaks);

        $pattern = <<<"REGEX"
            /(?:
                (?P<double_quote_string>
                    "(\\\\.|[^\"\\\\])*(?:"|$)
                ) |
                (?P<single_quote_string>
                    '(\\\\.|[^'\\\\])*(?:'|$)
                ) |
                (?P<word>
                    (?:\\\\.|[^$breaks])+
                ) |
                (?P<break>
                     [$breaks]+
                )
            )/x
REGEX;

        $tokens = array();

        if (!preg_match_all($pattern, $string, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
            return $tokens;
        }

        foreach ($matches as $set) {
            foreach ($set as $groupName => $match) {

                // Ignore integer indices preg_match outputs (duplicates of named groups)
                if (is_integer($groupName)) {
                    continue;
                }

                // Skip if the offset indicates this group didn't match
                if ($match[1] === -1) {
                    continue;
                }

                $tokens[] = array(
                    'type' => $typeMap[$groupName],
                    'value' => $match[0],
                    'offset' => $match[1],
                    'offsetEnd' => $match[1] + strlen($match[0])
                );

                // Move to the next set (only one group should match per set)
                continue;
            }
        }

        return $tokens;
    }

    /**
     * Reset the computed words so that $this->splitWords is forced to run again
     */
    protected function reset()
    {
        $this->words = null;
        $this->wordIndex = null;
    }
}


================================================
FILE: src/CompletionHandler.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion;

use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class CompletionHandler
{
    /**
     * Application to complete for
     * @var \Symfony\Component\Console\Application
     */
    protected $application;

    /**
     * @var Command
     */
    protected $command;

    /**
     * @var CompletionContext
     */
    protected $context;

    /**
     * Array of completion helpers.
     * @var CompletionInterface[]
     */
    protected $helpers = array();

    /**
     * Index the command name was detected at
     * @var int
     */
    private $commandWordIndex;

    public function __construct(Application $application, ?CompletionContext $context = null)
    {
        $this->application = $application;
        $this->context = $context;

        // Set up completions for commands that are built-into Application
        $this->addHandler(
            new Completion(
                'help',
                'command_name',
                Completion::TYPE_ARGUMENT,
                $this->getCommandNames()
            )
        );

        $this->addHandler(
            new Completion(
                'list',
                'namespace',
                Completion::TYPE_ARGUMENT,
                $application->getNamespaces()
            )
        );
    }

    public function setContext(CompletionContext $context)
    {
        $this->context = $context;
    }

    /**
     * @return CompletionContext
     */
    public function getContext()
    {
        return $this->context;
    }

    /**
     * @param CompletionInterface[] $array
     */
    public function addHandlers(array $array)
    {
        $this->helpers = array_merge($this->helpers, $array);
    }

    /**
     * @param CompletionInterface $helper
     */
    public function addHandler(CompletionInterface $helper)
    {
        $this->helpers[] = $helper;
    }

    /**
     * Do the actual completion, returning an array of strings to provide to the parent shell's completion system
     *
     * @throws \RuntimeException
     * @return string[]
     */
    public function runCompletion()
    {
        if (!$this->context) {
            throw new \RuntimeException('A CompletionContext must be set before requesting completion.');
        }

        // Set the command to query options and arugments from
        $this->command = $this->detectCommand();

        $process = array(
            'completeForOptionValues',
            'completeForOptionShortcuts',
            'completeForOptionShortcutValues',
            'completeForOptions',
            'completeForCommandName',
            'completeForCommandArguments'
        );

        foreach ($process as $methodName) {
            $result = $this->{$methodName}();

            if (false !== $result) {
                // Return the result of the first completion mode that matches
                return $this->filterResults((array) $result);
            }
        }

        return array();
    }

    /**
     * Get an InputInterface representation of the completion context
     *
     * @deprecated Incorrectly uses the ArrayInput API and is no longer needed.
     *             This will be removed in the next major version.
     *
     * @return ArrayInput
     */
    public function getInput()
    {
        // Filter the command line content to suit ArrayInput
        $words = $this->context->getWords();
        array_shift($words);
        $words = array_filter($words);

        return new ArrayInput($words);
    }

    /**
     * Attempt to complete the current word as a long-form option (--my-option)
     *
     * @return array|false
     */
    protected function completeForOptions()
    {
        $word = $this->context->getCurrentWord();

        if (substr($word, 0, 2) === '--') {
            $options = array();

            foreach ($this->getAllOptions() as $opt) {
                $options[] = '--'.$opt->getName();
            }

            return $options;
        }

        return false;
    }

    /**
     * Attempt to complete the current word as an option shortcut.
     *
     * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion.
     *
     * @return array|false
     */
    protected function completeForOptionShortcuts()
    {
        $word = $this->context->getCurrentWord();

        if (strpos($word, '-') === 0 && strlen($word) == 2) {
            $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition();

            if ($definition->hasShortcut(substr($word, 1))) {
                return array($word);
            }
        }

        return false;
    }

    /**
     * Attempt to complete the current word as the value of an option shortcut
     *
     * @return array|false
     */
    protected function completeForOptionShortcutValues()
    {
        $wordIndex = $this->context->getWordIndex();

        if ($this->command && $wordIndex > 1) {
            $left = $this->context->getWordAtIndex($wordIndex - 1);

            // Complete short options
            if ($left[0] == '-' && strlen($left) == 2) {
                $shortcut = substr($left, 1);
                $def = $this->command->getNativeDefinition();

                if (!$def->hasShortcut($shortcut)) {
                    return false;
                }

                $opt = $def->getOptionForShortcut($shortcut);
                if ($opt->isValueRequired() || $opt->isValueOptional()) {
                    return $this->completeOption($opt);
                }
            }
        }

        return false;
    }

    /**
     * Attemp to complete the current word as the value of a long-form option
     *
     * @return array|false
     */
    protected function completeForOptionValues()
    {
        $wordIndex = $this->context->getWordIndex();

        if ($this->command && $wordIndex > 1) {
            $left = $this->context->getWordAtIndex($wordIndex - 1);

            if (strpos($left, '--') === 0) {
                $name = substr($left, 2);
                $def = $this->command->getNativeDefinition();

                if (!$def->hasOption($name)) {
                    return false;
                }

                $opt = $def->getOption($name);
                if ($opt->isValueRequired() || $opt->isValueOptional()) {
                    return $this->completeOption($opt);
                }
            }
        }

        return false;
    }

    /**
     * Attempt to complete the current word as a command name
     *
     * @return array|false
     */
    protected function completeForCommandName()
    {
        if (!$this->command || $this->context->getWordIndex() == $this->commandWordIndex) {
            return $this->getCommandNames();
        }

        return false;
    }

    /**
     * Attempt to complete the current word as a command argument value
     *
     * @see Symfony\Component\Console\Input\InputArgument
     * @return array|false
     */
    protected function completeForCommandArguments()
    {
        if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) {
            return false;
        }

        $definition = $this->command->getNativeDefinition();
        $argWords = $this->mapArgumentsToWords($definition->getArguments());
        $wordIndex = $this->context->getWordIndex();

        if (isset($argWords[$wordIndex])) {
            $name = $argWords[$wordIndex];
        } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) {
            $name = end($argWords);
        } else {
            return false;
        }

        if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) {
            return $helper->run();
        }

        if ($this->command instanceof CompletionAwareInterface) {
            return $this->command->completeArgumentValues($name, $this->context);
        }

        return false;
    }

    /**
     * Find a CompletionInterface that matches the current command, target name, and target type
     *
     * @param string $name
     * @param string $type
     * @return CompletionInterface|null
     */
    protected function getCompletionHelper($name, $type)
    {
        foreach ($this->helpers as $helper) {
            if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) {
                continue;
            }

            if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) {
                if ($helper->getTargetName() == $name) {
                    return $helper;
                }
            }
        }

        return null;
    }

    /**
     * Complete the value for the given option if a value completion is availble
     *
     * @param InputOption $option
     * @return array|false
     */
    protected function completeOption(InputOption $option)
    {
        if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) {
            return $helper->run();
        }

        if ($this->command instanceof CompletionAwareInterface) {
            return $this->command->completeOptionValues($option->getName(), $this->context);
        }

        return false;
    }

    /**
     * Step through the command line to determine which word positions represent which argument values
     *
     * The word indexes of argument values are found by eliminating words that are known to not be arguments (options,
     * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument value,
     *
     * @param InputArgument[] $argumentDefinitions
     * @return array as [argument name => word index on command line]
     */
    protected function mapArgumentsToWords($argumentDefinitions)
    {
        $argumentPositions = array();
        $argumentNumber = 0;
        $previousWord = null;
        $argumentNames = array_keys($argumentDefinitions);

        // Build a list of option values to filter out
        $optionsWithArgs = $this->getOptionWordsWithValues();

        foreach ($this->context->getWords() as $wordIndex => $word) {
            // Skip program name, command name, options, and option values
            if ($wordIndex == 0
                || $wordIndex === $this->commandWordIndex
                || ($word && '-' === $word[0])
                || in_array($previousWord, $optionsWithArgs)) {
                $previousWord = $word;
                continue;
            } else {
                $previousWord = $word;
            }

            // If argument n exists, pair that argument's name with the current word
            if (isset($argumentNames[$argumentNumber])) {
                $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber];
            }

            $argumentNumber++;
        }

        return $argumentPositions;
    }

    /**
     * Build a list of option words/flags that will have a value after them
     * Options are returned in the format they appear as on the command line.
     *
     * @return string[] - eg. ['--myoption', '-m', ... ]
     */
    protected function getOptionWordsWithValues()
    {
        $strings = array();

        foreach ($this->getAllOptions() as $option) {
            if ($option->isValueRequired()) {
                $strings[] = '--' . $option->getName();

                if ($option->getShortcut()) {
                    $strings[] = '-' . $option->getShortcut();
                }
            }
        }

        return $strings;
    }

    /**
     * Filter out results that don't match the current word on the command line
     *
     * @param string[] $array
     * @return string[]
     */
    protected function filterResults(array $array)
    {
        $curWord = $this->context->getCurrentWord();

        return array_filter($array, function($val) use ($curWord) {
            return fnmatch($curWord.'*', $val);
        });
    }

    /**
     * Get the combined options of the application and entered command
     *
     * @return InputOption[]
     */
    protected function getAllOptions()
    {
        if (!$this->command) {
            return $this->application->getDefinition()->getOptions();
        }

        return array_merge(
            $this->command->getNativeDefinition()->getOptions(),
            $this->application->getDefinition()->getOptions()
        );
    }

    /**
     * Get command names available for completion
     *
     * Filters out hidden commands where supported.
     *
     * @return string[]
     */
    protected function getCommandNames()
    {
        $commands = array();

        foreach ($this->application->all() as $name => $command) {
            if (!$command->isHidden()) {
                $commands[] = $name;
            }
        }

        return $commands;
    }

    /**
     * Find the current command name in the command-line
     *
     * Note this only cares about flag-type options. Options with values cannot
     * appear before a command name in Symfony Console application.
     *
     * @return Command|null
     */
    private function detectCommand()
    {
        // Always skip the first word (program name)
        $skipNext = true;

        foreach ($this->context->getWords() as $index => $word) {

            // Skip word if flagged
            if ($skipNext) {
                $skipNext = false;
                continue;
            }

            // Skip empty words and words that look like options
            if (strlen($word) == 0 || $word[0] === '-') {
                continue;
            }

            // Return the first unambiguous match to argument-like words
            try {
                $cmd = $this->application->find($word);
                $this->commandWordIndex = $index;
                return $cmd;
            } catch (\InvalidArgumentException $e) {
                // Exception thrown, when multiple or no commands are found.
            }
        }

        // No command found
        return null;
    }
}


================================================
FILE: src/EnvironmentCompletionContext.php
================================================
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion;

class EnvironmentCompletionContext extends CompletionContext
{
    /**
     * Set up completion context from the environment variables set by the parent shell
     */
    public function __construct()
    {
        $this->commandLine = getenv('CMDLINE_CONTENTS');
        $this->charIndex = intval(getenv('CMDLINE_CURSOR_INDEX'));

        if ($this->commandLine === false) {
            $message = 'Failed to configure from environment; Environment var CMDLINE_CONTENTS not set.';

            if (getenv('COMP_LINE')) {
                $message .= "\n\nYou appear to be attempting completion using an out-dated hook. If you've just updated,"
                            . " you probably need to reinitialise the completion shell hook by reloading your shell"
                            . " profile or starting a new shell session. If you are using a hard-coded (rather than generated)"
                            . " hook, you will need to update that function with the new environment variable names."
                            . "\n\nSee here for details: https://github.com/stecman/symfony-console-completion/issues/31";
            }

            throw new \RuntimeException($message);
        }
    }

    /**
     * Use the word break characters set by the parent shell.
     *
     * @throws \RuntimeException
     */
    public function useWordBreaksFromEnvironment()
    {
        $breaks = getenv('CMDLINE_WORDBREAKS');

        if (!$breaks) {
            throw new \RuntimeException('Failed to read word breaks from environment; Environment var CMDLINE_WORDBREAKS not set');
        }

        $this->wordBreaks = $breaks;
    }
}


================================================
FILE: src/HookFactory.php
================================================
<?php


namespace Stecman\Component\Symfony\Console\BashCompletion;

final class HookFactory
{
    /**
     * Hook scripts
     *
     * These are shell-specific scripts that pass required information from that shell's
     * completion system to the interface of the completion command in this module.
     *
     * The following placeholders are replaced with their value at runtime:
     *
     *     %%function_name%%      - name of the generated shell function run for completion
     *     %%program_name%%       - command name completion will be enabled for
     *     %%program_path%%       - path to program the completion is for/generated by
     *     %%completion_command%% - command to be run to compute completions
     *
     * NOTE: Comments are stripped out by HookFactory::stripComments as eval reads
     *       input as a single line, causing it to break if comments are included.
     *       While comments work using `... | source /dev/stdin`, existing installations
     *       are likely using eval as it's been part of the instructions for a while.
     *
     * @var array
     */
    protected static $hooks = array(
        // BASH Hook
        'bash' => <<<'END'
# BASH completion for %%program_path%%
function %%function_name%% {

    # Copy BASH's completion variables to the ones the completion command expects
    # These line up exactly as the library was originally designed for BASH
    local CMDLINE_CONTENTS="$COMP_LINE";
    local CMDLINE_CURSOR_INDEX="$COMP_POINT";
    local CMDLINE_WORDBREAKS="$COMP_WORDBREAKS";

    export CMDLINE_CONTENTS CMDLINE_CURSOR_INDEX CMDLINE_WORDBREAKS;

    local RESULT STATUS;

    # Force splitting by newline instead of default delimiters
    local IFS=$'\n';

    RESULT="$(%%completion_command%% </dev/null)";
    STATUS=$?;

    local cur mail_check_backup;

    mail_check_backup=$MAILCHECK;
    MAILCHECK=-1;

    _get_comp_words_by_ref -n : cur;

    # Check if shell provided path completion is requested
    # @see Completion\ShellPathCompletion
    if [ $STATUS -eq 200 ]; then
        # Turn file/dir completion on temporarily and give control back to BASH
        compopt -o default;
        return 0;

    # Bail out if PHP didn't exit cleanly
    elif [ $STATUS -ne 0 ]; then
        echo -e "$RESULT";
        return $?;
    fi;

    COMPREPLY=(`compgen -W "$RESULT" -- $cur`);

    __ltrim_colon_completions "$cur";

    MAILCHECK=mail_check_backup;
};

if [ "$(type -t _get_comp_words_by_ref)" == "function" ]; then
    complete -F %%function_name%% "%%program_name%%";
else
    >&2 echo "Completion was not registered for %%program_name%%:";
    >&2 echo "The 'bash-completion' package is required but doesn't appear to be installed.";
fi;
END

        // ZSH Hook
        , 'zsh' => <<<'END'
# ZSH completion for %%program_path%%
function %%function_name%% {
    local -x CMDLINE_CONTENTS="$words";
    local -x CMDLINE_CURSOR_INDEX;
    (( CMDLINE_CURSOR_INDEX = ${#${(j. .)words[1,CURRENT]}} ));

    local RESULT STATUS;
    RESULT=("${(@f)$( %%completion_command%% )}");
    STATUS=$?;

    # Check if shell provided path completion is requested
    # @see Completion\ShellPathCompletion
    if [ $STATUS -eq 200 ]; then
        _path_files;
        return 0;

    # Bail out if PHP didn't exit cleanly
    elif [ $STATUS -ne 0 ]; then
        echo -e "$RESULT";
        return $?;
    fi;

    compadd -- $RESULT;
};

compdef %%function_name%% "%%program_name%%";
END
    );

    /**
     * Return the names of shells that have hooks
     *
     * @return string[]
     */
    public static function getShellTypes()
    {
        return array_keys(self::$hooks);
    }

    /**
     * Return a completion hook for the specified shell type
     *
     * @param string  $type - a key from self::$hooks
     * @param string  $programPath
     * @param ?string $programName
     * @param bool    $multiple
     *
     * @return string
     */
    public function generateHook($type, $programPath, $programName = null, $multiple = false)
    {
        if (!isset(self::$hooks[$type])) {
            throw new \RuntimeException(sprintf(
                "Cannot generate hook for unknown shell type '%s'. Available hooks are: %s",
                $type,
                implode(', ', self::getShellTypes())
            ));
        }

        // Use the program path if an alias/name is not given
        $programName = $programName ?: $programPath;

        if ($multiple) {
            $completionCommand = '$1 _completion';
        } else {
            $completionCommand = $programPath . ' _completion';
        }

        // Pass shell type during completion so output can be encoded if the shell requires it
        $completionCommand .= " --shell-type $type";

        return str_replace(
            array(
                '%%function_name%%',
                '%%program_name%%',
                '%%program_path%%',
                '%%completion_command%%',
            ),
            array(
                $this->generateFunctionName($programPath, $programName),
                $programName,
                $programPath,
                $completionCommand
            ),
            $this->stripComments(self::$hooks[$type])
        );
    }

    /**
     * Generate a function name that is unlikely to conflict with other generated function names in the same shell
     */
    protected function generateFunctionName($programPath, $programName)
    {
        return sprintf(
            '_%s_%s_complete',
            $this->sanitiseForFunctionName(basename($programName)),
            substr(md5($programPath), 0, 16)
        );
    }


    /**
     * Make a string safe for use as a shell function name
     *
     * @param string $name
     * @return string
     */
    protected function sanitiseForFunctionName($name)
    {
        $name = str_replace('-', '_', $name);
        return preg_replace('/[^A-Za-z0-9_]+/', '', $name);
    }

    /**
     * Strip '#' style comments from a string
     *
     * BASH's eval doesn't work with comments as it removes line breaks, so comments have to be stripped out
     * for this method of sourcing the hook to work. Eval seems to be the most reliable method of getting a
     * hook into a shell, so while it would be nice to render comments, this stripping is required for now.
     *
     * @param string $script
     * @return string
     */
    protected function stripComments($script)
    {
        return preg_replace('/(^\s*\#.*$)/m', '', $script);
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Common/CompletionHandlerTestCase.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Tests\Common;

use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use PHPUnit\Framework\TestCase;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionHandler;
use Symfony\Component\Console\Application;

/**
 * Base test case for running CompletionHandlers
 */
abstract class CompletionHandlerTestCase extends TestCase
{
    use ArraySubsetAsserts;

    /**
     * @var Application
     */
    protected $application;

    public static function setUpBeforeClass(): void
    {
        require_once __DIR__ . '/../Fixtures/CompletionAwareCommand.php';
        require_once __DIR__ . '/../Fixtures/HiddenCommand.php';
        require_once __DIR__ . '/../Fixtures/TestBasicCommand.php';
        require_once __DIR__ . '/../Fixtures/TestSymfonyStyleCommand.php';
    }

    protected function setUp(): void
    {
        parent::setUp();
        $this->application = new Application('Base application');
        $this->application->addCommands(array(
            new \CompletionAwareCommand(),
            new \TestBasicCommand(),
            new \TestSymfonyStyleCommand()
        ));

        if (method_exists('\HiddenCommand', 'setHidden')) {
            // Added in symfony 7.4
            if (method_exists($this->application, 'addCommand')) {
                $this->application->addCommand(new \HiddenCommand());
            } else {
                $this->application->add(new \HiddenCommand());
            }
        }
    }

    /**
     * Create a handler set up with the given commandline and cursor position
     *
     * @param $commandLine
     * @param ?int $cursorIndex
     * @return CompletionHandler
     */
    protected function createHandler($commandLine, $cursorIndex = null)
    {
        $context = new CompletionContext();
        $context->setCommandLine($commandLine);
        $context->setCharIndex($cursorIndex === null ? strlen($commandLine) : $cursorIndex);

        return new CompletionHandler($this->application, $context);
    }

    /**
     * Get the list of terms from the output of CompletionHandler
     * The array index needs to be reset so that PHPUnit's array equality assertions match correctly.
     *
     * @param string $handlerOutput
     * @return string[]
     */
    protected function getTerms($handlerOutput)
    {
        return array_values($handlerOutput);
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionCommandTest.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Tests;

use PHPUnit\Framework\TestCase;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\NullOutput;

class CompletionCommandTest extends TestCase
{
    /**
     * Ensure conflicting options names and shortcuts from the application do not break the completion command
     */
    public function testConflictingGlobalOptions()
    {
        $this->expectNotToPerformAssertions();

        $app = new Application('Base application');

        // Conflicting option shortcut
        $app->getDefinition()->addOption(
            new InputOption('conflicting-shortcut', 'g', InputOption::VALUE_NONE)
        );

        // Conflicting option name
        $app->getDefinition()->addOption(
            new InputOption('program', null, InputOption::VALUE_REQUIRED)
        );

        // Added in symfony 7.4
        if (method_exists($app, 'addCommand')) {
            $app->addCommand(new CompletionCommand());
        } else {
            $app->add(new CompletionCommand());
        }

        // Check completion command doesn't throw
        $app->doRun(new StringInput('_completion -g --program foo'), new NullOutput());
        $app->doRun(new StringInput('_completion --help'), new NullOutput());
        $app->doRun(new StringInput('help _completion'), new NullOutput());

        // Check default options are available
        $app->doRun(new StringInput('_completion -V -vv --no-ansi --quiet'), new NullOutput());
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Tests;

use PHPUnit\Framework\TestCase;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Stecman\Component\Symfony\Console\BashCompletion\EnvironmentCompletionContext;

class CompletionContextTest extends TestCase
{

    public function testWordBreakSplit()
    {
        $context = new CompletionContext();
        $context->setCommandLine('console  config:application --direction="west" --with-bruce --repeat 3');

        // Cursor at the end of the first word
        $context->setCharIndex(7);
        $words = $context->getWords();

        $this->assertEquals(array(
            'console',
            'config:application',
            '--direction',
            'west',
            '--with-bruce',
            '--repeat',
            '3'
        ), $words);
    }

    public function testCursorPosition()
    {
        $context = new CompletionContext();
        $context->setCommandLine('make horse --legs 4 --colour black ');

        // Cursor at the start of the line
        $context->setCharIndex(0);
        $this->assertEquals(0, $context->getWordIndex());

        // Cursor at the end of the line
        $context->setCharIndex(34);
        $this->assertEquals(5, $context->getWordIndex());
        $this->assertEquals('black', $context->getCurrentWord());

        // Cursor after space at the end of the string
        $context->setCharIndex(35);
        $this->assertEquals(6, $context->getWordIndex());
        $this->assertEquals('', $context->getCurrentWord());

        // Cursor in the middle of 'horse'
        $context->setCharIndex(8);
        $this->assertEquals(1, $context->getWordIndex());
        $this->assertEquals('hor', $context->getCurrentWord());

        // Cursor at the end of '--legs'
        $context->setCharIndex(17);
        $this->assertEquals(2, $context->getWordIndex());
        $this->assertEquals('--legs', $context->getCurrentWord());
    }

    public function testWordBreakingWithSmallInputs()
    {
        $context = new CompletionContext();

        // Cursor at the end of a word and not in the following space has no effect
        $context->setCommandLine('cmd a');
        $context->setCharIndex(5);
        $this->assertEquals(array('cmd', 'a'), $context->getWords());
        $this->assertEquals(1, $context->getWordIndex());
        $this->assertEquals('a', $context->getCurrentWord());

        // As above, but in the middle of the command line string
        $context->setCommandLine('cmd a');
        $context->setCharIndex(3);
        $this->assertEquals(array('cmd', 'a'), $context->getWords());
        $this->assertEquals(0, $context->getWordIndex());
        $this->assertEquals('cmd', $context->getCurrentWord());

        // Cursor at the end of the command line with a space appends an empty word
        $context->setCommandLine('cmd   a ');
        $context->setCharIndex(8);
        $this->assertEquals(array('cmd', 'a', ''), $context->getWords());
        $this->assertEquals(2, $context->getWordIndex());
        $this->assertEquals('', $context->getCurrentWord());

        // Cursor in break space before a word appends an empty word in that position
        $context->setCommandLine('cmd a');
        $context->setCharIndex(4);
        $this->assertEquals(array('cmd', '',  'a',), $context->getWords());
        $this->assertEquals(1, $context->getWordIndex());
        $this->assertEquals('', $context->getCurrentWord());
    }

    public function testQuotedStringWordBreaking()
    {
        $context = new CompletionContext();
        $context->setCharIndex(1000);
        $context->setCommandLine('make horse --legs=3 --name="Jeff the horse" --colour Extreme\\ Blanc \'foo " bar\'');

        // Ensure spaces and quotes are processed correctly
        $this->assertEquals(
            array(
                'make',
                'horse',
                '--legs',
                '3',
                '--name',
                'Jeff the horse',
                '--colour',
                'Extreme Blanc',
                'foo " bar',
                '',
            ),
            $context->getWords()
        );

        // Confirm the raw versions of the words are indexed correctly
        $this->assertEquals(
            array(
                'make',
                'horse',
                '--legs',
                '3',
                '--name',
                '"Jeff the horse"',
                '--colour',
                'Extreme\\ Blanc',
                "'foo \" bar'",
                '',
            ),
            $context->getRawWords()
        );

        $context = new CompletionContext();
        $context->setCommandLine('console --tag=');

        // Cursor after equals symbol on option argument
        $context->setCharIndex(14);
        $this->assertEquals(
            array(
                'console',
                '--tag',
                ''
            ),
            $context->getWords()
        );
    }

    public function testGetRawCurrentWord()
    {
        $context = new CompletionContext();

        $context->setCommandLine('cmd "double quoted" --option \'value\'');
        $context->setCharIndex(13);
        $this->assertEquals(1, $context->getWordIndex());

        $this->assertEquals(array('cmd', '"double q', '--option', "'value'"), $context->getRawWords());
        $this->assertEquals('"double q', $context->getRawCurrentWord());
    }

    public function testConfigureFromEnvironment()
    {
        putenv("CMDLINE_CONTENTS=beam up li");
        putenv('CMDLINE_CURSOR_INDEX=10');

        $context = new EnvironmentCompletionContext();

        $this->assertEquals(
            array(
                'beam',
                'up',
                'li'
            ),
            $context->getWords()
        );

        $this->assertEquals('li', $context->getCurrentWord());
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Tests;

require_once __DIR__ . '/Common/CompletionHandlerTestCase.php';

use Stecman\Component\Symfony\Console\BashCompletion\Completion;
use Stecman\Component\Symfony\Console\BashCompletion\Tests\Common\CompletionHandlerTestCase;

class CompletionHandlerTest extends CompletionHandlerTestCase
{
    public function testCompleteAppName()
    {
        $handler = $this->createHandler('app');

        // It's not valid to complete the application name, so this should return nothing
        $this->assertEmpty($handler->runCompletion());
    }

    public function testCompleteCommandNames()
    {
        $handler = $this->createHandler('app ');
        $this->assertEquals(
            array('help', 'list', 'completion', 'completion-aware', 'wave', 'walk:north'),
            $this->getTerms($handler->runCompletion())
        );
    }

    public function testCompleteCommandNameNonMatch()
    {
        $handler = $this->createHandler('app br');
        $this->assertEmpty($handler->runCompletion());
    }

    public function testCompleteCommandNamePartialTwoMatches()
    {
        $handler = $this->createHandler('app wa');
        $this->assertEquals(array('wave', 'walk:north'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteCommandNamePartialOneMatch()
    {
        $handler = $this->createHandler('app wav');
        $this->assertEquals(array('wave'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteCommandNameFull()
    {
        $handler = $this->createHandler('app wave');

        // Completing on a matching word should return that word so that completion can continue
        $this->assertEquals(array('wave'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteSingleDash()
    {
        $handler = $this->createHandler('app wave -');

        // Short options are not given as suggestions
        $this->assertEmpty($handler->runCompletion());
    }

    public function testCompleteOptionShortcut()
    {
        $handler = $this->createHandler('app wave -j');

        // If a valid option shortcut is completed on, the shortcut is returned so that completion can continue
        $this->assertEquals(array('-j'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteOptionShortcutFirst()
    {
        // Check command options complete
        $handler = $this->createHandler('app -v wave --');
        $this->assertArraySubset(array('--vigorous', '--jazz-hands'), $this->getTerms($handler->runCompletion()));

        // Check unambiguous command name still completes
        $handler = $this->createHandler('app --quiet wav');
        $this->assertEquals(array('wave'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteDoubleDash()
    {
        $handler = $this->createHandler('app wave --');
        $this->assertArraySubset(array('--vigorous', '--jazz-hands'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteOptionFull()
    {
        $handler = $this->createHandler('app wave --jazz');
        $this->assertArraySubset(array('--jazz-hands'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteOptionEqualsValue()
    {
        // Cursor at the "=" sign
        $handler = $this->createHandler('app completion-aware --option-with-suggestions=');
        $this->assertEquals(array('one-opt', 'two-opt'), $this->getTerms($handler->runCompletion()));

        // Cursor at an opening quote
        $handler = $this->createHandler('app completion-aware --option-with-suggestions="');
        $this->assertEquals(array('one-opt', 'two-opt'), $this->getTerms($handler->runCompletion()));

        // Cursor inside a quote with value
        $handler = $this->createHandler('app completion-aware --option-with-suggestions="two');
        $this->assertEquals(array('two-opt'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteOptionOrder()
    {
        // Completion of options should be able to happen anywhere after the command name
        $handler = $this->createHandler('app wave bruce --vi');
        $this->assertEquals(array('--vigorous'), $this->getTerms($handler->runCompletion()));

        // Completing an option mid-commandline should work as normal
        $handler = $this->createHandler('app wave --vi --jazz-hands bruce', 13);
        $this->assertEquals(array('--vigorous'), $this->getTerms($handler->runCompletion()));
    }

    public function testCompleteColonCommand()
    {
        // Normal bash behaviour is to count the colon character as a word break
        // Since a colon is used to namespace Symfony Framework console commands the
        // character in a command name should not be taken as a word break
        //
        // @see https://github.com/stecman/symfony-console-completion/pull/1
        $handler = $this->createHandler('app walk');
        $this->assertEquals(array('walk:north'), $this->getTerms($handler->runCompletion()));

        $handler = $this->createHandler('app walk:north');
        $this->assertEquals(array('walk:north'), $this->getTerms($handler->runCompletion()));

        $handler = $this->createHandler('app walk:north --deploy');
        $this->assertEquals(array('--deploy:jazz-hands'), $this->getTerms($handler->runCompletion()));
    }

    /**
     * @dataProvider completionAwareCommandDataProvider
     */
    public function testCompletionAwareCommand($commandLine, array $suggestions)
    {
        $handler = $this->createHandler($commandLine);
        $this->assertSame($suggestions, $this->getTerms($handler->runCompletion()));
    }

    public static function completionAwareCommandDataProvider(): array
    {
        return array(
            'not complete aware command' => array('app wave --vigorous ', array()),
            'argument suggestions' => array('app completion-aware any-arg ', array('one-arg', 'two-arg')),
            'argument no suggestions' => array('app completion-aware ', array()),
            'argument suggestions + context' => array('app completion-aware any-arg one', array('one-arg', 'one-arg-context')),
            'array argument suggestions' => array('app completion-aware any-arg one-arg array-arg1 ', array('one-arg', 'two-arg')),
            'array argument suggestions + context' => array('app completion-aware any-arg one-arg array-arg1 one', array('one-arg', 'one-arg-context')),
            'option suggestions' => array('app completion-aware --option-with-suggestions ', array('one-opt', 'two-opt')),
            'option no suggestions' => array('app completion-aware --option-without-suggestions ', array()),
            'option suggestions + context' => array(
                'app completion-aware --option-with-suggestions one', array('one-opt', 'one-opt-context')
            ),
        );
    }

    public function testShortCommandMatched()
    {
        $handler = $this->createHandler('app w:n --deploy');
        $this->assertEquals(array('--deploy:jazz-hands'), $this->getTerms($handler->runCompletion()));
    }

    public function testShortCommandNotMatched()
    {
        $handler = $this->createHandler('app w --deploy');
        $this->assertEquals(array(), $this->getTerms($handler->runCompletion()));
    }

    public function testHelpCommandCompletion()
    {
        $handler = $this->createHandler('app help ');
        $this->assertEquals(
            array('help', 'list', 'completion', 'completion-aware', 'wave', 'walk:north'),
            $this->getTerms($handler->runCompletion())
        );
    }

    public function testListCommandCompletion()
    {
        $handler = $this->createHandler('app list ');
        $this->assertEquals(
            array('walk'),
            $this->getTerms($handler->runCompletion())
        );
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionTest.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Tests;

require_once __DIR__ . '/Common/CompletionHandlerTestCase.php';

use Stecman\Component\Symfony\Console\BashCompletion\Completion;
use Stecman\Component\Symfony\Console\BashCompletion\Tests\Common\CompletionHandlerTestCase;

class CompletionTest extends CompletionHandlerTestCase
{
    /**
     * @dataProvider getCompletionTestInput
     */
    public function testCompletionResults($completions, $commandlineResultMap)
    {
        if (!is_array($completions)) {
            $completions = array($completions);
        }

        foreach ($commandlineResultMap as $commandLine => $result) {
            $handler = $this->createHandler($commandLine);
            $handler->addHandlers($completions);
            $this->assertEquals($result, $this->getTerms($handler->runCompletion()));
        }
    }

    public function getCompletionTestInput()
    {
        $options = array('smooth', 'latin', 'moody');

        return array(
            'command match' => array(
                new Completion(
                    'wave',
                    'target',
                    Completion::ALL_TYPES,
                    $options
                ),
                array(
                    'app walk:north --target ' => array(),
                    'app wave ' => $options
                )
            ),

            'type restriction option' => array(
                new Completion(
                    Completion::ALL_COMMANDS,
                    'target',
                    Completion::TYPE_OPTION,
                    $options
                ),
                array(
                    'app walk:north --target ' => $options,
                    'app wave ' => array()
                )
            ),

            'type restriction argument' => array(
                new Completion(
                    Completion::ALL_COMMANDS,
                    'target',
                    Completion::TYPE_ARGUMENT,
                    $options
                ),
                array(
                    'app walk:north --target ' => array(),
                    'app wave ' => $options
                )
            ),

            'makeGlobalHandler static' => array(
                Completion::makeGlobalHandler(
                    'target',
                    Completion::ALL_TYPES,
                    $options
                ),
                array(
                    'app walk:north --target ' => $options,
                    'app wave ' => $options
                )
            ),

            'with anonymous function' => array(
                new Completion(
                    'wave',
                    'style',
                    Completion::TYPE_OPTION,
                    function() {
                        return range(1, 5);
                    }
                ),
                array(
                    'app walk:north --target ' => array(),
                    'app wave ' => array(),
                    'app wave --style ' => array(1, 2,3, 4, 5)
                )
            ),

            'with callable array' => array(
                new Completion(
                    Completion::ALL_COMMANDS,
                    'target',
                    Completion::ALL_TYPES,
                    array($this, 'instanceMethodForCallableCheck')
                ),
                array(
                    'app walk:north --target ' => array('hello', 'world'),
                    'app wave ' => array('hello', 'world')
                )
            ),

            'multiple handlers' => array(
                array(
                    new Completion(
                        Completion::ALL_COMMANDS,
                        'target',
                        Completion::TYPE_OPTION,
                        array('all:option:target')
                    ),
                    new Completion(
                        Completion::ALL_COMMANDS,
                        'target',
                        Completion::ALL_TYPES,
                        array('all:all:target')
                    ),
                    new Completion(
                        Completion::ALL_COMMANDS,
                        'style',
                        Completion::TYPE_OPTION,
                        array('all:option:style')
                    ),
                ),
                array(
                    'app walk:north ' => array(),
                    'app walk:north -t ' => array('all:option:target'),
                    'app wave ' => array('all:all:target'),
                    'app wave bruce -s ' => array('all:option:style'),
                    'app walk:north --style ' => array('all:option:style'),
                )
            )
        );
    }

    /**
     * Used in the test "with callable array"
     * @return array
     */
    public function instanceMethodForCallableCheck()
    {
        return array('hello', 'world');
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/CompletionAwareCommand.php
================================================
<?php


use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class CompletionAwareCommand extends Command implements CompletionAwareInterface
{
    protected function configure(): void
    {
        $this->setName('completion-aware')
            ->addOption('option-with-suggestions', null, InputOption::VALUE_REQUIRED)
            ->addOption('option-without-suggestions', null, InputOption::VALUE_REQUIRED)
            ->addArgument('argument-without-suggestions')
            ->addArgument('argument-with-suggestions')
            ->addArgument('array-argument-with-suggestions', InputArgument::IS_ARRAY)
        ;
    }

    /**
     * Returns possible option values.
     *
     * @param string            $optionName Option name.
     * @param CompletionContext $context    Completion context.
     *
     * @return array
     */
    public function completeOptionValues($optionName, CompletionContext $context)
    {
        if ($optionName === 'option-with-suggestions') {
            $suggestions = array('one-opt', 'two-opt');

            if ('one' === $context->getCurrentWord()) {
                $suggestions[] = 'one-opt-context';
            }

            return $suggestions;
        }

        return array();
    }

    /**
     * Returns possible argument values.
     *
     * @param string            $argumentName Argument name.
     * @param CompletionContext $context      Completion context.
     *
     * @return array
     */
    public function completeArgumentValues($argumentName, CompletionContext $context)
    {
        if (in_array($argumentName, array('argument-with-suggestions', 'array-argument-with-suggestions'))) {
            $suggestions = array('one-arg', 'two-arg');

            if ('one' === $context->getCurrentWord()) {
                $suggestions[] = 'one-arg-context';
            }

            return $suggestions;
        }

        return array();
    }

}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/HiddenCommand.php
================================================
<?php

use Symfony\Component\Console\Command\Command;

class HiddenCommand extends Command
{
    protected function configure(): void
    {
        $this->setName('internals')
            ->setHidden(true);
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/TestBasicCommand.php
================================================
<?php

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;

class TestBasicCommand extends Command
{
    protected function configure(): void
    {
        $this->setName('wave')
            ->addOption(
                'vigorous'
            )
            ->addOption(
                'jazz-hands',
                'j'
            )
            ->addOption(
                'style',
                's',
                InputOption::VALUE_REQUIRED
            )
            ->addArgument(
                'target',
                InputArgument::REQUIRED
            );
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/TestSymfonyStyleCommand.php
================================================
<?php

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;

class TestSymfonyStyleCommand extends Command
{
    protected function configure(): void
    {
        $this->setName('walk:north')
            ->addOption(
                'power',
                'p'
            )
            ->addOption(
                'deploy:jazz-hands',
                'j'
            )
            ->addOption(
                'style',
                's',
                InputOption::VALUE_REQUIRED
            )
            ->addOption(
                'target',
                't',
                InputOption::VALUE_REQUIRED
            );
    }
}


================================================
FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php
================================================
<?php

namespace Stecman\Component\Symfony\Console\BashCompletion\Tests;

use PHPUnit\Framework\TestCase;
use Stecman\Component\Symfony\Console\BashCompletion\HookFactory;

class HookFactoryTest extends TestCase
{
    /**
     * @var HookFactory
     */
    protected $factory;

    protected function setUp(): void
    {
        parent::setUp();
        $this->factory = new HookFactory();
    }

    /**
     * @dataProvider generateHookDataProvider
     */
    public function testBashSyntax($programPath, $programName, $multiple)
    {
        if ($this->hasProgram('bash')) {
            $script = $this->factory->generateHook('bash', $programPath, $programName, $multiple);
            $this->assertSyntaxIsValid($script, 'bash -n', 'BASH hook');
        } else {
            $this->markTestSkipped("Couldn't detect BASH program to run hook syntax check");
        }
    }

    /**
     * @dataProvider generateHookDataProvider
     */
    public function testZshSyntax($programPath, $programName, $multiple)
    {
        if ($this->hasProgram('zsh')) {
            $script = $this->factory->generateHook('zsh', $programPath, $programName, $multiple);
            $this->assertSyntaxIsValid($script, 'zsh -n', 'ZSH hook');
        } else {
            $this->markTestSkipped("Couldn't detect ZSH program to run hook syntax check");
        }
    }

    public static function generateHookDataProvider()
    {
        return array(
            array('/path/to/myprogram', null, false),
            array('/path/to/myprogram', null, true),
            array('/path/to/myprogram', 'myprogram', false),
            array('/path/to/myprogram', 'myprogram', true),
            array('/path/to/my-program', 'my-program', false)
        );
    }

    public function testForMissingSemiColons()
    {
        $this->expectNotToPerformAssertions();

        $class = new \ReflectionClass('Stecman\Component\Symfony\Console\BashCompletion\HookFactory');
        $properties = $class->getStaticProperties();
        $hooks = $properties['hooks'];

        // Check each line is commented or closed correctly to be collapsed for eval
        foreach ($hooks as $shellType => $hook) {
            $line = strtok($hook, "\n");
            $lineNumber = 0;

            while ($line !== false) {
                $lineNumber++;

                if (!$this->isScriptLineValid($line)) {
                    $this->fail("$shellType hook appears to be missing a semicolon on line $lineNumber:\n> $line");
                }

                $line = strtok("\n");
            }
        }
    }

    /**
     * Check if a line of shell script is safe to be collapsed to one line for eval
     */
    protected function isScriptLineValid($line)
    {
        if (preg_match('/^\s*#/', $line)) {
            // Line is commented out
            return true;
        }

        if (preg_match('/[;\{\}]\s*$/', $line)) {
            // Line correctly ends with a semicolon or syntax
            return true;
        }

        if (preg_match('
                    /(
                        ;\s*then |
                        \s*else
                    )
                    \s*$
                    /x', $line)
        ) {
            // Line ends with another permitted sequence
            return true;
        }

        return false;
    }

    protected function hasProgram($programName)
    {
        exec(sprintf(
            'command -v %s',
            escapeshellarg($programName)
        ), $output, $return);

        return $return === 0;
    }

    /**
     * @param string $code - code to pipe to the syntax checking command
     * @param string $syntaxCheckCommand - equivalent to `bash -n`.
     * @param string $context - what the syntax check is for
     */
    protected function assertSyntaxIsValid($code, $syntaxCheckCommand, $context)
    {
        $process = proc_open(
            escapeshellcmd($syntaxCheckCommand),
            array(
                0 => array('pipe', 'r'),
                1 => array('pipe', 'w'),
                2 => array('pipe', 'w')
            ),
            $pipes
        );

        if (is_resource($process)) {
            // Push code into STDIN for the syntax checking process
            fwrite($pipes[0], $code);
            fclose($pipes[0]);

            $output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
            fclose($pipes[1]);
            fclose($pipes[2]);

            $status = proc_close($process);

            $this->assertSame(0, $status, "Syntax check for $context failed:\n$output");
        } else {
            throw new \RuntimeException("Failed to start process with command '$syntaxCheckCommand'");
        }
    }
}


================================================
FILE: tests/bootstrap.php
================================================
<?php

$filename = __DIR__ . '/../vendor/autoload.php';

if (!file_exists($filename)) {
    echo 'You must first install the vendors using composer.' . PHP_EOL;
    exit(1);
}

require_once $filename;
Download .txt
gitextract_ezfj5dnr/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── phpunit.yml
├── .gitignore
├── LICENCE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src/
│   ├── Completion/
│   │   ├── CompletionAwareInterface.php
│   │   ├── CompletionInterface.php
│   │   └── ShellPathCompletion.php
│   ├── Completion.php
│   ├── CompletionCommand.php
│   ├── CompletionContext.php
│   ├── CompletionHandler.php
│   ├── EnvironmentCompletionContext.php
│   └── HookFactory.php
└── tests/
    ├── Stecman/
    │   └── Component/
    │       └── Symfony/
    │           └── Console/
    │               └── BashCompletion/
    │                   ├── Common/
    │                   │   └── CompletionHandlerTestCase.php
    │                   ├── CompletionCommandTest.php
    │                   ├── CompletionContextTest.php
    │                   ├── CompletionHandlerTest.php
    │                   ├── CompletionTest.php
    │                   ├── Fixtures/
    │                   │   ├── CompletionAwareCommand.php
    │                   │   ├── HiddenCommand.php
    │                   │   ├── TestBasicCommand.php
    │                   │   └── TestSymfonyStyleCommand.php
    │                   └── HookFactoryTest.php
    └── bootstrap.php
Download .txt
SYMBOL INDEX (143 symbols across 19 files)

FILE: src/Completion.php
  class Completion (line 8) | class Completion implements CompletionInterface
    method makeGlobalHandler (line 51) | public static function makeGlobalHandler($targetName, $type, $completion)
    method __construct (line 62) | public function __construct($commandName, $targetName, $type, $complet...
    method run (line 75) | public function run()
    method getType (line 90) | public function getType()
    method setType (line 101) | public function setType($type)
    method getCommandName (line 112) | public function getCommandName()
    method setCommandName (line 123) | public function setCommandName($commandName)
    method getTargetName (line 134) | public function getTargetName()
    method setTargetName (line 145) | public function setTargetName($targetName)
    method getCompletion (line 155) | public function getCompletion()
    method setCompletion (line 166) | public function setCompletion($completion)
    method isCallable (line 176) | public function isCallable()

FILE: src/Completion/CompletionAwareInterface.php
  type CompletionAwareInterface (line 7) | interface CompletionAwareInterface
    method completeOptionValues (line 17) | public function completeOptionValues($optionName, CompletionContext $c...
    method completeArgumentValues (line 26) | public function completeArgumentValues($argumentName, CompletionContex...

FILE: src/Completion/CompletionInterface.php
  type CompletionInterface (line 6) | interface CompletionInterface
    method getType (line 23) | public function getType();
    method getCommandName (line 32) | public function getCommandName();
    method getTargetName (line 40) | public function getTargetName();
    method run (line 47) | public function run();

FILE: src/Completion/ShellPathCompletion.php
  class ShellPathCompletion (line 11) | class ShellPathCompletion implements CompletionInterface
    method __construct (line 25) | public function __construct($commandName, $targetName, $type)
    method getType (line 35) | public function getType()
    method getCommandName (line 43) | public function getCommandName()
    method getTargetName (line 51) | public function getTargetName()
    method run (line 61) | public function run()

FILE: src/CompletionCommand.php
  class CompletionCommand (line 11) | class CompletionCommand extends SymfonyCommand
    method configure (line 18) | protected function configure(): void
    method getNativeDefinition (line 43) | public function getNativeDefinition(): InputDefinition
    method mergeApplicationDefinition (line 54) | public function mergeApplicationDefinition(bool $mergeArgs = true): void
    method filterApplicationOptions (line 77) | protected function filterApplicationOptions(array $appOptions)
    method execute (line 94) | protected function execute(InputInterface $input, OutputInterface $out...
    method escapeForShell (line 145) | protected function escapeForShell($result, $shellType)
    method runCompletion (line 181) | protected function runCompletion()
    method configureCompletion (line 192) | protected function configureCompletion(CompletionHandler $handler)
    method getShellType (line 202) | protected function getShellType()
    method createDefinition (line 211) | protected function createDefinition()

FILE: src/CompletionContext.php
  class CompletionContext (line 11) | class CompletionContext
    method setCommandLine (line 81) | public function setCommandLine($commandLine)
    method getCommandLine (line 92) | public function getCommandLine()
    method getCurrentWord (line 105) | public function getCurrentWord()
    method getRawCurrentWord (line 121) | public function getRawCurrentWord()
    method getWordAtIndex (line 137) | public function getWordAtIndex($index)
    method getWords (line 152) | public function getWords()
    method getRawWords (line 168) | public function getRawWords()
    method getWordIndex (line 183) | public function getWordIndex()
    method getCharIndex (line 201) | public function getCharIndex()
    method setCharIndex (line 211) | public function setCharIndex($index)
    method setWordBreaks (line 228) | public function setWordBreaks($charList)
    method splitCommand (line 240) | protected function splitCommand()
    method getTokenValue (line 294) | protected function getTokenValue($token)
    method tokenizeString (line 317) | protected function tokenizeString($string)
    method reset (line 385) | protected function reset()

FILE: src/CompletionHandler.php
  class CompletionHandler (line 13) | class CompletionHandler
    method __construct (line 43) | public function __construct(Application $application, ?CompletionConte...
    method setContext (line 68) | public function setContext(CompletionContext $context)
    method getContext (line 76) | public function getContext()
    method addHandlers (line 84) | public function addHandlers(array $array)
    method addHandler (line 92) | public function addHandler(CompletionInterface $helper)
    method runCompletion (line 103) | public function runCompletion()
    method getInput (line 141) | public function getInput()
    method completeForOptions (line 156) | protected function completeForOptions()
    method completeForOptionShortcuts (line 180) | protected function completeForOptionShortcuts()
    method completeForOptionShortcutValues (line 200) | protected function completeForOptionShortcutValues()
    method completeForOptionValues (line 231) | protected function completeForOptionValues()
    method completeForCommandName (line 261) | protected function completeForCommandName()
    method completeForCommandArguments (line 276) | protected function completeForCommandArguments()
    method getCompletionHelper (line 312) | protected function getCompletionHelper($name, $type)
    method completeOption (line 335) | protected function completeOption(InputOption $option)
    method mapArgumentsToWords (line 357) | protected function mapArgumentsToWords($argumentDefinitions)
    method getOptionWordsWithValues (line 396) | protected function getOptionWordsWithValues()
    method filterResults (line 419) | protected function filterResults(array $array)
    method getAllOptions (line 433) | protected function getAllOptions()
    method getCommandNames (line 452) | protected function getCommandNames()
    method detectCommand (line 473) | private function detectCommand()

FILE: src/EnvironmentCompletionContext.php
  class EnvironmentCompletionContext (line 6) | class EnvironmentCompletionContext extends CompletionContext
    method __construct (line 11) | public function __construct()
    method useWordBreaksFromEnvironment (line 36) | public function useWordBreaksFromEnvironment()

FILE: src/HookFactory.php
  class HookFactory (line 6) | final class HookFactory
    method getShellTypes (line 121) | public static function getShellTypes()
    method generateHook (line 136) | public function generateHook($type, $programPath, $programName = null,...
    method generateFunctionName (line 178) | protected function generateFunctionName($programPath, $programName)
    method sanitiseForFunctionName (line 194) | protected function sanitiseForFunctionName($name)
    method stripComments (line 210) | protected function stripComments($script)

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Common/CompletionHandlerTestCase.php
  class CompletionHandlerTestCase (line 14) | abstract class CompletionHandlerTestCase extends TestCase
    method setUpBeforeClass (line 23) | public static function setUpBeforeClass(): void
    method setUp (line 31) | protected function setUp(): void
    method createHandler (line 58) | protected function createHandler($commandLine, $cursorIndex = null)
    method getTerms (line 74) | protected function getTerms($handlerOutput)

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionCommandTest.php
  class CompletionCommandTest (line 12) | class CompletionCommandTest extends TestCase
    method testConflictingGlobalOptions (line 17) | public function testConflictingGlobalOptions()

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php
  class CompletionContextTest (line 9) | class CompletionContextTest extends TestCase
    method testWordBreakSplit (line 12) | public function testWordBreakSplit()
    method testCursorPosition (line 32) | public function testCursorPosition()
    method testWordBreakingWithSmallInputs (line 62) | public function testWordBreakingWithSmallInputs()
    method testQuotedStringWordBreaking (line 95) | public function testQuotedStringWordBreaking()
    method testGetRawCurrentWord (line 150) | public function testGetRawCurrentWord()
    method testConfigureFromEnvironment (line 162) | public function testConfigureFromEnvironment()

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php
  class CompletionHandlerTest (line 10) | class CompletionHandlerTest extends CompletionHandlerTestCase
    method testCompleteAppName (line 12) | public function testCompleteAppName()
    method testCompleteCommandNames (line 20) | public function testCompleteCommandNames()
    method testCompleteCommandNameNonMatch (line 29) | public function testCompleteCommandNameNonMatch()
    method testCompleteCommandNamePartialTwoMatches (line 35) | public function testCompleteCommandNamePartialTwoMatches()
    method testCompleteCommandNamePartialOneMatch (line 41) | public function testCompleteCommandNamePartialOneMatch()
    method testCompleteCommandNameFull (line 47) | public function testCompleteCommandNameFull()
    method testCompleteSingleDash (line 55) | public function testCompleteSingleDash()
    method testCompleteOptionShortcut (line 63) | public function testCompleteOptionShortcut()
    method testCompleteOptionShortcutFirst (line 71) | public function testCompleteOptionShortcutFirst()
    method testCompleteDoubleDash (line 82) | public function testCompleteDoubleDash()
    method testCompleteOptionFull (line 88) | public function testCompleteOptionFull()
    method testCompleteOptionEqualsValue (line 94) | public function testCompleteOptionEqualsValue()
    method testCompleteOptionOrder (line 109) | public function testCompleteOptionOrder()
    method testCompleteColonCommand (line 120) | public function testCompleteColonCommand()
    method testCompletionAwareCommand (line 140) | public function testCompletionAwareCommand($commandLine, array $sugges...
    method completionAwareCommandDataProvider (line 146) | public static function completionAwareCommandDataProvider(): array
    method testShortCommandMatched (line 163) | public function testShortCommandMatched()
    method testShortCommandNotMatched (line 169) | public function testShortCommandNotMatched()
    method testHelpCommandCompletion (line 175) | public function testHelpCommandCompletion()
    method testListCommandCompletion (line 184) | public function testListCommandCompletion()

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionTest.php
  class CompletionTest (line 10) | class CompletionTest extends CompletionHandlerTestCase
    method testCompletionResults (line 15) | public function testCompletionResults($completions, $commandlineResult...
    method getCompletionTestInput (line 28) | public function getCompletionTestInput()
    method instanceMethodForCallableCheck (line 149) | public function instanceMethodForCallableCheck()

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/CompletionAwareCommand.php
  class CompletionAwareCommand (line 10) | class CompletionAwareCommand extends Command implements CompletionAwareI...
    method configure (line 12) | protected function configure(): void
    method completeOptionValues (line 31) | public function completeOptionValues($optionName, CompletionContext $c...
    method completeArgumentValues (line 54) | public function completeArgumentValues($argumentName, CompletionContex...

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/HiddenCommand.php
  class HiddenCommand (line 5) | class HiddenCommand extends Command
    method configure (line 7) | protected function configure(): void

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/TestBasicCommand.php
  class TestBasicCommand (line 7) | class TestBasicCommand extends Command
    method configure (line 9) | protected function configure(): void

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/TestSymfonyStyleCommand.php
  class TestSymfonyStyleCommand (line 6) | class TestSymfonyStyleCommand extends Command
    method configure (line 8) | protected function configure(): void

FILE: tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php
  class HookFactoryTest (line 8) | class HookFactoryTest extends TestCase
    method setUp (line 15) | protected function setUp(): void
    method testBashSyntax (line 24) | public function testBashSyntax($programPath, $programName, $multiple)
    method testZshSyntax (line 37) | public function testZshSyntax($programPath, $programName, $multiple)
    method generateHookDataProvider (line 47) | public static function generateHookDataProvider()
    method testForMissingSemiColons (line 58) | public function testForMissingSemiColons()
    method isScriptLineValid (line 86) | protected function isScriptLineValid($line)
    method hasProgram (line 113) | protected function hasProgram($programName)
    method assertSyntaxIsValid (line 128) | protected function assertSyntaxIsValid($code, $syntaxCheckCommand, $co...
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (101K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 242,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"composer\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    vers"
  },
  {
    "path": ".github/workflows/phpunit.yml",
    "chars": 1268,
    "preview": "name: PHPUnit\n\non: pull_request\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: phpunit-${{ github.head_ref || git"
  },
  {
    "path": ".gitignore",
    "chars": 71,
    "preview": "vendor\n.idea\n/build/\nphpunit.xml\n/composer.lock\n/.phpunit.result.cache\n"
  },
  {
    "path": "LICENCE",
    "chars": 1082,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Stephen Holdaway\n\nPermission is hereby granted, free of charge, to any person "
  },
  {
    "path": "README.md",
    "chars": 9690,
    "preview": "# BASH/ZSH auto-complete for Symfony Console applications\n\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ste"
  },
  {
    "path": "composer.json",
    "chars": 743,
    "preview": "{\n    \"name\": \"stecman/symfony-console-completion\",\n    \"description\": \"Automatic BASH completion for Symfony Console Co"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 830,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNam"
  },
  {
    "path": "src/Completion/CompletionAwareInterface.php",
    "chars": 705,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion;\n\nuse Stecman\\Component\\Symfony\\Console\\Bas"
  },
  {
    "path": "src/Completion/CompletionInterface.php",
    "chars": 1521,
    "preview": "<?php\n\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion;\n\ninterface CompletionInterface\n{\n    // S"
  },
  {
    "path": "src/Completion/ShellPathCompletion.php",
    "chars": 1349,
    "preview": "<?php\n\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion;\n\n/**\n * Shell Path Completion\n *\n * Defer"
  },
  {
    "path": "src/Completion.php",
    "chars": 4335,
    "preview": "<?php\n\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion;\n\nuse Stecman\\Component\\Symfony\\Console\\BashCompletio"
  },
  {
    "path": "src/CompletionCommand.php",
    "chars": 7730,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion;\n\nuse Symfony\\Component\\Console\\Command\\Command as Sym"
  },
  {
    "path": "src/CompletionContext.php",
    "chars": 11393,
    "preview": "<?php\n\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion;\n\n/**\n * Command line context for completion\n *\n * Re"
  },
  {
    "path": "src/CompletionHandler.php",
    "chars": 14526,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion;\n\nuse Stecman\\Component\\Symfony\\Console\\BashCompletion"
  },
  {
    "path": "src/EnvironmentCompletionContext.php",
    "chars": 1718,
    "preview": "<?php\n\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion;\n\nclass EnvironmentCompletionContext extends Completi"
  },
  {
    "path": "src/HookFactory.php",
    "chars": 6521,
    "preview": "<?php\n\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion;\n\nfinal class HookFactory\n{\n    /**\n     * Hook scrip"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/Common/CompletionHandlerTestCase.php",
    "chars": 2476,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Tests\\Common;\n\nuse DMS\\PHPUnitExtensions\\ArraySubset\\A"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionCommandTest.php",
    "chars": 1695,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Tests;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Stecman\\Co"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionContextTest.php",
    "chars": 5935,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Tests;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Stecman\\Co"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionHandlerTest.php",
    "chars": 7956,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Tests;\n\nrequire_once __DIR__ . '/Common/CompletionHand"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/CompletionTest.php",
    "chars": 4978,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Tests;\n\nrequire_once __DIR__ . '/Common/CompletionHand"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/CompletionAwareCommand.php",
    "chars": 2171,
    "preview": "<?php\n\n\nuse Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\CompletionAwareInterface;\nuse Stecman\\Component\\"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/HiddenCommand.php",
    "chars": 215,
    "preview": "<?php\n\nuse Symfony\\Component\\Console\\Command\\Command;\n\nclass HiddenCommand extends Command\n{\n    protected function conf"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/TestBasicCommand.php",
    "chars": 674,
    "preview": "<?php\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Compo"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/Fixtures/TestSymfonyStyleCommand.php",
    "chars": 684,
    "preview": "<?php\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputOption;\n\nclass TestSymfon"
  },
  {
    "path": "tests/Stecman/Component/Symfony/Console/BashCompletion/HookFactoryTest.php",
    "chars": 4708,
    "preview": "<?php\n\nnamespace Stecman\\Component\\Symfony\\Console\\BashCompletion\\Tests;\n\nuse PHPUnit\\Framework\\TestCase;\nuse Stecman\\Co"
  },
  {
    "path": "tests/bootstrap.php",
    "chars": 201,
    "preview": "<?php\n\n$filename = __DIR__ . '/../vendor/autoload.php';\n\nif (!file_exists($filename)) {\n    echo 'You must first install"
  }
]

About this extraction

This page contains the full source code of the stecman/symfony-console-completion GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (93.2 KB), approximately 21.8k tokens, and a symbol index with 143 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!