2022-03-14 16:22:30 -04:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Console\Command ;
2023-02-24 06:26:40 -05:00
use Symfony\Component\Console\Attribute\AsCommand ;
2022-03-14 16:22:30 -04:00
use Symfony\Component\Console\Completion\CompletionInput ;
use Symfony\Component\Console\Completion\CompletionSuggestions ;
use Symfony\Component\Console\Completion\Output\BashCompletionOutput ;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface ;
2023-02-24 06:26:40 -05:00
use Symfony\Component\Console\Completion\Output\FishCompletionOutput ;
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput ;
2022-03-14 16:22:30 -04:00
use Symfony\Component\Console\Exception\CommandNotFoundException ;
use Symfony\Component\Console\Exception\ExceptionInterface ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
/**
* Responsible for providing the values to the shell completion .
*
* @ author Wouter de Jong < wouter @ wouterj . nl >
*/
2023-02-24 06:26:40 -05:00
#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
2022-03-14 16:22:30 -04:00
final class CompleteCommand extends Command
{
2023-02-24 06:26:40 -05:00
public const COMPLETION_API_VERSION = '1' ;
/**
* @ deprecated since Symfony 6.1
*/
2022-03-14 16:22:30 -04:00
protected static $defaultName = '|_complete' ;
2023-02-24 06:26:40 -05:00
/**
* @ deprecated since Symfony 6.1
*/
2022-03-14 16:22:30 -04:00
protected static $defaultDescription = 'Internal command to provide shell completion suggestions' ;
private $completionOutputs ;
private $isDebug = false ;
/**
* @ param array < string , class - string < CompletionOutputInterface >> $completionOutputs A list of additional completion outputs , with shell name as key and FQCN as value
*/
public function __construct ( array $completionOutputs = [])
{
// must be set before the parent constructor, as the property value is used in configure()
2023-02-24 06:26:40 -05:00
$this -> completionOutputs = $completionOutputs + [
'bash' => BashCompletionOutput :: class ,
'fish' => FishCompletionOutput :: class ,
'zsh' => ZshCompletionOutput :: class ,
];
2022-03-14 16:22:30 -04:00
parent :: __construct ();
}
protected function configure () : void
{
$this
-> addOption ( 'shell' , 's' , InputOption :: VALUE_REQUIRED , 'The shell type ("' . implode ( '", "' , array_keys ( $this -> completionOutputs )) . '")' )
-> addOption ( 'input' , 'i' , InputOption :: VALUE_REQUIRED | InputOption :: VALUE_IS_ARRAY , 'An array of input tokens (e.g. COMP_WORDS or argv)' )
-> addOption ( 'current' , 'c' , InputOption :: VALUE_REQUIRED , 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)' )
2023-02-24 06:26:40 -05:00
-> addOption ( 'api-version' , 'a' , InputOption :: VALUE_REQUIRED , 'The API version of the completion script' )
-> addOption ( 'symfony' , 'S' , InputOption :: VALUE_REQUIRED , 'deprecated' )
2022-03-14 16:22:30 -04:00
;
}
protected function initialize ( InputInterface $input , OutputInterface $output )
{
2023-02-24 06:26:40 -05:00
$this -> isDebug = filter_var ( getenv ( 'SYMFONY_COMPLETION_DEBUG' ), \FILTER_VALIDATE_BOOL );
2022-03-14 16:22:30 -04:00
}
protected function execute ( InputInterface $input , OutputInterface $output ) : int
{
try {
2023-02-24 06:26:40 -05:00
// "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1
$version = $input -> getOption ( 'symfony' ) ? '1' : $input -> getOption ( 'api-version' );
if ( $version && version_compare ( $version , self :: COMPLETION_API_VERSION , '<' )) {
$message = sprintf ( 'Completion script version is not supported ("%s" given, ">=%s" required).' , $version , self :: COMPLETION_API_VERSION );
$this -> log ( $message );
2022-03-14 16:22:30 -04:00
2023-02-24 06:26:40 -05:00
$output -> writeln ( $message . ' Install the Symfony completion script again by using the "completion" command.' );
2022-03-14 16:22:30 -04:00
2023-02-24 06:26:40 -05:00
return 126 ;
}
2022-03-14 16:22:30 -04:00
$shell = $input -> getOption ( 'shell' );
if ( ! $shell ) {
throw new \RuntimeException ( 'The "--shell" option must be set.' );
}
if ( ! $completionOutput = $this -> completionOutputs [ $shell ] ? ? false ) {
throw new \RuntimeException ( sprintf ( 'Shell completion is not supported for your shell: "%s" (supported: "%s").' , $shell , implode ( '", "' , array_keys ( $this -> completionOutputs ))));
}
$completionInput = $this -> createCompletionInput ( $input );
$suggestions = new CompletionSuggestions ();
$this -> log ([
'' ,
'<comment>' . date ( 'Y-m-d H:i:s' ) . '</>' ,
'<info>Input:</> <comment>("|" indicates the cursor position)</>' ,
' ' . ( string ) $completionInput ,
'<info>Command:</>' ,
' ' . ( string ) implode ( ' ' , $_SERVER [ 'argv' ]),
'<info>Messages:</>' ,
]);
$command = $this -> findCommand ( $completionInput , $output );
if ( null === $command ) {
$this -> log ( ' No command found, completing using the Application class.' );
$this -> getApplication () -> complete ( $completionInput , $suggestions );
} elseif (
$completionInput -> mustSuggestArgumentValuesFor ( 'command' )
&& $command -> getName () !== $completionInput -> getCompletionValue ()
2023-02-24 06:26:40 -05:00
&& ! \in_array ( $completionInput -> getCompletionValue (), $command -> getAliases (), true )
2022-03-14 16:22:30 -04:00
) {
$this -> log ( ' No command found, completing using the Application class.' );
// expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
2023-02-24 06:26:40 -05:00
$suggestions -> suggestValues ( array_filter ( array_merge ([ $command -> getName ()], $command -> getAliases ())));
2022-03-14 16:22:30 -04:00
} else {
$command -> mergeApplicationDefinition ();
$completionInput -> bind ( $command -> getDefinition ());
if ( CompletionInput :: TYPE_OPTION_NAME === $completionInput -> getCompletionType ()) {
$this -> log ( ' Completing option names for the <comment>' . \get_class ( $command instanceof LazyCommand ? $command -> getCommand () : $command ) . '</> command.' );
$suggestions -> suggestOptions ( $command -> getDefinition () -> getOptions ());
} else {
$this -> log ([
' Completing using the <comment>' . \get_class ( $command instanceof LazyCommand ? $command -> getCommand () : $command ) . '</> class.' ,
' Completing <comment>' . $completionInput -> getCompletionType () . '</> for <comment>' . $completionInput -> getCompletionName () . '</>' ,
]);
if ( null !== $compval = $completionInput -> getCompletionValue ()) {
$this -> log ( ' Current value: <comment>' . $compval . '</>' );
}
$command -> complete ( $completionInput , $suggestions );
}
}
/** @var CompletionOutputInterface $completionOutput */
$completionOutput = new $completionOutput ();
$this -> log ( '<info>Suggestions:</>' );
if ( $options = $suggestions -> getOptionSuggestions ()) {
$this -> log ( ' --' . implode ( ' --' , array_map ( function ( $o ) { return $o -> getName (); }, $options )));
} elseif ( $values = $suggestions -> getValueSuggestions ()) {
$this -> log ( ' ' . implode ( ' ' , $values ));
} else {
$this -> log ( ' <comment>No suggestions were provided</>' );
}
$completionOutput -> write ( $suggestions , $output );
} catch ( \Throwable $e ) {
$this -> log ([
'<error>Error!</error>' ,
( string ) $e ,
]);
if ( $output -> isDebug ()) {
throw $e ;
}
2023-05-29 11:13:32 -04:00
return 2 ;
2022-03-14 16:22:30 -04:00
}
2023-05-29 11:13:32 -04:00
return 0 ;
2022-03-14 16:22:30 -04:00
}
private function createCompletionInput ( InputInterface $input ) : CompletionInput
{
$currentIndex = $input -> getOption ( 'current' );
if ( ! $currentIndex || ! ctype_digit ( $currentIndex )) {
throw new \RuntimeException ( 'The "--current" option must be set and it must be an integer.' );
}
$completionInput = CompletionInput :: fromTokens ( $input -> getOption ( 'input' ), ( int ) $currentIndex );
try {
$completionInput -> bind ( $this -> getApplication () -> getDefinition ());
2023-02-24 06:26:40 -05:00
} catch ( ExceptionInterface ) {
2022-03-14 16:22:30 -04:00
}
return $completionInput ;
}
private function findCommand ( CompletionInput $completionInput , OutputInterface $output ) : ? Command
{
try {
$inputName = $completionInput -> getFirstArgument ();
if ( null === $inputName ) {
return null ;
}
return $this -> getApplication () -> find ( $inputName );
2023-02-24 06:26:40 -05:00
} catch ( CommandNotFoundException ) {
2022-03-14 16:22:30 -04:00
}
return null ;
}
private function log ( $messages ) : void
{
if ( ! $this -> isDebug ) {
return ;
}
$commandName = basename ( $_SERVER [ 'argv' ][ 0 ]);
file_put_contents ( sys_get_temp_dir () . '/sf_' . $commandName . '.log' , implode ( \PHP_EOL , ( array ) $messages ) . \PHP_EOL , \FILE_APPEND );
}
}