Skip to content

Commit dbdc3f5

Browse files
authored
Merge pull request #19 from smeghead/feature/getopt
Moved command-line option parsing logic into separate classes for bet…
2 parents 8d9d46a + dcb6f57 commit dbdc3f5

File tree

7 files changed

+178
-171
lines changed

7 files changed

+178
-171
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
### Bug fix
4+
5+
* Moved command-line option parsing logic into separate classes for better maintainability
6+
37
## v0.0.7 (2025-03-25)
48

59
### Features

bin/php-variable-hard-usage

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php
1111
}
1212

1313
use Smeghead\PhpVariableHardUsage\Command;
14+
use Smeghead\PhpVariableHardUsage\Option\GetOptions;
15+
16+
$getOptions = new GetOptions($_SERVER['argv']);
17+
$result = $getOptions->parse();
1418

1519
$command = new Command();
16-
$exitCode = $command->run($argv);
20+
$exitCode = $command->run($result->options, $result->paths);
1721
exit($exitCode);

src/Command.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
final class Command
1010
{
1111
/**
12+
* @param array<string, string|bool> $options
1213
* @param list<string> $argv
1314
* @return int 終了コード
1415
*/
15-
public function run(array $argv): int
16+
public function run(array $options, array $argv): int
1617
{
17-
$factory = new CommandFactory($argv);
18+
$factory = new CommandFactory($options, $argv);
1819
$command = $factory->create();
1920
return $command->execute();
2021
}

src/Option/CommandFactory.php

Lines changed: 46 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -16,208 +16,98 @@
1616
*/
1717
final class CommandFactory
1818
{
19-
/** @var array<string> */
20-
private array $argv;
19+
/** @var list<string> */
20+
private const array SUB_COMMANDS = [
21+
'single',
22+
'scopes',
23+
'check',
24+
];
2125

2226
/**
27+
* @param array<string, string|bool> $options オプション
2328
* @param array<string> $argv コマンドライン引数
2429
*/
25-
public function __construct(array $argv)
30+
public function __construct(private readonly array $options, private readonly array $argv)
2631
{
27-
$this->argv = $argv;
2832
}
2933

3034
/**
3135
* コマンドライン引数を解析し、コマンドと引数を返す
3236
*/
3337
public function create(): CommandInterface
3438
{
35-
// 引数がない場合はヘルプコマンド
36-
if (count($this->argv) < 2) {
37-
return new HelpCommand();
38-
}
39-
40-
$command = $this->argv[1];
41-
4239
// ヘルプと バージョン表示は特別処理
43-
if ($command === '--help') {
40+
if (array_key_exists('help', $this->options)) {
4441
return new HelpCommand();
4542
}
4643

47-
if ($command === '--version') {
44+
if (array_key_exists('version', $this->options)) {
4845
return new VersionCommand();
4946
}
5047

51-
// コマンドに応じた処理
52-
switch ($command) {
53-
case 'single':
54-
return $this->parseSingleCommand();
55-
56-
case 'scopes':
57-
return $this->parseScopesCommand();
58-
59-
case 'check':
60-
return $this->parseCheckCommand();
61-
62-
default:
63-
// 後方互換性のため、引数そのものをファイル名として解釈
64-
return new SingleCommand($command);
48+
if (count($this->argv) === 0) {
49+
return new HelpCommand();
50+
}
51+
52+
$paths = $this->argv;
53+
if (in_array($this->argv[0], self::SUB_COMMANDS, true)) {
54+
$subCommand = $this->argv[0];
55+
$paths = array_slice($this->argv, 1);
56+
// コマンドに応じた処理
57+
switch ($subCommand) {
58+
case 'single':
59+
return $this->parseSingleCommand($paths);
60+
case 'scopes':
61+
return $this->parseScopesCommand($paths);
62+
case 'check':
63+
return $this->parseCheckCommand($paths);
64+
}
6565
}
66+
return new SingleCommand($paths[0]);
6667
}
6768

6869
/**
6970
* 単一ファイルコマンドを解析
71+
*
72+
* @param list<string> $paths
7073
*/
71-
private function parseSingleCommand(): CommandInterface
74+
private function parseSingleCommand(array $paths): CommandInterface
7275
{
73-
$args = array_slice($this->argv, 2);
74-
75-
if (empty($args)) {
76+
if (empty($paths)) {
7677
return new HelpCommand();
7778
}
7879

79-
return new SingleCommand($args[0]);
80+
return new SingleCommand($paths[0]);
8081
}
8182

8283
/**
8384
* スコープコマンドを解析
85+
* @param list<string> $paths
8486
*/
85-
private function parseScopesCommand(): CommandInterface
87+
private function parseScopesCommand(array $paths): CommandInterface
8688
{
87-
$args = array_slice($this->argv, 2);
88-
89-
if (empty($args)) {
89+
if (empty($paths)) {
9090
return new HelpCommand();
9191
}
9292

93-
return new ScopesCommand($args);
93+
return new ScopesCommand($paths);
9494
}
9595

9696
/**
9797
* チェックコマンドを解析
98+
* @param list<string> $paths
9899
*/
99-
private function parseCheckCommand(): CommandInterface
100+
private function parseCheckCommand(array $paths): CommandInterface
100101
{
101-
$args = array_slice($this->argv, 2);
102-
103-
if (empty($args)) {
102+
if (empty($paths)) {
104103
return new HelpCommand();
105104
}
106-
107-
$parsedArgs = $this->parseArguments($args);
108-
109-
if (empty($parsedArgs->paths)) {
110-
return new HelpCommand();
111-
}
112-
113-
$threshold = isset($parsedArgs->options['threshold']) ? intval($parsedArgs->options['threshold']) : null;
114-
115-
return new CheckCommand($parsedArgs->paths, $threshold);
116-
}
117-
118-
/**
119-
* コマンドライン引数を解析して、オプションとパスに分離する
120-
*
121-
* @param array<string> $args
122-
* @return ParsedArguments
123-
*/
124-
private function parseArguments(array $args): ParsedArguments
125-
{
126-
$options = [];
127-
$paths = [];
128-
129-
$i = 0;
130-
while ($i < count($args)) {
131-
$arg = $args[$i];
132-
133-
if ($this->isOptionWithValue($arg, '--threshold', $args, $i)) {
134-
$options['threshold'] = (int)$args[$i + 1];
135-
$i += 2;
136-
} elseif ($this->isOptionWithInlineValue($arg, '--threshold=', $matches)) {
137-
$options['threshold'] = (int)$matches[1];
138-
$i++;
139-
} elseif ($this->isOption($arg)) {
140-
[$name, $value] = $this->parseOption($arg);
141-
$options[$name] = $value;
142-
$i++;
143-
} else {
144-
$paths[] = $arg;
145-
$i++;
146-
}
147-
}
148-
149-
return new ParsedArguments($paths, $options);
150-
}
151-
152-
/**
153-
* 値を持つオプションかどうかを判定
154-
*
155-
* @param string $arg 現在の引数
156-
* @param string $optionName オプション名
157-
* @param array<string> $args 全引数
158-
* @param int $index 現在の位置
159-
* @return bool
160-
*/
161-
private function isOptionWithValue(string $arg, string $optionName, array $args, int $index): bool
162-
{
163-
return $arg === $optionName && isset($args[$index + 1]);
164-
}
165-
166-
/**
167-
* インライン値を持つオプションかどうかを判定
168-
*
169-
* @param string $arg 現在の引数
170-
* @param string $prefix オプションのプレフィックス
171-
* @param null &$matches 正規表現のマッチ結果を格納する変数
172-
* @return bool
173-
*/
174-
private function isOptionWithInlineValue(string $arg, string $prefix, &$matches): bool
175-
{
176-
return preg_match('/^' . preg_quote($prefix, '/') . '(\d+)$/', $arg, $matches) === 1;
177-
}
178-
179-
/**
180-
* オプションかどうかを判定
181-
*
182-
* @param string $arg 現在の引数
183-
* @return bool
184-
*/
185-
private function isOption(string $arg): bool
186-
{
187-
return strpos($arg, '--') === 0;
188-
}
189-
190-
/**
191-
* オプション文字列をパースして名前と値を取得
192-
*
193-
* @param string $option オプション文字列
194-
* @return array{0: string, 1: string|bool} [オプション名, オプション値]
195-
*/
196-
private function parseOption(string $option): array
197-
{
198-
$optName = substr($option, 2);
199-
200-
if (strpos($optName, '=') !== false) {
201-
[$name, $value] = explode('=', $optName, 2);
202-
return [$name, $value];
105+
106+
$threshold = $this->options['threshold'] ?? null;
107+
if (isset($threshold)) {
108+
$threshold = (int) $threshold;
203109
}
204110

205-
return [$optName, true];
206-
}
111+
return new CheckCommand($paths, $threshold);
112+
}
207113
}
208-
209-
/**
210-
* パース済みの引数を表すクラス
211-
*/
212-
final class ParsedArguments
213-
{
214-
/**
215-
* @param array<string> $paths パスのリスト
216-
* @param array<string, string|int|bool|null> $options オプションのマップ
217-
*/
218-
public function __construct(
219-
public readonly array $paths,
220-
public readonly array $options
221-
) {
222-
}
223-
}

src/Option/GetOptions.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Smeghead\PhpVariableHardUsage\Option;
6+
7+
final class GetOptions
8+
{
9+
/**
10+
* @param array<string> $argv
11+
*/
12+
public function __construct(private readonly array $argv)
13+
{
14+
}
15+
16+
public function parse(): GetOptionsResult
17+
{
18+
$options = [];
19+
$args = [];
20+
$count = count($this->argv);
21+
for ($i = 0; $i < $count; $i++) {
22+
$arg = $this->argv[$i];
23+
if (strpos($arg, '--') === 0) {
24+
$key = substr($arg, 2);
25+
$value = true;
26+
if (strpos($key, '=') !== false) {
27+
[$key, $value] = explode('=', $key, 2);
28+
}
29+
$options[$key] = $value;
30+
} else {
31+
$args[] = $arg;
32+
}
33+
}
34+
return new GetOptionsResult($options, array_slice($args, 1));
35+
}
36+
}
37+
38+
final class GetOptionsResult {
39+
/**
40+
* @param array<string, string|bool> $options
41+
* @param array<string> $paths
42+
*/
43+
public function __construct(
44+
public array $options,
45+
public array $paths,
46+
) {}
47+
}

0 commit comments

Comments
 (0)