Skip to content

Moved command-line option parsing logic into separate classes for bet… #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion bin/php-variable-hard-usage
Original file line number Diff line number Diff line change
Expand Up @@ -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);
5 changes: 3 additions & 2 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
final class Command
{
/**
* @param array<string, string|bool> $options
* @param list<string> $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();
}
Expand Down
202 changes: 46 additions & 156 deletions src/Option/CommandFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,208 +16,98 @@
*/
final class CommandFactory
{
/** @var array<string> */
private array $argv;
/** @var list<string> */
private const array SUB_COMMANDS = [
'single',
'scopes',
'check',
];

/**
* @param array<string, string|bool> $options オプション
* @param array<string> $argv コマンドライン引数
*/
public function __construct(array $argv)
public function __construct(private readonly array $options, private readonly array $argv)
{
$this->argv = $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<string> $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<string> $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<string> $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<string> $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<string> $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 (isset($threshold)) {
$threshold = (int) $threshold;
}

return [$optName, true];
}
return new CheckCommand($paths, $threshold);
}
}

/**
* パース済みの引数を表すクラス
*/
final class ParsedArguments
{
/**
* @param array<string> $paths パスのリスト
* @param array<string, string|int|bool|null> $options オプションのマップ
*/
public function __construct(
public readonly array $paths,
public readonly array $options
) {
}
}
47 changes: 47 additions & 0 deletions src/Option/GetOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Smeghead\PhpVariableHardUsage\Option;

final class GetOptions
{
/**
* @param array<string> $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<string, string|bool> $options
* @param array<string> $paths
*/
public function __construct(
public array $options,
public array $paths,
) {}
}
Loading