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