From 9d1ebe189a2bd883aa6fb66d332a9df5aca19640 Mon Sep 17 00:00:00 2001 From: smeghead Date: Wed, 26 Mar 2025 23:59:03 +0900 Subject: [PATCH 1/2] Moved command-line option parsing logic into separate classes for better maintainability --- CHANGELOG.md | 4 + bin/php-variable-hard-usage | 6 +- src/Command.php | 5 +- src/Option/CommandFactory.php | 202 ++++++++-------------------------- src/Option/GetOptions.php | 47 ++++++++ test/CommandFactoryTest.php | 24 ++-- test/GetOptionsTest.php | 61 ++++++++++ 7 files changed, 178 insertions(+), 171 deletions(-) create mode 100644 src/Option/GetOptions.php create mode 100644 test/GetOptionsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f1959..bd7a5d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +### Bug fix + + * Moved command-line option parsing logic into separate classes for better maintainability + ## v0.0.7 (2025-03-25) ### Features diff --git a/bin/php-variable-hard-usage b/bin/php-variable-hard-usage index 9101408..6230f5d 100755 --- a/bin/php-variable-hard-usage +++ b/bin/php-variable-hard-usage @@ -11,7 +11,11 @@ foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php } use Smeghead\PhpVariableHardUsage\Command; +use Smeghead\PhpVariableHardUsage\Option\GetOptions; + +$getOptions = new GetOptions($_SERVER['argv']); +$result = $getOptions->parse(); $command = new Command(); -$exitCode = $command->run($argv); +$exitCode = $command->run($result->options, $result->paths); exit($exitCode); \ No newline at end of file diff --git a/src/Command.php b/src/Command.php index 1805305..b78dd61 100644 --- a/src/Command.php +++ b/src/Command.php @@ -9,12 +9,13 @@ final class Command { /** + * @param array $options * @param list $argv * @return int 終了コード */ - public function run(array $argv): int + public function run(array $options, array $argv): int { - $factory = new CommandFactory($argv); + $factory = new CommandFactory($options, $argv); $command = $factory->create(); return $command->execute(); } diff --git a/src/Option/CommandFactory.php b/src/Option/CommandFactory.php index 7b6eeb6..4e94519 100644 --- a/src/Option/CommandFactory.php +++ b/src/Option/CommandFactory.php @@ -16,15 +16,19 @@ */ final class CommandFactory { - /** @var array */ - private array $argv; + /** @var list */ + private const array SUB_COMMANDS = [ + 'single', + 'scopes', + 'check', + ]; /** + * @param array $options オプション * @param array $argv コマンドライン引数 */ - public function __construct(array $argv) + public function __construct(private readonly array $options, private readonly array $argv) { - $this->argv = $argv; } /** @@ -32,192 +36,78 @@ public function __construct(array $argv) */ public function create(): CommandInterface { - // 引数がない場合はヘルプコマンド - if (count($this->argv) < 2) { - return new HelpCommand(); - } - - $command = $this->argv[1]; - // ヘルプと バージョン表示は特別処理 - if ($command === '--help') { + if (array_key_exists('help', $this->options)) { return new HelpCommand(); } - if ($command === '--version') { + if (array_key_exists('version', $this->options)) { return new VersionCommand(); } - // コマンドに応じた処理 - switch ($command) { - case 'single': - return $this->parseSingleCommand(); - - case 'scopes': - return $this->parseScopesCommand(); - - case 'check': - return $this->parseCheckCommand(); - - default: - // 後方互換性のため、引数そのものをファイル名として解釈 - return new SingleCommand($command); + if (count($this->argv) === 0) { + return new HelpCommand(); + } + + $paths = $this->argv; + if (in_array($this->argv[0], self::SUB_COMMANDS, true)) { + $subCommand = $this->argv[0]; + $paths = array_slice($this->argv, 1); + // コマンドに応じた処理 + switch ($subCommand) { + case 'single': + return $this->parseSingleCommand($paths); + case 'scopes': + return $this->parseScopesCommand($paths); + case 'check': + return $this->parseCheckCommand($paths); + } } + return new SingleCommand($paths[0]); } /** * 単一ファイルコマンドを解析 + * + * @param list $paths */ - private function parseSingleCommand(): CommandInterface + private function parseSingleCommand(array $paths): CommandInterface { - $args = array_slice($this->argv, 2); - - if (empty($args)) { + if (empty($paths)) { return new HelpCommand(); } - return new SingleCommand($args[0]); + return new SingleCommand($paths[0]); } /** * スコープコマンドを解析 + * @param list $paths */ - private function parseScopesCommand(): CommandInterface + private function parseScopesCommand(array $paths): CommandInterface { - $args = array_slice($this->argv, 2); - - if (empty($args)) { + if (empty($paths)) { return new HelpCommand(); } - return new ScopesCommand($args); + return new ScopesCommand($paths); } /** * チェックコマンドを解析 + * @param list $paths */ - private function parseCheckCommand(): CommandInterface + private function parseCheckCommand(array $paths): CommandInterface { - $args = array_slice($this->argv, 2); - - if (empty($args)) { + if (empty($paths)) { return new HelpCommand(); } - - $parsedArgs = $this->parseArguments($args); - - if (empty($parsedArgs->paths)) { - return new HelpCommand(); - } - - $threshold = isset($parsedArgs->options['threshold']) ? intval($parsedArgs->options['threshold']) : null; - - return new CheckCommand($parsedArgs->paths, $threshold); - } - - /** - * コマンドライン引数を解析して、オプションとパスに分離する - * - * @param array $args - * @return ParsedArguments - */ - private function parseArguments(array $args): ParsedArguments - { - $options = []; - $paths = []; - - $i = 0; - while ($i < count($args)) { - $arg = $args[$i]; - - if ($this->isOptionWithValue($arg, '--threshold', $args, $i)) { - $options['threshold'] = (int)$args[$i + 1]; - $i += 2; - } elseif ($this->isOptionWithInlineValue($arg, '--threshold=', $matches)) { - $options['threshold'] = (int)$matches[1]; - $i++; - } elseif ($this->isOption($arg)) { - [$name, $value] = $this->parseOption($arg); - $options[$name] = $value; - $i++; - } else { - $paths[] = $arg; - $i++; - } - } - - return new ParsedArguments($paths, $options); - } - - /** - * 値を持つオプションかどうかを判定 - * - * @param string $arg 現在の引数 - * @param string $optionName オプション名 - * @param array $args 全引数 - * @param int $index 現在の位置 - * @return bool - */ - private function isOptionWithValue(string $arg, string $optionName, array $args, int $index): bool - { - return $arg === $optionName && isset($args[$index + 1]); - } - - /** - * インライン値を持つオプションかどうかを判定 - * - * @param string $arg 現在の引数 - * @param string $prefix オプションのプレフィックス - * @param null &$matches 正規表現のマッチ結果を格納する変数 - * @return bool - */ - private function isOptionWithInlineValue(string $arg, string $prefix, &$matches): bool - { - return preg_match('/^' . preg_quote($prefix, '/') . '(\d+)$/', $arg, $matches) === 1; - } - - /** - * オプションかどうかを判定 - * - * @param string $arg 現在の引数 - * @return bool - */ - private function isOption(string $arg): bool - { - return strpos($arg, '--') === 0; - } - - /** - * オプション文字列をパースして名前と値を取得 - * - * @param string $option オプション文字列 - * @return array{0: string, 1: string|bool} [オプション名, オプション値] - */ - private function parseOption(string $option): array - { - $optName = substr($option, 2); - - if (strpos($optName, '=') !== false) { - [$name, $value] = explode('=', $optName, 2); - return [$name, $value]; + + $threshold = $this->options['threshold'] ?? null; + if (is_numeric($threshold)) { + $threshold = (int) $threshold; } - return [$optName, true]; - } + return new CheckCommand($paths, $threshold); + } } - -/** - * パース済みの引数を表すクラス - */ -final class ParsedArguments -{ - /** - * @param array $paths パスのリスト - * @param array $options オプションのマップ - */ - public function __construct( - public readonly array $paths, - public readonly array $options - ) { - } -} \ No newline at end of file diff --git a/src/Option/GetOptions.php b/src/Option/GetOptions.php new file mode 100644 index 0000000..4f3c48f --- /dev/null +++ b/src/Option/GetOptions.php @@ -0,0 +1,47 @@ + $argv + */ + public function __construct(private readonly array $argv) + { + } + + public function parse(): GetOptionsResult + { + $options = []; + $args = []; + $count = count($this->argv); + for ($i = 0; $i < $count; $i++) { + $arg = $this->argv[$i]; + if (strpos($arg, '--') === 0) { + $key = substr($arg, 2); + $value = true; + if (strpos($key, '=') !== false) { + [$key, $value] = explode('=', $key, 2); + } + $options[$key] = $value; + } else { + $args[] = $arg; + } + } + return new GetOptionsResult($options, array_slice($args, 1)); + } +} + +final class GetOptionsResult { + /** + * @param array $options + * @param array $paths + */ + public function __construct( + public array $options, + public array $paths, + ) {} +} \ No newline at end of file diff --git a/test/CommandFactoryTest.php b/test/CommandFactoryTest.php index 8f1a5ce..26c2121 100644 --- a/test/CommandFactoryTest.php +++ b/test/CommandFactoryTest.php @@ -14,48 +14,48 @@ class CommandFactoryTest extends TestCase { public function testParseNoArgs(): void { - $argv = ['script.php']; - $sut = new CommandFactory($argv); + $argv = []; + $sut = new CommandFactory([], $argv); $result = $sut->create(); $this->assertInstanceOf(HelpCommand::class, $result); } public function testParseHelp(): void { - $argv = ['script.php', '--help']; - $sut = new CommandFactory($argv); + $argv = []; + $sut = new CommandFactory(['help' => false], $argv); $result = $sut->create(); $this->assertInstanceOf(HelpCommand::class, $result); } public function testParseVersion(): void { - $argv = ['script.php', '--version']; - $sut = new CommandFactory($argv); + $argv = []; + $sut = new CommandFactory(['version' => false], $argv); $result = $sut->create(); $this->assertInstanceOf(VersionCommand::class, $result); } public function testParseSingle(): void { - $argv = ['script.php', 'single', 'file.php']; - $sut = new CommandFactory($argv); + $argv = ['single', 'file.php']; + $sut = new CommandFactory([], $argv); $result = $sut->create(); $this->assertInstanceOf(SingleCommand::class, $result); } public function testParseScopes(): void { - $argv = ['script.php', 'scopes', 'dir1', 'dir2']; - $sut = new CommandFactory($argv); + $argv = ['scopes', 'dir1', 'dir2']; + $sut = new CommandFactory([], $argv); $result = $sut->create(); $this->assertInstanceOf(ScopesCommand::class, $result); } public function testParseCheck(): void { - $argv = ['script.php', 'check', '--threshold', '200', 'dir1', 'dir2']; - $sut = new CommandFactory($argv); + $argv = ['check', 'dir1', 'dir2']; + $sut = new CommandFactory(['threshold' => '200'], $argv); $result = $sut->create(); $this->assertInstanceOf(CheckCommand::class, $result); } diff --git a/test/GetOptionsTest.php b/test/GetOptionsTest.php new file mode 100644 index 0000000..9d44a97 --- /dev/null +++ b/test/GetOptionsTest.php @@ -0,0 +1,61 @@ +parse(); + $this->assertSame([], $result->paths); + } + + public function testHelp(): void + { + $argv = ['script.php', '--help']; + $sut = new GetOptions($argv); + $result = $sut->parse(); + $this->assertArrayHasKey('help', $result->options); + $this->assertSame(true, $result->options['help']); + } + + public function testVersion(): void + { + $argv = ['script.php', '--version']; + $sut = new GetOptions($argv); + $result = $sut->parse(); + $this->assertArrayHasKey('version', $result->options); + $this->assertSame(true, $result->options['version']); + } + + public function testSingle(): void + { + $argv = ['script.php', 'single', 'file.php']; + $sut = new GetOptions($argv); + $result = $sut->parse(); + $this->assertSame(['single', 'file.php'], $result->paths); + } + + public function testScopes(): void + { + $argv = ['script.php', 'scopes', 'dir1', 'dir2']; + $sut = new GetOptions($argv); + $result = $sut->parse(); + $this->assertSame(['scopes', 'dir1', 'dir2'], $result->paths); + } + + public function testCheckWithThreshold(): void + { + $argv = ['script.php', 'check', 'dir1', 'dir2', '--threshold=200']; + $sut = new GetOptions($argv); + $result = $sut->parse(); + $this->assertSame(['check', 'dir1', 'dir2'], $result->paths); + $this->assertArrayHasKey('threshold', $result->options); + $this->assertSame('200', $result->options['threshold']); + } +} \ No newline at end of file From dcb6f57d22eddd8b3632a1ff1745d33c0a8ebf1c Mon Sep 17 00:00:00 2001 From: smeghead Date: Thu, 27 Mar 2025 00:02:12 +0900 Subject: [PATCH 2/2] fix: phpstan error. --- src/Option/CommandFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Option/CommandFactory.php b/src/Option/CommandFactory.php index 4e94519..9f059fb 100644 --- a/src/Option/CommandFactory.php +++ b/src/Option/CommandFactory.php @@ -104,7 +104,7 @@ private function parseCheckCommand(array $paths): CommandInterface } $threshold = $this->options['threshold'] ?? null; - if (is_numeric($threshold)) { + if (isset($threshold)) { $threshold = (int) $threshold; }