1
0
Fork 0

Add a way to control which scripts get args and where (#12086)

Add support for `@no_additional_args` and `@additional_args` tags inside script handlers.
pull/11942/head
Jordi Boggiano 2024-09-18 14:44:55 +02:00 committed by GitHub
parent 8bc8c4383a
commit 17930441a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 5 deletions

View File

@ -410,6 +410,38 @@ JSON array of commands.
You can also call a shell/bash script, which will have the path to You can also call a shell/bash script, which will have the path to
the PHP executable available in it as a `PHP_BINARY` env var. the PHP executable available in it as a `PHP_BINARY` env var.
## Controlling additional arguments
When running scripts like `composer script-name arg arg2` or `composer script-name -- --option`,
Composer will by default append `arg`, `arg2` and `--option` to the script's command.
If you do not want these args in a given command, you can put `@no_additional_args`
anywhere in it, that will remove the default behavior and that flag will be removed
as well before running the command.
If you want the args to be added somewhere else than at the very end, then you can put
`@additional_args` to be able to choose exactly where they go.
For example running `composer run-commands ARG` with the below config:
```json
{
"scripts": {
"run-commands": [
"echo hello @no_additional_args",
"command-with-args @additional_args && do-something-without-args --here"
]
}
}
```
Would end up executing these commands:
```
echo hello
command-with-args ARG && do-something-without-args --here
```
## Setting environment variables ## Setting environment variables
To set an environment variable in a cross-platform way, you can use `@putenv`: To set an environment variable in a cross-platform way, you can use `@putenv`:

View File

@ -202,7 +202,12 @@ class EventDispatcher
$return = 0; $return = 0;
$this->ensureBinDirIsInPath(); $this->ensureBinDirIsInPath();
$formattedEventNameWithArgs = $event->getName() . ($event->getArguments() !== [] ? ' (' . implode(', ', $event->getArguments()) . ')' : ''); $additionalArgs = $event->getArguments();
if (is_string($callable) && str_contains($callable, '@no_additional_args')) {
$callable = Preg::replace('{ ?@no_additional_args}', '', $callable);
$additionalArgs = [];
}
$formattedEventNameWithArgs = $event->getName() . ($additionalArgs !== [] ? ' (' . implode(', ', $additionalArgs) . ')' : '');
if (!is_string($callable)) { if (!is_string($callable)) {
if (!is_callable($callable)) { if (!is_callable($callable)) {
$className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0]; $className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0];
@ -220,7 +225,12 @@ class EventDispatcher
$scriptName = $script[0]; $scriptName = $script[0];
unset($script[0]); unset($script[0]);
$args = array_merge($script, $event->getArguments()); $index = array_search('@additional_args', $script, true);
if ($index !== false) {
$args = array_splice($script, $index, 0, $additionalArgs);
} else {
$args = array_merge($script, $additionalArgs);
}
$flags = $event->getFlags(); $flags = $event->getFlags();
if (isset($flags['script-alias-input'])) { if (isset($flags['script-alias-input'])) {
$argsString = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $script)); $argsString = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $script));
@ -294,7 +304,7 @@ class EventDispatcher
$app->add($cmd); $app->add($cmd);
$app->setDefaultCommand((string) $cmd->getName(), true); $app->setDefaultCommand((string) $cmd->getName(), true);
try { try {
$args = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $event->getArguments())); $args = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $additionalArgs));
// reusing the output from $this->io is mostly needed for tests, but generally speaking // reusing the output from $this->io is mostly needed for tests, but generally speaking
// it does not hurt to keep the same stream as the current Application // it does not hurt to keep the same stream as the current Application
if ($this->io instanceof ConsoleIO) { if ($this->io instanceof ConsoleIO) {
@ -313,13 +323,17 @@ class EventDispatcher
throw $e; throw $e;
} }
} else { } else {
$args = implode(' ', array_map(['Composer\Util\ProcessExecutor', 'escape'], $event->getArguments())); $args = implode(' ', array_map(['Composer\Util\ProcessExecutor', 'escape'], $additionalArgs));
// @putenv does not receive arguments // @putenv does not receive arguments
if (strpos($callable, '@putenv ') === 0) { if (strpos($callable, '@putenv ') === 0) {
$exec = $callable; $exec = $callable;
} else { } else {
$exec = $callable . ($args === '' ? '' : ' '.$args); if (str_contains($callable, '@additional_args')) {
$exec = str_replace('@additional_args', $args, $callable);
} else {
$exec = $callable . ($args === '' ? '' : ' '.$args);
}
} }
if ($this->io->isVerbose()) { if ($this->io->isVerbose()) {

View File

@ -311,6 +311,51 @@ class EventDispatcherTest extends TestCase
} }
} }
public function testDispatcherSupportForAdditionalArgs(): void
{
$process = $this->getProcessExecutorMock();
$dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
->setConstructorArgs([
$this->createComposerInstance(),
$io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
$process,
])
->onlyMethods([
'getListeners',
])
->getMock();
$reflMethod = new \ReflectionMethod($dispatcher, 'getPhpExecCommand');
if (PHP_VERSION_ID < 80100) {
$reflMethod->setAccessible(true);
}
$phpCmd = $reflMethod->invoke($dispatcher);
$args = ProcessExecutor::escape('ARG').' '.ProcessExecutor::escape('ARG2').' '.ProcessExecutor::escape('--arg');
$process->expects([
'echo -n foo',
$phpCmd.' foo.php '.$args.' then the rest',
'echo -n bar '.$args,
], true);
$listeners = [
'echo -n foo @no_additional_args',
'@php foo.php @additional_args then the rest',
'echo -n bar',
];
$dispatcher->expects($this->atLeastOnce())
->method('getListeners')
->will($this->returnValue($listeners));
$dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false, ['ARG', 'ARG2', '--arg']);
$expected = '> post-install-cmd: echo -n foo'.PHP_EOL.
'> post-install-cmd: @php foo.php '.$args.' then the rest'.PHP_EOL.
'> post-install-cmd: echo -n bar '.$args.PHP_EOL;
self::assertEquals($expected, $io->getOutput());
}
public static function createsVendorBinFolderChecksEnvDoesNotContainsBin(): void public static function createsVendorBinFolderChecksEnvDoesNotContainsBin(): void
{ {
mkdir(__DIR__ . '/vendor/bin', 0700, true); mkdir(__DIR__ . '/vendor/bin', 0700, true);