44
55namespace CssLint ;
66
7+ use RuntimeException ;
8+ use Throwable ;
9+
710/**
811 * @phpstan-import-type Errors from \CssLint\Linter
912 * @package CssLint
@@ -24,58 +27,21 @@ class Cli
2427 public function run (array $ arguments ): int
2528 {
2629 $ cliArgs = $ this ->parseArguments ($ arguments );
27- if ($ cliArgs ->filePathOrCssString === null || $ cliArgs ->filePathOrCssString === '' || $ cliArgs ->filePathOrCssString === '0 ' ) {
30+ if ($ cliArgs ->input === null || $ cliArgs ->input === '' || $ cliArgs ->input === '0 ' ) {
2831 $ this ->printUsage ();
2932 return self ::RETURN_CODE_SUCCESS ;
3033 }
3134
32- $ properties = new \CssLint \Properties ();
33- if ($ cliArgs ->options !== null && $ cliArgs ->options !== '' && $ cliArgs ->options !== '0 ' ) {
34- $ options = json_decode ($ cliArgs ->options , true );
35-
36- if (json_last_error () !== 0 ) {
37- $ errorMessage = json_last_error_msg ();
38- $ this ->printError ('Unable to parse option argument: ' . $ errorMessage );
39- return self ::RETURN_CODE_ERROR ;
40- }
41-
42- if (!$ options ) {
43- $ this ->printError ('Unable to parse empty option argument ' );
44- return self ::RETURN_CODE_ERROR ;
45- }
35+ try {
36+ $ properties = $ this ->getPropertiesFromOptions ($ cliArgs ->options );
4637
47- if (!is_array ($ options )) {
48- $ this ->printError ('Unable to parse option argument: must be a json object ' );
49- return self ::RETURN_CODE_ERROR ;
50- }
51-
52- $ properties ->setOptions ($ options );
53- }
38+ $ cssLinter = new Linter ($ properties );
5439
55- $ cssLinter = new \CssLint \Linter ($ properties );
56-
57- $ filePathOrCssString = $ cliArgs ->filePathOrCssString ;
58- if (!file_exists ($ filePathOrCssString )) {
59- return $ this ->lintString ($ cssLinter , $ filePathOrCssString );
60- }
61-
62- $ filePath = $ filePathOrCssString ;
63- if (!is_readable ($ filePath )) {
64- $ this ->printError ('File " ' . $ filePath . '" is not readable ' );
40+ return $ this ->lintInput ($ cssLinter , $ cliArgs ->input );
41+ } catch (Throwable $ throwable ) {
42+ $ this ->printError ($ throwable ->getMessage ());
6543 return self ::RETURN_CODE_ERROR ;
6644 }
67-
68- return $ this ->lintFile ($ cssLinter , $ filePath );
69- }
70-
71- /**
72- * Retrieve the parsed Cli arguments from given arguments array
73- * @param string[] $arguments arguments to be parsed (@see $_SERVER['argv'])
74- * @return \CssLint\CliArgs an instance of Cli arguments object containing parsed arguments
75- */
76- private function parseArguments (array $ arguments ): \CssLint \CliArgs
77- {
78- return new \CssLint \CliArgs ($ arguments );
7945 }
8046
8147 /**
@@ -86,7 +52,7 @@ private function printUsage(): void
8652 $ this ->printLine ('Usage: ' . PHP_EOL .
8753 '------ ' . PHP_EOL .
8854 PHP_EOL .
89- ' ' . self ::SCRIPT_NAME . " [--options='{ }'] css_file_or_string_to_lint " . PHP_EOL .
55+ ' ' . self ::SCRIPT_NAME . " [--options='{ }'] input_to_lint " . PHP_EOL .
9056 PHP_EOL .
9157 'Arguments: ' . PHP_EOL .
9258 '---------- ' . PHP_EOL .
@@ -100,35 +66,143 @@ private function printUsage(): void
10066 ' Example: --options= \'{ "constructors": {"o" : false}, "allowedIndentationChars": ["\t"] } \'' .
10167 PHP_EOL .
10268 PHP_EOL .
103- ' css_file_or_string_to_lint ' . PHP_EOL .
104- ' The CSS file path (absolute or relative) or a CSS string to be linted ' . PHP_EOL .
69+ ' input_to_lint ' . PHP_EOL .
70+ ' The CSS file path (absolute or relative) ' . PHP_EOL .
71+ ' a glob pattern of file(s) to be linted ' . PHP_EOL .
72+ ' or a CSS string to be linted ' . PHP_EOL .
10573 ' Example: ' . PHP_EOL .
106- ' ./path/to/css_file_path_to_lint.css ' . PHP_EOL .
74+ ' "./path/to/css_file_path_to_lint.css" ' . PHP_EOL .
75+ ' "./path/to/css_file_path_to_lint/*.css" ' . PHP_EOL .
10776 ' ".test { color: red; }" ' . PHP_EOL .
10877 PHP_EOL .
10978 'Examples: ' . PHP_EOL .
11079 '--------- ' . PHP_EOL .
11180 PHP_EOL .
11281 ' Lint a CSS file: ' . PHP_EOL .
113- ' ' . self ::SCRIPT_NAME . ' ./path/to/css_file_path_to_lint.css ' . PHP_EOL . PHP_EOL .
82+ ' ' . self ::SCRIPT_NAME . ' " ./path/to/css_file_path_to_lint.css" ' . PHP_EOL . PHP_EOL .
11483 ' Lint a CSS string: ' . PHP_EOL .
115- ' ' . self ::SCRIPT_NAME . ' ".test { color: red; }" ' . PHP_EOL . PHP_EOL .
84+ ' ' . self ::SCRIPT_NAME . ' ".test { color: red; }" ' . PHP_EOL . PHP_EOL .
11685 ' Lint with only tabulation as indentation: ' . PHP_EOL .
11786 ' ' . self ::SCRIPT_NAME .
11887 ' --options= \'{ "allowedIndentationChars": ["\t"] } \' ".test { color: red; }" ' . PHP_EOL .
11988 PHP_EOL . PHP_EOL );
12089 }
12190
91+ /**
92+ * Retrieve the parsed Cli arguments from given arguments array
93+ * @param string[] $arguments arguments to be parsed (@see $_SERVER['argv'])
94+ * @return CliArgs an instance of Cli arguments object containing parsed arguments
95+ */
96+ private function parseArguments (array $ arguments ): CliArgs
97+ {
98+ return new CliArgs ($ arguments );
99+ }
100+
101+ /**
102+ * Retrieve the properties from the given options
103+ * @param string $options the options to be parsed
104+ */
105+ private function getPropertiesFromOptions (?string $ options ): Properties
106+ {
107+ $ properties = new Properties ();
108+ if ($ options === null || $ options === '' || $ options === '0 ' ) {
109+ return $ properties ;
110+ }
111+
112+ $ options = json_decode ($ options , true );
113+
114+ if (json_last_error () !== 0 ) {
115+ $ errorMessage = json_last_error_msg ();
116+ throw new RuntimeException ('Unable to parse option argument: ' . $ errorMessage );
117+ }
118+
119+ if (!$ options ) {
120+ throw new RuntimeException ('Unable to parse empty option argument ' );
121+ }
122+
123+ if (!is_array ($ options )) {
124+ throw new RuntimeException ('Unable to parse option argument: must be a json object ' );
125+ }
126+
127+ $ properties ->setOptions ($ options );
128+
129+ return $ properties ;
130+ }
131+
132+ private function lintInput (Linter $ cssLinter , string $ input ): int
133+ {
134+ if (file_exists ($ input )) {
135+ return $ this ->lintFile ($ cssLinter , $ input );
136+ }
137+
138+ if ($ this ->isGlobPattern ($ input )) {
139+ return $ this ->lintGlob ($ input );
140+ }
141+
142+ return $ this ->lintString ($ cssLinter , $ input );
143+ }
144+
145+ /**
146+ * Checks if a given string is a glob pattern.
147+ *
148+ * A glob pattern typically includes wildcard characters:
149+ * - '*' matches any sequence of characters.
150+ * - '?' matches any single character.
151+ * - '[]' matches any one character in the specified set.
152+ *
153+ * Optionally, if using the GLOB_BRACE flag, brace patterns like {foo,bar} are also valid.
154+ *
155+ * @param string $pattern The string to evaluate.
156+ * @return bool True if the string is a glob pattern, false otherwise.
157+ */
158+ private function isGlobPattern (string $ pattern ): bool
159+ {
160+ // Must be one line, no unscaped spaces
161+ if (preg_match ('/\s/ ' , $ pattern )) {
162+ return false ;
163+ }
164+
165+ // Check for basic wildcard characters.
166+ if (str_contains ($ pattern , '* ' ) || str_contains ($ pattern , '? ' ) || str_contains ($ pattern , '[ ' )) {
167+ return true ;
168+ }
169+
170+ // Optionally check for brace patterns, used with GLOB_BRACE.
171+ return str_contains ($ pattern , '{ ' ) || str_contains ($ pattern , '} ' );
172+ }
173+
174+ private function lintGlob (string $ glob ): int
175+ {
176+ $ cssLinter = new Linter ();
177+ $ files = glob ($ glob );
178+ if ($ files === [] || $ files === false ) {
179+ $ this ->printError ('No files found for glob " ' . $ glob . '" ' );
180+ return self ::RETURN_CODE_ERROR ;
181+ }
182+
183+ $ returnCode = self ::RETURN_CODE_SUCCESS ;
184+ foreach ($ files as $ file ) {
185+ $ returnCode = max ($ returnCode , $ this ->lintFile ($ cssLinter , $ file ));
186+ }
187+
188+ return $ returnCode ;
189+ }
190+
122191 /**
123192 * Performs lint on a given file path
124- * @param \CssLint\ Linter $cssLinter the instance of the linter
193+ * @param Linter $cssLinter the instance of the linter
125194 * @param string $filePath the path of the file to be linted
126195 * @return int the return code related to the execution of the linter
127196 */
128- private function lintFile (\ CssLint \ Linter $ cssLinter , string $ filePath ): int
197+ private function lintFile (Linter $ cssLinter , string $ filePath ): int
129198 {
130199 $ this ->printLine ('# Lint CSS file " ' . $ filePath . '"... ' );
131200
201+ if (!is_readable ($ filePath )) {
202+ $ this ->printError ('File " ' . $ filePath . '" is not readable ' );
203+ return self ::RETURN_CODE_ERROR ;
204+ }
205+
132206 if ($ cssLinter ->lintFile ($ filePath )) {
133207 $ this ->printLine ("\033[32m => CSS file \"" . $ filePath . "\" is valid \033[0m " . PHP_EOL );
134208 return self ::RETURN_CODE_SUCCESS ;
@@ -142,11 +216,11 @@ private function lintFile(\CssLint\Linter $cssLinter, string $filePath): int
142216
143217 /**
144218 * Performs lint on a given string
145- * @param \CssLint\ Linter $cssLinter the instance of the linter
219+ * @param Linter $cssLinter the instance of the linter
146220 * @param string $stringValue the CSS string to be linted
147221 * @return int the return code related to the execution of the linter
148222 */
149- private function lintString (\ CssLint \ Linter $ cssLinter , string $ stringValue ): int
223+ private function lintString (Linter $ cssLinter , string $ stringValue ): int
150224 {
151225 $ this ->printLine ('# Lint CSS string... ' );
152226
0 commit comments