Skip to content

Commit 1c758c0

Browse files
authored
Rework InputOptionsSerializer (#77)
In #28, the feature to forward the command options was introduced. It had to later be followed up by #41, #42 in order to fix the issue where the option value may require quoting. This piece of code is heavily under-tested (and it was really hard to test it as is) and is prone to bugs (I found 2 adding tests). In this PR, I'm extracting this element into `InputOptionsSerializer` as a closed piece (static & final) as IMO there is no need to make it extensible: if broken it needs to be fixed. This extraction allows much easier testing. This PR also fixes: - the case of negatable options - detection quoting with may fail to detect some special characters
1 parent 5614166 commit 1c758c0

File tree

3 files changed

+427
-37
lines changed

3 files changed

+427
-37
lines changed

src/InputOptionsSerializer.php

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Webmozarts Console Parallelization package.
5+
*
6+
* (c) Webmozarts GmbH <office@webmozarts.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Webmozarts\Console\Parallelization;
15+
16+
use function array_diff_key;
17+
use function array_fill_keys;
18+
use function array_keys;
19+
use function array_map;
20+
use function implode;
21+
use function is_string;
22+
use function preg_match;
23+
use function sprintf;
24+
use function str_replace;
25+
use Symfony\Component\Console\Input\InputDefinition;
26+
use Symfony\Component\Console\Input\InputInterface;
27+
use Symfony\Component\Console\Input\InputOption;
28+
29+
final class InputOptionsSerializer
30+
{
31+
private function __construct()
32+
{
33+
}
34+
35+
/**
36+
* @param string[] $excludedOptionNames
37+
*
38+
* @return string[]
39+
*/
40+
public static function serialize(
41+
InputDefinition $commandDefinition,
42+
InputInterface $input,
43+
array $excludedOptionNames
44+
): array {
45+
$filteredOptions = array_diff_key(
46+
$input->getOptions(),
47+
array_fill_keys($excludedOptionNames, ''),
48+
);
49+
50+
return array_map(
51+
static fn (string $name) => self::serializeOption(
52+
$commandDefinition->getOption($name),
53+
$name,
54+
$filteredOptions[$name],
55+
),
56+
array_keys($filteredOptions),
57+
);
58+
}
59+
60+
/**
61+
* @param string|bool|int|float|null|array<string|bool|int|float|null> $value
62+
*/
63+
private static function serializeOption(
64+
InputOption $option,
65+
string $name,
66+
$value
67+
): string {
68+
if ($option->isNegatable()) {
69+
return sprintf(
70+
'--%s%s',
71+
$value ? '' : 'no-',
72+
$name,
73+
);
74+
}
75+
76+
if (!$option->acceptValue()) {
77+
return sprintf(
78+
'--%s',
79+
$name,
80+
);
81+
}
82+
83+
if ($option->isArray()) {
84+
return implode(
85+
'',
86+
array_map(
87+
static fn ($item) => self::serializeOptionWithValue($name, $item),
88+
$value,
89+
),
90+
);
91+
}
92+
93+
return self::serializeOptionWithValue($name, $value);
94+
}
95+
96+
/**
97+
* @param string|bool|int|float|null|array<string|bool|int|float|null> $value
98+
*/
99+
private static function serializeOptionWithValue(
100+
string $name,
101+
$value
102+
): string {
103+
return sprintf(
104+
'--%s=%s',
105+
$name,
106+
self::quoteOptionValue($value),
107+
);
108+
}
109+
110+
/**
111+
* Ensure that an option value is quoted correctly before it is passed to a
112+
* child process.
113+
*
114+
* @param string|bool|int|float|null $value
115+
*
116+
* @return string|bool|int|float|null
117+
*/
118+
private static function quoteOptionValue($value)
119+
{
120+
if (self::isValueRequiresQuoting($value)) {
121+
return sprintf(
122+
'"%s"',
123+
str_replace('"', '\"', $value),
124+
);
125+
}
126+
127+
return $value;
128+
}
129+
130+
/**
131+
* Validate whether a command option requires quoting.
132+
*/
133+
private static function isValueRequiresQuoting($value): bool
134+
{
135+
return is_string($value) && 0 < preg_match('/[\s\W]/x', $value);
136+
}
137+
}

src/Parallelization.php

+8-37
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
namespace Webmozarts\Console\Parallelization;
1515

16-
use function array_diff_key;
17-
use function array_fill_keys;
1816
use function array_filter;
1917
use function array_map;
2018
use function array_merge;
@@ -336,7 +334,14 @@ protected function executeMasterProcess(
336334
),
337335
'--child',
338336
]),
339-
$this->serializeInputOptions($input, ['child', 'processes']),
337+
// Forward all the options except for "processes" to the children
338+
// this way the children can inherit the options such as env
339+
// or no-debug.
340+
InputOptionsSerializer::serialize(
341+
$this->getDefinition(),
342+
$input,
343+
['child', 'processes'],
344+
),
340345
);
341346

342347
$terminalWidth = (new Terminal())->getWidth();
@@ -570,38 +575,4 @@ private function runTolerantSingleCommand(
570575
}
571576
}
572577
}
573-
574-
/**
575-
* @param string[] $blackListParams
576-
*
577-
* @return string[]
578-
*/
579-
private function serializeInputOptions(InputInterface $input, array $blackListParams): array
580-
{
581-
$options = array_diff_key(
582-
array_filter($input->getOptions()),
583-
array_fill_keys($blackListParams, ''),
584-
);
585-
586-
$preparedOptionList = [];
587-
foreach ($options as $name => $value) {
588-
$definition = $this->getDefinition();
589-
$option = $definition->getOption($name);
590-
591-
$optionString = '';
592-
if (!$option->acceptValue()) {
593-
$optionString .= '--'.$name;
594-
} elseif ($option->isArray()) {
595-
foreach ($value as $arrayValue) {
596-
$optionString .= '--'.$name.'='.$this->quoteOptionValue($arrayValue);
597-
}
598-
} else {
599-
$optionString .= '--'.$name.'='.$this->quoteOptionValue($value);
600-
}
601-
602-
$preparedOptionList[] = $optionString;
603-
}
604-
605-
return $preparedOptionList;
606-
}
607578
}

0 commit comments

Comments
 (0)