2021-08-27 06:46:27 -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 ;
use Symfony\Component\Console\Command\Command ;
2022-03-14 16:22:30 -04:00
use Symfony\Component\Console\Command\CompleteCommand ;
use Symfony\Component\Console\Command\DumpCompletionCommand ;
2021-08-27 06:46:27 -04:00
use Symfony\Component\Console\Command\HelpCommand ;
use Symfony\Component\Console\Command\LazyCommand ;
use Symfony\Component\Console\Command\ListCommand ;
use Symfony\Component\Console\Command\SignalableCommandInterface ;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface ;
2022-03-14 16:22:30 -04:00
use Symfony\Component\Console\Completion\CompletionInput ;
use Symfony\Component\Console\Completion\CompletionSuggestions ;
2023-02-24 06:26:40 -05:00
use Symfony\Component\Console\Completion\Suggestion ;
2021-08-27 06:46:27 -04:00
use Symfony\Component\Console\Event\ConsoleCommandEvent ;
use Symfony\Component\Console\Event\ConsoleErrorEvent ;
use Symfony\Component\Console\Event\ConsoleSignalEvent ;
use Symfony\Component\Console\Event\ConsoleTerminateEvent ;
use Symfony\Component\Console\Exception\CommandNotFoundException ;
use Symfony\Component\Console\Exception\ExceptionInterface ;
use Symfony\Component\Console\Exception\LogicException ;
use Symfony\Component\Console\Exception\NamespaceNotFoundException ;
use Symfony\Component\Console\Exception\RuntimeException ;
use Symfony\Component\Console\Formatter\OutputFormatter ;
use Symfony\Component\Console\Helper\DebugFormatterHelper ;
2023-02-24 06:26:40 -05:00
use Symfony\Component\Console\Helper\DescriptorHelper ;
2021-08-27 06:46:27 -04:00
use Symfony\Component\Console\Helper\FormatterHelper ;
use Symfony\Component\Console\Helper\Helper ;
use Symfony\Component\Console\Helper\HelperSet ;
use Symfony\Component\Console\Helper\ProcessHelper ;
use Symfony\Component\Console\Helper\QuestionHelper ;
use Symfony\Component\Console\Input\ArgvInput ;
use Symfony\Component\Console\Input\ArrayInput ;
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputAwareInterface ;
use Symfony\Component\Console\Input\InputDefinition ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\ConsoleOutput ;
use Symfony\Component\Console\Output\ConsoleOutputInterface ;
use Symfony\Component\Console\Output\OutputInterface ;
use Symfony\Component\Console\SignalRegistry\SignalRegistry ;
use Symfony\Component\Console\Style\SymfonyStyle ;
use Symfony\Component\ErrorHandler\ErrorHandler ;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface ;
use Symfony\Contracts\Service\ResetInterface ;
/**
* An Application is the container for a collection of commands .
*
* It is the main entry point of a Console application .
*
* This class is optimized for a standard CLI environment .
*
* Usage :
*
* $app = new Application ( 'myapp' , '1.0 (stable)' );
* $app -> add ( new SimpleCommand ());
* $app -> run ();
*
* @ author Fabien Potencier < fabien @ symfony . com >
*/
class Application implements ResetInterface
{
2022-03-14 16:22:30 -04:00
private array $commands = [];
private bool $wantHelps = false ;
2023-02-24 06:26:40 -05:00
private ? Command $runningCommand = null ;
2022-03-14 16:22:30 -04:00
private string $name ;
private string $version ;
2023-02-24 06:26:40 -05:00
private ? CommandLoaderInterface $commandLoader = null ;
2022-03-14 16:22:30 -04:00
private bool $catchExceptions = true ;
private bool $autoExit = true ;
2023-02-24 06:26:40 -05:00
private InputDefinition $definition ;
private HelperSet $helperSet ;
private ? EventDispatcherInterface $dispatcher = null ;
private Terminal $terminal ;
2022-03-14 16:22:30 -04:00
private string $defaultCommand ;
private bool $singleCommand = false ;
private bool $initialized = false ;
2023-05-29 11:13:32 -04:00
private ? SignalRegistry $signalRegistry = null ;
2022-03-14 16:22:30 -04:00
private array $signalsToDispatchEvent = [];
2021-08-27 06:46:27 -04:00
public function __construct ( string $name = 'UNKNOWN' , string $version = 'UNKNOWN' )
{
$this -> name = $name ;
$this -> version = $version ;
$this -> terminal = new Terminal ();
$this -> defaultCommand = 'list' ;
if ( \defined ( 'SIGINT' ) && SignalRegistry :: isSupported ()) {
$this -> signalRegistry = new SignalRegistry ();
$this -> signalsToDispatchEvent = [ \SIGINT , \SIGTERM , \SIGUSR1 , \SIGUSR2 ];
}
}
/**
* @ final
*/
public function setDispatcher ( EventDispatcherInterface $dispatcher )
{
$this -> dispatcher = $dispatcher ;
}
public function setCommandLoader ( CommandLoaderInterface $commandLoader )
{
$this -> commandLoader = $commandLoader ;
}
public function getSignalRegistry () : SignalRegistry
{
if ( ! $this -> signalRegistry ) {
throw new RuntimeException ( 'Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.' );
}
return $this -> signalRegistry ;
}
public function setSignalsToDispatchEvent ( int ... $signalsToDispatchEvent )
{
$this -> signalsToDispatchEvent = $signalsToDispatchEvent ;
}
/**
* Runs the current application .
*
* @ return int 0 if everything went fine , or an error code
*
* @ throws \Exception When running fails . Bypass this when { @ link setCatchExceptions ()} .
*/
2022-03-14 16:22:30 -04:00
public function run ( InputInterface $input = null , OutputInterface $output = null ) : int
2021-08-27 06:46:27 -04:00
{
if ( \function_exists ( 'putenv' )) {
@ putenv ( 'LINES=' . $this -> terminal -> getHeight ());
@ putenv ( 'COLUMNS=' . $this -> terminal -> getWidth ());
}
2023-02-24 06:26:40 -05:00
$input ? ? = new ArgvInput ();
$output ? ? = new ConsoleOutput ();
2021-08-27 06:46:27 -04:00
$renderException = function ( \Throwable $e ) use ( $output ) {
if ( $output instanceof ConsoleOutputInterface ) {
$this -> renderThrowable ( $e , $output -> getErrorOutput ());
} else {
$this -> renderThrowable ( $e , $output );
}
};
if ( $phpHandler = set_exception_handler ( $renderException )) {
restore_exception_handler ();
if ( ! \is_array ( $phpHandler ) || ! $phpHandler [ 0 ] instanceof ErrorHandler ) {
$errorHandler = true ;
} elseif ( $errorHandler = $phpHandler [ 0 ] -> setExceptionHandler ( $renderException )) {
$phpHandler [ 0 ] -> setExceptionHandler ( $errorHandler );
}
}
$this -> configureIO ( $input , $output );
try {
$exitCode = $this -> doRun ( $input , $output );
} catch ( \Exception $e ) {
if ( ! $this -> catchExceptions ) {
throw $e ;
}
$renderException ( $e );
$exitCode = $e -> getCode ();
if ( is_numeric ( $exitCode )) {
$exitCode = ( int ) $exitCode ;
2023-02-24 06:26:40 -05:00
if ( $exitCode <= 0 ) {
2021-08-27 06:46:27 -04:00
$exitCode = 1 ;
}
} else {
$exitCode = 1 ;
}
} finally {
// if the exception handler changed, keep it
// otherwise, unregister $renderException
if ( ! $phpHandler ) {
if ( set_exception_handler ( $renderException ) === $renderException ) {
restore_exception_handler ();
}
restore_exception_handler ();
} elseif ( ! $errorHandler ) {
$finalHandler = $phpHandler [ 0 ] -> setExceptionHandler ( null );
if ( $finalHandler !== $renderException ) {
$phpHandler [ 0 ] -> setExceptionHandler ( $finalHandler );
}
}
}
if ( $this -> autoExit ) {
if ( $exitCode > 255 ) {
$exitCode = 255 ;
}
exit ( $exitCode );
}
return $exitCode ;
}
/**
* Runs the current application .
*
* @ return int 0 if everything went fine , or an error code
*/
public function doRun ( InputInterface $input , OutputInterface $output )
{
if ( true === $input -> hasParameterOption ([ '--version' , '-V' ], true )) {
$output -> writeln ( $this -> getLongVersion ());
return 0 ;
}
try {
// Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
$input -> bind ( $this -> getDefinition ());
2023-02-24 06:26:40 -05:00
} catch ( ExceptionInterface ) {
2021-08-27 06:46:27 -04:00
// Errors must be ignored, full binding/validation happens later when the command is known.
}
$name = $this -> getCommandName ( $input );
if ( true === $input -> hasParameterOption ([ '--help' , '-h' ], true )) {
if ( ! $name ) {
$name = 'help' ;
$input = new ArrayInput ([ 'command_name' => $this -> defaultCommand ]);
} else {
$this -> wantHelps = true ;
}
}
if ( ! $name ) {
$name = $this -> defaultCommand ;
$definition = $this -> getDefinition ();
$definition -> setArguments ( array_merge (
$definition -> getArguments (),
[
'command' => new InputArgument ( 'command' , InputArgument :: OPTIONAL , $definition -> getArgument ( 'command' ) -> getDescription (), $name ),
]
));
}
try {
$this -> runningCommand = null ;
// the command name MUST be the first element of the input
$command = $this -> find ( $name );
} catch ( \Throwable $e ) {
2023-02-24 06:26:40 -05:00
if (( $e instanceof CommandNotFoundException && ! $e instanceof NamespaceNotFoundException ) && 1 === \count ( $alternatives = $e -> getAlternatives ()) && $input -> isInteractive ()) {
$alternative = $alternatives [ 0 ];
$style = new SymfonyStyle ( $input , $output );
$output -> writeln ( '' );
$formattedBlock = ( new FormatterHelper ()) -> formatBlock ( sprintf ( 'Command "%s" is not defined.' , $name ), 'error' , true );
$output -> writeln ( $formattedBlock );
if ( ! $style -> confirm ( sprintf ( 'Do you want to run "%s" instead? ' , $alternative ), false )) {
if ( null !== $this -> dispatcher ) {
$event = new ConsoleErrorEvent ( $input , $output , $e );
$this -> dispatcher -> dispatch ( $event , ConsoleEvents :: ERROR );
return $event -> getExitCode ();
}
return 1 ;
}
$command = $this -> find ( $alternative );
} else {
2021-08-27 06:46:27 -04:00
if ( null !== $this -> dispatcher ) {
$event = new ConsoleErrorEvent ( $input , $output , $e );
$this -> dispatcher -> dispatch ( $event , ConsoleEvents :: ERROR );
if ( 0 === $event -> getExitCode ()) {
return 0 ;
}
$e = $event -> getError ();
}
2023-02-24 06:26:40 -05:00
try {
if ( $e instanceof CommandNotFoundException && $namespace = $this -> findNamespace ( $name )) {
$helper = new DescriptorHelper ();
$helper -> describe ( $output instanceof ConsoleOutputInterface ? $output -> getErrorOutput () : $output , $this , [
'format' => 'txt' ,
'raw_text' => false ,
'namespace' => $namespace ,
'short' => false ,
]);
return isset ( $event ) ? $event -> getExitCode () : 1 ;
}
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
throw $e ;
} catch ( NamespaceNotFoundException ) {
throw $e ;
2021-08-27 06:46:27 -04:00
}
}
}
if ( $command instanceof LazyCommand ) {
$command = $command -> getCommand ();
}
$this -> runningCommand = $command ;
$exitCode = $this -> doRunCommand ( $command , $input , $output );
$this -> runningCommand = null ;
return $exitCode ;
}
public function reset ()
{
}
public function setHelperSet ( HelperSet $helperSet )
{
$this -> helperSet = $helperSet ;
}
/**
* Get the helper set associated with the command .
*/
2022-03-14 16:22:30 -04:00
public function getHelperSet () : HelperSet
2021-08-27 06:46:27 -04:00
{
2022-03-14 16:22:30 -04:00
return $this -> helperSet ? ? = $this -> getDefaultHelperSet ();
2021-08-27 06:46:27 -04:00
}
public function setDefinition ( InputDefinition $definition )
{
$this -> definition = $definition ;
}
/**
* Gets the InputDefinition related to this Application .
*/
2022-03-14 16:22:30 -04:00
public function getDefinition () : InputDefinition
2021-08-27 06:46:27 -04:00
{
2022-03-14 16:22:30 -04:00
$this -> definition ? ? = $this -> getDefaultInputDefinition ();
2021-08-27 06:46:27 -04:00
if ( $this -> singleCommand ) {
$inputDefinition = $this -> definition ;
$inputDefinition -> setArguments ();
return $inputDefinition ;
}
return $this -> definition ;
}
2022-03-14 16:22:30 -04:00
/**
* Adds suggestions to $suggestions for the current completion input ( e . g . option or argument ) .
*/
public function complete ( CompletionInput $input , CompletionSuggestions $suggestions ) : void
{
if (
CompletionInput :: TYPE_ARGUMENT_VALUE === $input -> getCompletionType ()
&& 'command' === $input -> getCompletionName ()
) {
2023-02-24 06:26:40 -05:00
foreach ( $this -> all () as $name => $command ) {
// skip hidden commands and aliased commands as they already get added below
if ( $command -> isHidden () || $command -> getName () !== $name ) {
continue ;
}
$suggestions -> suggestValue ( new Suggestion ( $command -> getName (), $command -> getDescription ()));
foreach ( $command -> getAliases () as $name ) {
$suggestions -> suggestValue ( new Suggestion ( $name , $command -> getDescription ()));
}
}
2022-03-14 16:22:30 -04:00
return ;
}
if ( CompletionInput :: TYPE_OPTION_NAME === $input -> getCompletionType ()) {
$suggestions -> suggestOptions ( $this -> getDefinition () -> getOptions ());
return ;
}
}
2021-08-27 06:46:27 -04:00
/**
* Gets the help message .
*/
2022-03-14 16:22:30 -04:00
public function getHelp () : string
2021-08-27 06:46:27 -04:00
{
return $this -> getLongVersion ();
}
/**
* Gets whether to catch exceptions or not during commands execution .
*/
2022-03-14 16:22:30 -04:00
public function areExceptionsCaught () : bool
2021-08-27 06:46:27 -04:00
{
return $this -> catchExceptions ;
}
/**
* Sets whether to catch exceptions or not during commands execution .
*/
public function setCatchExceptions ( bool $boolean )
{
$this -> catchExceptions = $boolean ;
}
/**
* Gets whether to automatically exit after a command execution or not .
*/
2022-03-14 16:22:30 -04:00
public function isAutoExitEnabled () : bool
2021-08-27 06:46:27 -04:00
{
return $this -> autoExit ;
}
/**
* Sets whether to automatically exit after a command execution or not .
*/
public function setAutoExit ( bool $boolean )
{
$this -> autoExit = $boolean ;
}
/**
* Gets the name of the application .
*/
2022-03-14 16:22:30 -04:00
public function getName () : string
2021-08-27 06:46:27 -04:00
{
return $this -> name ;
}
/**
* Sets the application name .
**/
public function setName ( string $name )
{
$this -> name = $name ;
}
/**
* Gets the application version .
*/
2022-03-14 16:22:30 -04:00
public function getVersion () : string
2021-08-27 06:46:27 -04:00
{
return $this -> version ;
}
/**
* Sets the application version .
*/
public function setVersion ( string $version )
{
$this -> version = $version ;
}
/**
* Returns the long version of the application .
*
2022-03-14 16:22:30 -04:00
* @ return string
2021-08-27 06:46:27 -04:00
*/
public function getLongVersion ()
{
if ( 'UNKNOWN' !== $this -> getName ()) {
if ( 'UNKNOWN' !== $this -> getVersion ()) {
return sprintf ( '%s <info>%s</info>' , $this -> getName (), $this -> getVersion ());
}
return $this -> getName ();
}
return 'Console Tool' ;
}
/**
* Registers a new command .
*/
2022-03-14 16:22:30 -04:00
public function register ( string $name ) : Command
2021-08-27 06:46:27 -04:00
{
return $this -> add ( new Command ( $name ));
}
/**
* Adds an array of command objects .
*
* If a Command is not enabled it will not be added .
*
* @ param Command [] $commands An array of commands
*/
public function addCommands ( array $commands )
{
foreach ( $commands as $command ) {
$this -> add ( $command );
}
}
/**
* Adds a command object .
*
* If a command with the same name already exists , it will be overridden .
* If the command is not enabled it will not be added .
*
2022-03-14 16:22:30 -04:00
* @ return Command | null
2021-08-27 06:46:27 -04:00
*/
public function add ( Command $command )
{
$this -> init ();
$command -> setApplication ( $this );
if ( ! $command -> isEnabled ()) {
$command -> setApplication ( null );
return null ;
}
if ( ! $command instanceof LazyCommand ) {
// Will throw if the command is not correctly initialized.
$command -> getDefinition ();
}
if ( ! $command -> getName ()) {
throw new LogicException ( sprintf ( 'The command defined in "%s" cannot have an empty name.' , get_debug_type ( $command )));
}
$this -> commands [ $command -> getName ()] = $command ;
foreach ( $command -> getAliases () as $alias ) {
$this -> commands [ $alias ] = $command ;
}
return $command ;
}
/**
* Returns a registered command by name or alias .
*
2022-03-14 16:22:30 -04:00
* @ return Command
2021-08-27 06:46:27 -04:00
*
* @ throws CommandNotFoundException When given command name does not exist
*/
public function get ( string $name )
{
$this -> init ();
if ( ! $this -> has ( $name )) {
throw new CommandNotFoundException ( sprintf ( 'The command "%s" does not exist.' , $name ));
}
// When the command has a different name than the one used at the command loader level
if ( ! isset ( $this -> commands [ $name ])) {
throw new CommandNotFoundException ( sprintf ( 'The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".' , $name ));
}
$command = $this -> commands [ $name ];
if ( $this -> wantHelps ) {
$this -> wantHelps = false ;
$helpCommand = $this -> get ( 'help' );
$helpCommand -> setCommand ( $command );
return $helpCommand ;
}
return $command ;
}
/**
* Returns true if the command exists , false otherwise .
*/
2022-03-14 16:22:30 -04:00
public function has ( string $name ) : bool
2021-08-27 06:46:27 -04:00
{
$this -> init ();
2023-02-24 06:26:40 -05:00
return isset ( $this -> commands [ $name ]) || ( $this -> commandLoader ? -> has ( $name ) && $this -> add ( $this -> commandLoader -> get ( $name )));
2021-08-27 06:46:27 -04:00
}
/**
* Returns an array of all unique namespaces used by currently registered commands .
*
* It does not return the global namespace which always exists .
*
2022-03-14 16:22:30 -04:00
* @ return string []
2021-08-27 06:46:27 -04:00
*/
2022-03-14 16:22:30 -04:00
public function getNamespaces () : array
2021-08-27 06:46:27 -04:00
{
$namespaces = [];
foreach ( $this -> all () as $command ) {
if ( $command -> isHidden ()) {
continue ;
}
2022-03-14 16:22:30 -04:00
$namespaces [] = $this -> extractAllNamespaces ( $command -> getName ());
2021-08-27 06:46:27 -04:00
foreach ( $command -> getAliases () as $alias ) {
2022-03-14 16:22:30 -04:00
$namespaces [] = $this -> extractAllNamespaces ( $alias );
2021-08-27 06:46:27 -04:00
}
}
2022-03-14 16:22:30 -04:00
return array_values ( array_unique ( array_filter ( array_merge ([], ... $namespaces ))));
2021-08-27 06:46:27 -04:00
}
/**
* Finds a registered namespace by a name or an abbreviation .
*
* @ throws NamespaceNotFoundException When namespace is incorrect or ambiguous
*/
2022-03-14 16:22:30 -04:00
public function findNamespace ( string $namespace ) : string
2021-08-27 06:46:27 -04:00
{
$allNamespaces = $this -> getNamespaces ();
$expr = implode ( '[^:]*:' , array_map ( 'preg_quote' , explode ( ':' , $namespace ))) . '[^:]*' ;
$namespaces = preg_grep ( '{^' . $expr . '}' , $allNamespaces );
if ( empty ( $namespaces )) {
$message = sprintf ( 'There are no commands defined in the "%s" namespace.' , $namespace );
if ( $alternatives = $this -> findAlternatives ( $namespace , $allNamespaces )) {
if ( 1 == \count ( $alternatives )) {
$message .= " \n \n Did you mean this? \n " ;
} else {
$message .= " \n \n Did you mean one of these? \n " ;
}
$message .= implode ( " \n " , $alternatives );
}
throw new NamespaceNotFoundException ( $message , $alternatives );
}
$exact = \in_array ( $namespace , $namespaces , true );
if ( \count ( $namespaces ) > 1 && ! $exact ) {
throw new NamespaceNotFoundException ( sprintf ( " The namespace \" %s \" is ambiguous. \n Did you mean one of these? \n %s. " , $namespace , $this -> getAbbreviationSuggestions ( array_values ( $namespaces ))), array_values ( $namespaces ));
}
return $exact ? $namespace : reset ( $namespaces );
}
/**
* Finds a command by name or alias .
*
* Contrary to get , this command tries to find the best
* match if you give it an abbreviation of a name or alias .
*
2022-03-14 16:22:30 -04:00
* @ return Command
2021-08-27 06:46:27 -04:00
*
* @ throws CommandNotFoundException When command name is incorrect or ambiguous
*/
public function find ( string $name )
{
$this -> init ();
$aliases = [];
foreach ( $this -> commands as $command ) {
foreach ( $command -> getAliases () as $alias ) {
if ( ! $this -> has ( $alias )) {
$this -> commands [ $alias ] = $command ;
}
}
}
if ( $this -> has ( $name )) {
return $this -> get ( $name );
}
$allCommands = $this -> commandLoader ? array_merge ( $this -> commandLoader -> getNames (), array_keys ( $this -> commands )) : array_keys ( $this -> commands );
$expr = implode ( '[^:]*:' , array_map ( 'preg_quote' , explode ( ':' , $name ))) . '[^:]*' ;
$commands = preg_grep ( '{^' . $expr . '}' , $allCommands );
if ( empty ( $commands )) {
$commands = preg_grep ( '{^' . $expr . '}i' , $allCommands );
}
// if no commands matched or we just matched namespaces
if ( empty ( $commands ) || \count ( preg_grep ( '{^' . $expr . '$}i' , $commands )) < 1 ) {
if ( false !== $pos = strrpos ( $name , ':' )) {
// check if a namespace exists and contains commands
$this -> findNamespace ( substr ( $name , 0 , $pos ));
}
$message = sprintf ( 'Command "%s" is not defined.' , $name );
if ( $alternatives = $this -> findAlternatives ( $name , $allCommands )) {
// remove hidden commands
$alternatives = array_filter ( $alternatives , function ( $name ) {
return ! $this -> get ( $name ) -> isHidden ();
});
if ( 1 == \count ( $alternatives )) {
$message .= " \n \n Did you mean this? \n " ;
} else {
$message .= " \n \n Did you mean one of these? \n " ;
}
$message .= implode ( " \n " , $alternatives );
}
throw new CommandNotFoundException ( $message , array_values ( $alternatives ));
}
// filter out aliases for commands which are already on the list
if ( \count ( $commands ) > 1 ) {
$commandList = $this -> commandLoader ? array_merge ( array_flip ( $this -> commandLoader -> getNames ()), $this -> commands ) : $this -> commands ;
$commands = array_unique ( array_filter ( $commands , function ( $nameOrAlias ) use ( & $commandList , $commands , & $aliases ) {
if ( ! $commandList [ $nameOrAlias ] instanceof Command ) {
$commandList [ $nameOrAlias ] = $this -> commandLoader -> get ( $nameOrAlias );
}
$commandName = $commandList [ $nameOrAlias ] -> getName ();
$aliases [ $nameOrAlias ] = $commandName ;
return $commandName === $nameOrAlias || ! \in_array ( $commandName , $commands );
}));
}
if ( \count ( $commands ) > 1 ) {
$usableWidth = $this -> terminal -> getWidth () - 10 ;
$abbrevs = array_values ( $commands );
$maxLen = 0 ;
foreach ( $abbrevs as $abbrev ) {
$maxLen = max ( Helper :: width ( $abbrev ), $maxLen );
}
$abbrevs = array_map ( function ( $cmd ) use ( $commandList , $usableWidth , $maxLen , & $commands ) {
if ( $commandList [ $cmd ] -> isHidden ()) {
unset ( $commands [ array_search ( $cmd , $commands )]);
return false ;
}
$abbrev = str_pad ( $cmd , $maxLen , ' ' ) . ' ' . $commandList [ $cmd ] -> getDescription ();
return Helper :: width ( $abbrev ) > $usableWidth ? Helper :: substr ( $abbrev , 0 , $usableWidth - 3 ) . '...' : $abbrev ;
}, array_values ( $commands ));
if ( \count ( $commands ) > 1 ) {
$suggestions = $this -> getAbbreviationSuggestions ( array_filter ( $abbrevs ));
throw new CommandNotFoundException ( sprintf ( " Command \" %s \" is ambiguous. \n Did you mean one of these? \n %s. " , $name , $suggestions ), array_values ( $commands ));
}
}
$command = $this -> get ( reset ( $commands ));
if ( $command -> isHidden ()) {
throw new CommandNotFoundException ( sprintf ( 'The command "%s" does not exist.' , $name ));
}
return $command ;
}
/**
* Gets the commands ( registered in the given namespace if provided ) .
*
* The array keys are the full names and the values the command instances .
*
2022-03-14 16:22:30 -04:00
* @ return Command []
2021-08-27 06:46:27 -04:00
*/
public function all ( string $namespace = null )
{
$this -> init ();
if ( null === $namespace ) {
if ( ! $this -> commandLoader ) {
return $this -> commands ;
}
$commands = $this -> commands ;
foreach ( $this -> commandLoader -> getNames () as $name ) {
if ( ! isset ( $commands [ $name ]) && $this -> has ( $name )) {
$commands [ $name ] = $this -> get ( $name );
}
}
return $commands ;
}
$commands = [];
foreach ( $this -> commands as $name => $command ) {
if ( $namespace === $this -> extractNamespace ( $name , substr_count ( $namespace , ':' ) + 1 )) {
$commands [ $name ] = $command ;
}
}
if ( $this -> commandLoader ) {
foreach ( $this -> commandLoader -> getNames () as $name ) {
if ( ! isset ( $commands [ $name ]) && $namespace === $this -> extractNamespace ( $name , substr_count ( $namespace , ':' ) + 1 ) && $this -> has ( $name )) {
$commands [ $name ] = $this -> get ( $name );
}
}
}
return $commands ;
}
/**
* Returns an array of possible abbreviations given a set of names .
*
2022-03-14 16:22:30 -04:00
* @ return string [][]
2021-08-27 06:46:27 -04:00
*/
2022-03-14 16:22:30 -04:00
public static function getAbbreviations ( array $names ) : array
2021-08-27 06:46:27 -04:00
{
$abbrevs = [];
foreach ( $names as $name ) {
for ( $len = \strlen ( $name ); $len > 0 ; -- $len ) {
$abbrev = substr ( $name , 0 , $len );
$abbrevs [ $abbrev ][] = $name ;
}
}
return $abbrevs ;
}
public function renderThrowable ( \Throwable $e , OutputInterface $output ) : void
{
$output -> writeln ( '' , OutputInterface :: VERBOSITY_QUIET );
$this -> doRenderThrowable ( $e , $output );
if ( null !== $this -> runningCommand ) {
$output -> writeln ( sprintf ( '<info>%s</info>' , OutputFormatter :: escape ( sprintf ( $this -> runningCommand -> getSynopsis (), $this -> getName ()))), OutputInterface :: VERBOSITY_QUIET );
$output -> writeln ( '' , OutputInterface :: VERBOSITY_QUIET );
}
}
protected function doRenderThrowable ( \Throwable $e , OutputInterface $output ) : void
{
do {
$message = trim ( $e -> getMessage ());
if ( '' === $message || OutputInterface :: VERBOSITY_VERBOSE <= $output -> getVerbosity ()) {
$class = get_debug_type ( $e );
$title = sprintf ( ' [%s%s] ' , $class , 0 !== ( $code = $e -> getCode ()) ? ' (' . $code . ')' : '' );
$len = Helper :: width ( $title );
} else {
$len = 0 ;
}
if ( str_contains ( $message , " @anonymous \0 " )) {
$message = preg_replace_callback ( '/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/' , function ( $m ) {
return class_exists ( $m [ 0 ], false ) ? ( get_parent_class ( $m [ 0 ]) ? : key ( class_implements ( $m [ 0 ])) ? : 'class' ) . '@anonymous' : $m [ 0 ];
}, $message );
}
$width = $this -> terminal -> getWidth () ? $this -> terminal -> getWidth () - 1 : \PHP_INT_MAX ;
$lines = [];
foreach ( '' !== $message ? preg_split ( '/\r?\n/' , $message ) : [] as $line ) {
foreach ( $this -> splitStringByWidth ( $line , $width - 4 ) as $line ) {
// pre-format lines to get the right string length
$lineLength = Helper :: width ( $line ) + 4 ;
$lines [] = [ $line , $lineLength ];
$len = max ( $lineLength , $len );
}
}
$messages = [];
if ( ! $e instanceof ExceptionInterface || OutputInterface :: VERBOSITY_VERBOSE <= $output -> getVerbosity ()) {
$messages [] = sprintf ( '<comment>%s</comment>' , OutputFormatter :: escape ( sprintf ( 'In %s line %s:' , basename ( $e -> getFile ()) ? : 'n/a' , $e -> getLine () ? : 'n/a' )));
}
$messages [] = $emptyLine = sprintf ( '<error>%s</error>' , str_repeat ( ' ' , $len ));
if ( '' === $message || OutputInterface :: VERBOSITY_VERBOSE <= $output -> getVerbosity ()) {
$messages [] = sprintf ( '<error>%s%s</error>' , $title , str_repeat ( ' ' , max ( 0 , $len - Helper :: width ( $title ))));
}
foreach ( $lines as $line ) {
$messages [] = sprintf ( '<error> %s %s</error>' , OutputFormatter :: escape ( $line [ 0 ]), str_repeat ( ' ' , $len - $line [ 1 ]));
}
$messages [] = $emptyLine ;
$messages [] = '' ;
$output -> writeln ( $messages , OutputInterface :: VERBOSITY_QUIET );
if ( OutputInterface :: VERBOSITY_VERBOSE <= $output -> getVerbosity ()) {
$output -> writeln ( '<comment>Exception trace:</comment>' , OutputInterface :: VERBOSITY_QUIET );
// exception related properties
$trace = $e -> getTrace ();
array_unshift ( $trace , [
'function' => '' ,
'file' => $e -> getFile () ? : 'n/a' ,
'line' => $e -> getLine () ? : 'n/a' ,
'args' => [],
]);
for ( $i = 0 , $count = \count ( $trace ); $i < $count ; ++ $i ) {
$class = $trace [ $i ][ 'class' ] ? ? '' ;
$type = $trace [ $i ][ 'type' ] ? ? '' ;
$function = $trace [ $i ][ 'function' ] ? ? '' ;
$file = $trace [ $i ][ 'file' ] ? ? 'n/a' ;
$line = $trace [ $i ][ 'line' ] ? ? 'n/a' ;
$output -> writeln ( sprintf ( ' %s%s at <info>%s:%s</info>' , $class , $function ? $type . $function . '()' : '' , $file , $line ), OutputInterface :: VERBOSITY_QUIET );
}
$output -> writeln ( '' , OutputInterface :: VERBOSITY_QUIET );
}
} while ( $e = $e -> getPrevious ());
}
/**
* Configures the input and output instances based on the user arguments and options .
*/
protected function configureIO ( InputInterface $input , OutputInterface $output )
{
if ( true === $input -> hasParameterOption ([ '--ansi' ], true )) {
$output -> setDecorated ( true );
} elseif ( true === $input -> hasParameterOption ([ '--no-ansi' ], true )) {
$output -> setDecorated ( false );
}
if ( true === $input -> hasParameterOption ([ '--no-interaction' , '-n' ], true )) {
$input -> setInteractive ( false );
}
switch ( $shellVerbosity = ( int ) getenv ( 'SHELL_VERBOSITY' )) {
2023-02-24 06:26:40 -05:00
case - 1 :
$output -> setVerbosity ( OutputInterface :: VERBOSITY_QUIET );
break ;
case 1 :
$output -> setVerbosity ( OutputInterface :: VERBOSITY_VERBOSE );
break ;
case 2 :
$output -> setVerbosity ( OutputInterface :: VERBOSITY_VERY_VERBOSE );
break ;
case 3 :
$output -> setVerbosity ( OutputInterface :: VERBOSITY_DEBUG );
break ;
default :
$shellVerbosity = 0 ;
break ;
2021-08-27 06:46:27 -04:00
}
if ( true === $input -> hasParameterOption ([ '--quiet' , '-q' ], true )) {
$output -> setVerbosity ( OutputInterface :: VERBOSITY_QUIET );
$shellVerbosity = - 1 ;
} else {
if ( $input -> hasParameterOption ( '-vvv' , true ) || $input -> hasParameterOption ( '--verbose=3' , true ) || 3 === $input -> getParameterOption ( '--verbose' , false , true )) {
$output -> setVerbosity ( OutputInterface :: VERBOSITY_DEBUG );
$shellVerbosity = 3 ;
} elseif ( $input -> hasParameterOption ( '-vv' , true ) || $input -> hasParameterOption ( '--verbose=2' , true ) || 2 === $input -> getParameterOption ( '--verbose' , false , true )) {
$output -> setVerbosity ( OutputInterface :: VERBOSITY_VERY_VERBOSE );
$shellVerbosity = 2 ;
} elseif ( $input -> hasParameterOption ( '-v' , true ) || $input -> hasParameterOption ( '--verbose=1' , true ) || $input -> hasParameterOption ( '--verbose' , true ) || $input -> getParameterOption ( '--verbose' , false , true )) {
$output -> setVerbosity ( OutputInterface :: VERBOSITY_VERBOSE );
$shellVerbosity = 1 ;
}
}
if ( - 1 === $shellVerbosity ) {
$input -> setInteractive ( false );
}
if ( \function_exists ( 'putenv' )) {
@ putenv ( 'SHELL_VERBOSITY=' . $shellVerbosity );
}
$_ENV [ 'SHELL_VERBOSITY' ] = $shellVerbosity ;
$_SERVER [ 'SHELL_VERBOSITY' ] = $shellVerbosity ;
}
/**
* Runs the current command .
*
* If an event dispatcher has been attached to the application ,
* events are also dispatched during the life - cycle of the command .
*
* @ return int 0 if everything went fine , or an error code
*/
protected function doRunCommand ( Command $command , InputInterface $input , OutputInterface $output )
{
foreach ( $command -> getHelperSet () as $helper ) {
if ( $helper instanceof InputAwareInterface ) {
$helper -> setInput ( $input );
}
}
2023-02-24 06:26:40 -05:00
if ( $this -> signalsToDispatchEvent ) {
$commandSignals = $command instanceof SignalableCommandInterface ? $command -> getSubscribedSignals () : [];
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
if ( $commandSignals || null !== $this -> dispatcher ) {
if ( ! $this -> signalRegistry ) {
throw new RuntimeException ( 'Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.' );
}
2022-03-14 16:22:30 -04:00
2023-02-24 06:26:40 -05:00
if ( Terminal :: hasSttyAvailable ()) {
$sttyMode = shell_exec ( 'stty -g' );
foreach ([ \SIGINT , \SIGTERM ] as $signal ) {
$this -> signalRegistry -> register ( $signal , static function () use ( $sttyMode ) {
shell_exec ( 'stty ' . $sttyMode );
});
}
2022-03-14 16:22:30 -04:00
}
}
2023-02-24 06:26:40 -05:00
if ( null !== $this -> dispatcher ) {
2021-08-27 06:46:27 -04:00
foreach ( $this -> signalsToDispatchEvent as $signal ) {
$event = new ConsoleSignalEvent ( $command , $input , $output , $signal );
$this -> signalRegistry -> register ( $signal , function ( $signal , $hasNext ) use ( $event ) {
$this -> dispatcher -> dispatch ( $event , ConsoleEvents :: SIGNAL );
// No more handlers, we try to simulate PHP default behavior
if ( ! $hasNext ) {
if ( ! \in_array ( $signal , [ \SIGUSR1 , \SIGUSR2 ], true )) {
exit ( 0 );
}
}
});
}
}
2023-02-24 06:26:40 -05:00
foreach ( $commandSignals as $signal ) {
2021-08-27 06:46:27 -04:00
$this -> signalRegistry -> register ( $signal , [ $command , 'handleSignal' ]);
}
}
if ( null === $this -> dispatcher ) {
return $command -> run ( $input , $output );
}
// bind before the console.command event, so the listeners have access to input options/arguments
try {
$command -> mergeApplicationDefinition ();
$input -> bind ( $command -> getDefinition ());
2023-02-24 06:26:40 -05:00
} catch ( ExceptionInterface ) {
2021-08-27 06:46:27 -04:00
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
}
$event = new ConsoleCommandEvent ( $command , $input , $output );
$e = null ;
try {
$this -> dispatcher -> dispatch ( $event , ConsoleEvents :: COMMAND );
if ( $event -> commandShouldRun ()) {
$exitCode = $command -> run ( $input , $output );
} else {
$exitCode = ConsoleCommandEvent :: RETURN_CODE_DISABLED ;
}
} catch ( \Throwable $e ) {
$event = new ConsoleErrorEvent ( $input , $output , $e , $command );
$this -> dispatcher -> dispatch ( $event , ConsoleEvents :: ERROR );
$e = $event -> getError ();
if ( 0 === $exitCode = $event -> getExitCode ()) {
$e = null ;
}
}
$event = new ConsoleTerminateEvent ( $command , $input , $output , $exitCode );
$this -> dispatcher -> dispatch ( $event , ConsoleEvents :: TERMINATE );
if ( null !== $e ) {
throw $e ;
}
return $event -> getExitCode ();
}
/**
* Gets the name of the command based on input .
*/
2022-03-14 16:22:30 -04:00
protected function getCommandName ( InputInterface $input ) : ? string
2021-08-27 06:46:27 -04:00
{
return $this -> singleCommand ? $this -> defaultCommand : $input -> getFirstArgument ();
}
/**
* Gets the default input definition .
*/
2022-03-14 16:22:30 -04:00
protected function getDefaultInputDefinition () : InputDefinition
2021-08-27 06:46:27 -04:00
{
return new InputDefinition ([
new InputArgument ( 'command' , InputArgument :: REQUIRED , 'The command to execute' ),
new InputOption ( '--help' , '-h' , InputOption :: VALUE_NONE , 'Display help for the given command. When no command is given display help for the <info>' . $this -> defaultCommand . '</info> command' ),
new InputOption ( '--quiet' , '-q' , InputOption :: VALUE_NONE , 'Do not output any message' ),
new InputOption ( '--verbose' , '-v|vv|vvv' , InputOption :: VALUE_NONE , 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug' ),
new InputOption ( '--version' , '-V' , InputOption :: VALUE_NONE , 'Display this application version' ),
2022-03-14 16:22:30 -04:00
new InputOption ( '--ansi' , '' , InputOption :: VALUE_NEGATABLE , 'Force (or disable --no-ansi) ANSI output' , null ),
2021-08-27 06:46:27 -04:00
new InputOption ( '--no-interaction' , '-n' , InputOption :: VALUE_NONE , 'Do not ask any interactive question' ),
]);
}
/**
* Gets the default commands that should always be available .
*
2022-03-14 16:22:30 -04:00
* @ return Command []
2021-08-27 06:46:27 -04:00
*/
2022-03-14 16:22:30 -04:00
protected function getDefaultCommands () : array
2021-08-27 06:46:27 -04:00
{
2022-03-14 16:22:30 -04:00
return [ new HelpCommand (), new ListCommand (), new CompleteCommand (), new DumpCompletionCommand ()];
2021-08-27 06:46:27 -04:00
}
/**
* Gets the default helper set with the helpers that should always be available .
*/
2022-03-14 16:22:30 -04:00
protected function getDefaultHelperSet () : HelperSet
2021-08-27 06:46:27 -04:00
{
return new HelperSet ([
new FormatterHelper (),
new DebugFormatterHelper (),
new ProcessHelper (),
new QuestionHelper (),
]);
}
/**
* Returns abbreviated suggestions in string format .
*/
private function getAbbreviationSuggestions ( array $abbrevs ) : string
{
return ' ' . implode ( " \n " , $abbrevs );
}
/**
* Returns the namespace part of the command name .
*
* This method is not part of public API and should not be used directly .
*/
2022-03-14 16:22:30 -04:00
public function extractNamespace ( string $name , int $limit = null ) : string
2021-08-27 06:46:27 -04:00
{
$parts = explode ( ':' , $name , - 1 );
return implode ( ':' , null === $limit ? $parts : \array_slice ( $parts , 0 , $limit ));
}
/**
* Finds alternative of $name among $collection ,
* if nothing is found in $collection , try in $abbrevs .
*
2022-03-14 16:22:30 -04:00
* @ return string []
2021-08-27 06:46:27 -04:00
*/
private function findAlternatives ( string $name , iterable $collection ) : array
{
$threshold = 1e3 ;
$alternatives = [];
$collectionParts = [];
foreach ( $collection as $item ) {
$collectionParts [ $item ] = explode ( ':' , $item );
}
foreach ( explode ( ':' , $name ) as $i => $subname ) {
foreach ( $collectionParts as $collectionName => $parts ) {
$exists = isset ( $alternatives [ $collectionName ]);
if ( ! isset ( $parts [ $i ]) && $exists ) {
$alternatives [ $collectionName ] += $threshold ;
continue ;
} elseif ( ! isset ( $parts [ $i ])) {
continue ;
}
$lev = levenshtein ( $subname , $parts [ $i ]);
if ( $lev <= \strlen ( $subname ) / 3 || '' !== $subname && str_contains ( $parts [ $i ], $subname )) {
$alternatives [ $collectionName ] = $exists ? $alternatives [ $collectionName ] + $lev : $lev ;
} elseif ( $exists ) {
$alternatives [ $collectionName ] += $threshold ;
}
}
}
foreach ( $collection as $item ) {
$lev = levenshtein ( $name , $item );
if ( $lev <= \strlen ( $name ) / 3 || str_contains ( $item , $name )) {
$alternatives [ $item ] = isset ( $alternatives [ $item ]) ? $alternatives [ $item ] - $lev : $lev ;
}
}
$alternatives = array_filter ( $alternatives , function ( $lev ) use ( $threshold ) { return $lev < 2 * $threshold ; });
ksort ( $alternatives , \SORT_NATURAL | \SORT_FLAG_CASE );
return array_keys ( $alternatives );
}
/**
* Sets the default Command name .
*
2022-03-14 16:22:30 -04:00
* @ return $this
2021-08-27 06:46:27 -04:00
*/
2022-03-14 16:22:30 -04:00
public function setDefaultCommand ( string $commandName , bool $isSingleCommand = false ) : static
2021-08-27 06:46:27 -04:00
{
$this -> defaultCommand = explode ( '|' , ltrim ( $commandName , '|' ))[ 0 ];
if ( $isSingleCommand ) {
// Ensure the command exist
$this -> find ( $commandName );
$this -> singleCommand = true ;
}
return $this ;
}
/**
* @ internal
*/
public function isSingleCommand () : bool
{
return $this -> singleCommand ;
}
private function splitStringByWidth ( string $string , int $width ) : array
{
// str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
// additionally, array_slice() is not enough as some character has doubled width.
// we need a function to split string not by character count but by string width
if ( false === $encoding = mb_detect_encoding ( $string , null , true )) {
return str_split ( $string , $width );
}
$utf8String = mb_convert_encoding ( $string , 'utf8' , $encoding );
$lines = [];
$line = '' ;
$offset = 0 ;
while ( preg_match ( '/.{1,10000}/u' , $utf8String , $m , 0 , $offset )) {
$offset += \strlen ( $m [ 0 ]);
foreach ( preg_split ( '//u' , $m [ 0 ]) as $char ) {
// test if $char could be appended to current line
if ( mb_strwidth ( $line . $char , 'utf8' ) <= $width ) {
$line .= $char ;
continue ;
}
// if not, push current line to array and make new line
$lines [] = str_pad ( $line , $width );
$line = $char ;
}
}
$lines [] = \count ( $lines ) ? str_pad ( $line , $width ) : $line ;
mb_convert_variables ( $encoding , 'utf8' , $lines );
return $lines ;
}
/**
* Returns all namespaces of the command name .
*
2022-03-14 16:22:30 -04:00
* @ return string []
2021-08-27 06:46:27 -04:00
*/
private function extractAllNamespaces ( string $name ) : array
{
// -1 as third argument is needed to skip the command short name when exploding
$parts = explode ( ':' , $name , - 1 );
$namespaces = [];
foreach ( $parts as $part ) {
if ( \count ( $namespaces )) {
$namespaces [] = end ( $namespaces ) . ':' . $part ;
} else {
$namespaces [] = $part ;
}
}
return $namespaces ;
}
private function init ()
{
if ( $this -> initialized ) {
return ;
}
$this -> initialized = true ;
foreach ( $this -> getDefaultCommands () as $command ) {
$this -> add ( $command );
}
}
}