Ввод консоли (аргументы и опции)

Дата обновления перевода 2023-01-19

Ввод консоли (аргументы и опции)

Наиболее интересной частью команд являются аргументы и опции, которые вы можете сделать доступными. Эти аргументы и опции позволяют вам передавать динамическую информацию из терминала в команду.

Использование аргументов команды

Аргументы - это строки, разделённые пробелами, которые идут после самого имени команды. Они упорядочены и могут быть обязательными или необязательными. Например, чтобы добавить необязательный аргумент last_name в команду и сделать аргумент name обязательным:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;

class GreetCommand extends Command
{
    // ...

    protected function configure(): void
    {
        $this
            // ...
            ->addArgument('name', InputArgument::REQUIRED, 'Who do you want to greet?')
            ->addArgument('last_name', InputArgument::OPTIONAL, 'Your last name?')
        ;
    }
}

Теперь у вас есть доступ к аргументу last_name в вашей команде:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends Command
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $text = 'Hi '.$input->getArgument('name');

        $lastName = $input->getArgument('last_name');
        if ($lastName) {
            $text .= ' '.$lastName;
        }

        $output->writeln($text.'!');

        return Command::SUCCESS;
    }
}

Теперь команда может быть использована любым из следующих способов:

1
2
3
4
5
$ php bin/console app:greet Fabien
Привет, Фабиен!

$ php bin/console app:greet Fabien Potencier
Привет, Фабиен Потенсье!

Также возможно позволить аргументу использовать список значений (представьте, что вы хотите поздороваться со всеми вашими друзьями). Только последний аргумент может быть списком:

1
2
3
4
5
6
7
$this
    // ...
    ->addArgument(
        'names',
        InputArgument::IS_ARRAY,
        'С кем вы хотите поздороваться (разделите несколько имён пробелом)?'
    );

Чтобы использовать это, просто укажите столько имён, сколько хотите:

1
$ php bin/console app:greet Fabien Ryan Bernhard

Вы можете получить доступ к аргументу names, как к массиву:

1
2
3
4
$names = $input->getArgument('names');
if (count($names) > 0) {
    $text .= ' '.implode(', ', $names);
}

Существует три варианта аргументов, которые вы можете использовать:

InputArgument::REQUIRED
Аргумент обязателен. Команда не будет выполнена, если этого аргумента нет;
InputArgument::OPTIONAL
Аргумент необязателен и поэтому может быть опущен. Это поведение аргументов по уполчанию;
InputArgument::IS_ARRAY
Аргумент может содержать любое количество значений. По этой причине, он может быть использован в конце списка аргументов.

Вы можете комбинировать IS_ARRAY с REQUIRED и OPTIONAL таким образом:

1
2
3
4
5
6
7
$this
    // ...
    ->addArgument(
        'names',
        InputArgument::IS_ARRAY | InputArgument::REQUIRED,
        'С кем вы хотите поздороваться (разделите несколько имён пробелом)?'
    );

Использование опций команды

В отличие от аргументов, опции не упорядочены (то есть вы можете указывать их в любом порядке) и указываются с двумя дефисами (например, --yell). Опции всегда необязательны, и могут быть настроены так, чтобы принимать значение (например, --dir=src) или просто как булев флажок без значения (например, --yell).

К примеру, добавьте новую опцию к команде, которая может быть использована для указания того, сколько раз подряд должно быть напечатано сообщение:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...
use Symfony\Component\Console\Input\InputOption;

$this
    // ...
    ->addOption(
        // это имя, которое должны ввести пользователи, чтобы передать эту опцию (например, --iterations=5)
        'iterations',
        // это необязательное сокращение имени опции, которое обычно является просто буквой
        // (например, `i`, чтобы пользователи передавали её как `-i`); используйте это для часто используемых опций
        // или опций с длинными именами
        null,
        // это тип опции (например, требует значения, может быть передана больше одного раза и т.д.)
        InputOption::VALUE_REQUIRED,
        // описание опции, отображённое при демонстрации помощи команды
        'How many times should the message be printed?',
        // значение опции по умолчанию (для тех, которые разрешают передачу значений)
        1
    )
;

Далее, используйте это в команде, чтобы напечатать сообщение несколько раз:

1
2
3
for ($i = 0; $i < $input->getOption('iterations'); $i++) {
    $output->writeln($text);
}

Теперь, когда вы выполняете команду, вы можете по желанию указать флажок --iterations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# --iterations не предоставлены, используется значение по умолчанию (1)
$ php bin/console app:greet Fabien
Привет, Фабиен!

$ php bin/console app:greet Fabien --iterations=5
Привет, Фабиен
Привет, Фабиен
Привет, Фабиен
Привет, Фабиен
Привет, Фабиен
Привет, Фабиен

# порядок опций неважен
$ php bin/console app:greet Fabien --iterations=5 --yell
$ php bin/console app:greet Fabien --yell --iterations=5
$ php bin/console app:greet --yell --iterations=5 Fabien

Tip

Вы также можете объявить шорткат из одной буквы, который вы можете вызвать с помощью одного дефиса, например, -i:

1
2
3
4
5
6
7
8
9
$this
    // ...
    ->addOption(
        'iterations',
        'i',
        InputOption::VALUE_REQUIRED,
        'Сколько раз должно быть напечатано сообщение?',
        1
    );

Отметьте, что в соответствии с требованиями стандарта docopt, длинные опции могут указывать свои значения после пробела или символа = (например, --iterations 5 или --iterations=5), а короткие опции могут использовать только пробелы или вообще никакого разделения (например, -i 5 или -i5).

Caution

Хотя возможно отделить опцию от её значения с помощью пробела, использование этой формы приводит к двусмысленности, если опция появляется до имени команды. Например, php bin/console --iterations 5 app:greet Fabien - двусмысленно; Symfony посчитает 5 именем команды. Чтобы избежать такой ситуации, всегда помещайте опции после имени команды или избегайте использования пробелов для разделения имени опции и её значения.

Существует пять вариантов опций, которые вы можете использовать:

InputOption::VALUE_IS_ARRAY
Эта опция принимает несколько значений (например, --dir=/foo --dir=/bar);
InputOption::VALUE_NONE
Не принимает ввод для этой опции (например, --yell). Возвращённое значение будет булевым (false, если опция не предоставлена). Это поведение аргументов по умолчанию;
InputOption::VALUE_REQUIRED
Это значение обязательно (например, --iterations=5), но сама опция всё ещё необязательна;
InputOption::VALUE_OPTIONAL
Эта опция может иметь или не иметь значения (например, --yell или --yell=loud).
InputOption::VALUE_NEGATABLE
Принимает либо флажок (например, --yell) или его отрицание (например, --no-yell).

Вы можете комбинировать VALUE_IS_ARRAY с VALUE_REQUIRED или VALUE_OPTIONAL таким образом:

1
2
3
4
5
6
7
8
9
$this
    // ...
    ->addOption(
        'colors',
        null,
        InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
        'Какие цвета вам нравятся?',
        array('blue', 'red')
    );

Опции с опциональными аргументами

Ничего не запрещает вам создать команду с опцией, которая опционально принимает значение, но это немного сложно. Рассмотрите этот пример:

1
2
3
4
5
6
7
8
9
10
11
12
// ...
use Symfony\Component\Console\Input\InputOption;

$this
    // ...
    ->addOption(
        'yell',
        null,
        InputOption::VALUE_OPTIONAL,
        'Should I yell while greeting?'
    )
;

Эта опция может быть использована 3 способами: greet --yell, greet --yell=louder и greet. Однако, сложно различить передачу опции без значения (greet --yell) и отсутствие передачи опции (greet).

Чтобы решить эту проблему, вам нужно установить значение опции по умолчанию как false:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
use Symfony\Component\Console\Input\InputOption;

$this
    // ...
    ->addOption(
        'yell',
        null,
        InputOption::VALUE_OPTIONAL,
        'Should I yell while greeting?',
        false // это значение по умолчанию вместо null
    )
;

Теперь возможно различать между отсутствием передачи опции и отсутствием передачи значения опции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$optionValue = $input->getOption('yell');
if (false === $optionValue) {
    // в этом случае, опция не была передана при выполнении команды
    $yell = false;
    $yellLouder = false;
} elseif (null === $optionValue) {
    // в этом случае, опция была передана при выполнении команды,
    // но ей не было дано значение
    $yell = true;
    $yellLouder = false;
} else {
    // в этом случае, опция была передана при выполнении команды и
    // ей было присвоено какое-то конкретное значение
    $yell = true;
    if ('louder' === $optionValue) {
        $yellLouder = true;
    } else {
        $yellLouder = false;
    }
}

Код выше можно упростить следующим образом, потому как false !== null:

1
2
3
$optionValue = $input->getOption('yell');
$yell = ($optionValue !== false);
$yellLouder = ($optionValue === 'louder');

Добавление заполнения значения аргумента/опции

Если установлено заполнение Console , имена команды и опции будут автозаполнены оболочкой. Однако, вы также можете реализовать заполнение значения для ввода в ваших командах. Например, вы можете захотеть заполнить все имена пользователей из базы данных в аргументе name вашей команды приветствия.

Чтобы достичь этого, используйте 5ый аргумент addArgument()/ addOption:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ...
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;

class GreetCommand extends Command
{
    // ...
    protected function configure(): void
    {
        $this
            ->addArgument(
                'names',
                InputArgument::IS_ARRAY,
                'Who do you want to greet (separate multiple names with a space)?',
                null,
                function (CompletionInput $input) {
                    // значение пользователя уже введено, например, при вводе "app:greet Fa" ранее
                    // pressing Tab, this will contain "Fa"
                    $currentValue = $input->getCompletionValue();

                    // получить список имен пользователей откуда-то (например, из базы данных)
                    // вы можете использовать $currentValue, чтобы отфильтровать имена
                    $availableUsernames = ...;

                    // затем предложенные имена пользователей в качестве значений
                    return $availableUsernames;
                }
            )
        ;
    }
}

6.1

Аргумент к addOption()/ addArgument() был представлен в Symfony 6.1. До этой версии вам нужно было переопределять метод команды complete().

Это всё, что вам нужно! Предполагая, что пользователи "Fabien" и "Fabrice" существуют, нажатие таба после ввода app:greet Fa даст вам эти имена в качестве предложения.

Tip

Скрипт оболочки может обрабатывать огромные количества предложений будет автоматически фильтровать предложенные значения, основываясь на существующем вводе пользователя. Вам не нужно реализовывать никакую логику фильтрации в команде.

Вы можете использовать CompletionInput::getCompletionValue(), чтобы получить текущий ввод, если это помогает улучшить производительность (например, уменьшив количество рядов, полученных из базы данных).

Тестирование скрипта заполнения

Компонент Console поставляется со специальным классом CommandCompletionTester, который помогает вам модульно тестировать логику заполнения:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
use Symfony\Component\Console\Application;

class GreetCommandTest extends TestCase
{
    public function testComplete()
    {
        $application = new Application();
        $application->add(new GreetCommand());

        // создать новый тестер с командой приветствия
        $tester = new CommandCompletionTester($application->get('app:greet'));

        // заполнить ввод без существующего ввода (пустая строка представляет позицию
        // курсора)
        $suggestions = $tester->complete(['']);
        $this->assertSame(['Fabien', 'Fabrice', 'Wouter'], $suggestions);

        // заполнить ввод с "Fa" в качестве ввода
        $suggestions = $tester->complete(['Fa']);
        $this->assertSame(['Fabien', 'Fabrice'], $suggestions);
    }
}