1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace Smeghead \PhpVariableHardUsage \Option ;
6+
7+ use Smeghead \PhpVariableHardUsage \Command \CheckCommand ;
8+ use Smeghead \PhpVariableHardUsage \Command \CommandInterface ;
9+ use Smeghead \PhpVariableHardUsage \Command \HelpCommand ;
10+ use Smeghead \PhpVariableHardUsage \Command \ScopesCommand ;
11+ use Smeghead \PhpVariableHardUsage \Command \SingleCommand ;
12+ use Smeghead \PhpVariableHardUsage \Command \VersionCommand ;
13+
14+ /**
15+ * コマンドライン引数を解析し、適切なコマンドと引数を生成するクラス
16+ */
17+ final class CommandFactory
18+ {
19+ /** @var array<string> */
20+ private array $ argv ;
21+
22+ /**
23+ * @param array<string> $argv コマンドライン引数
24+ */
25+ public function __construct (array $ argv )
26+ {
27+ $ this ->argv = $ argv ;
28+ }
29+
30+ /**
31+ * コマンドライン引数を解析し、コマンドと引数を返す
32+ */
33+ public function create (): CommandInterface
34+ {
35+ // 引数がない場合はヘルプコマンド
36+ if (count ($ this ->argv ) < 2 ) {
37+ return new HelpCommand ();
38+ }
39+
40+ $ command = $ this ->argv [1 ];
41+
42+ // ヘルプと バージョン表示は特別処理
43+ if ($ command === '--help ' ) {
44+ return new HelpCommand ();
45+ }
46+
47+ if ($ command === '--version ' ) {
48+ return new VersionCommand ();
49+ }
50+
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 );
65+ }
66+ }
67+
68+ /**
69+ * 単一ファイルコマンドを解析
70+ */
71+ private function parseSingleCommand (): CommandInterface
72+ {
73+ $ args = array_slice ($ this ->argv , 2 );
74+
75+ if (empty ($ args )) {
76+ return new HelpCommand ();
77+ }
78+
79+ return new SingleCommand ($ args [0 ]);
80+ }
81+
82+ /**
83+ * スコープコマンドを解析
84+ */
85+ private function parseScopesCommand (): CommandInterface
86+ {
87+ $ args = array_slice ($ this ->argv , 2 );
88+
89+ if (empty ($ args )) {
90+ return new HelpCommand ();
91+ }
92+
93+ return new ScopesCommand ($ args );
94+ }
95+
96+ /**
97+ * チェックコマンドを解析
98+ */
99+ private function parseCheckCommand (): CommandInterface
100+ {
101+ $ args = array_slice ($ this ->argv , 2 );
102+
103+ if (empty ($ args )) {
104+ return new HelpCommand ();
105+ }
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 ];
203+ }
204+
205+ return [$ optName , true ];
206+ }
207+ }
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+ }
0 commit comments