Skip to content

Commit 8ac3ec7

Browse files
committed
feat: support file glob pattern as input
Signed-off-by: Emilien Escalle <[email protected]>
1 parent a4720e4 commit 8ac3ec7

File tree

9 files changed

+290
-132
lines changed

9 files changed

+290
-132
lines changed

.php-cs-fixer.dist.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010

1111
return (new Config())
1212
->setRules([
13-
'@PSR12' => true,
14-
'array_indentation' => true,
15-
'@PHP83Migration' => true,
13+
'@PER-CS' => true,
14+
'@PHP84Migration' => true,
15+
'fully_qualified_strict_types' => [
16+
'import_symbols' => true,
17+
],
18+
'global_namespace_import' => true,
19+
1620
])
1721
->setFinder($finder)
1822
->setUsingCache(true)
1923
->setCacheFile(__DIR__ . '/tools/cache/.php-cs-fixer.cache')
20-
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());
24+
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());

docs/usage.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Result:
2727
Usage:
2828
------
2929
30-
php-css-lint [--options='{ }'] css_file_or_string_to_lint
30+
php-css-lint [--options='{ }'] input_to_lint
3131
3232
Arguments:
3333
----------
@@ -40,17 +40,20 @@ Arguments:
4040
* "nonStandards": { "property" => bool }: will merge with the current property
4141
Example: --options='{ "constructors": {"o" : false}, "allowedIndentationChars": ["\t"] }'
4242
43-
css_file_or_string_to_lint
44-
The CSS file path (absolute or relative) or a CSS string to be linted
43+
input_to_lint
44+
The CSS file path (absolute or relative)
45+
a glob pattern of file(s) to be linted
46+
or a CSS string to be linted
4547
Example:
46-
./path/to/css_file_path_to_lint.css
48+
"./path/to/css_file_path_to_lint.css"
49+
"./path/to/css_file_path_to_lint/*.css"
4750
".test { color: red; }"
4851
4952
Examples:
5053
---------
5154
5255
Lint a CSS file:
53-
php-css-lint ./path/to/css_file_path_to_lint.css
56+
php-css-lint "./path/to/css_file_path_to_lint.css"
5457
5558
Lint a CSS string:
5659
php-css-lint ".test { color: red; }"
@@ -64,7 +67,7 @@ Examples:
6467
In a terminal, execute:
6568

6669
```sh
67-
php vendor/bin/php-css-lint /path/to/not_valid_file.css
70+
php vendor/bin/php-css-lint "/path/to/not_valid_file.css"
6871
```
6972

7073
Result:
@@ -77,6 +80,29 @@ Result:
7780
- Unterminated "selector content" (line: 17, char: 0)
7881
```
7982

83+
### Lint file(s) matching a glob pattern
84+
85+
See <https://www.php.net/manual/en/function.glob.php> for supported patterns.
86+
87+
In a terminal, execute:
88+
89+
```sh
90+
php vendor/bin/php-css-lint "/path/to/*.css"
91+
```
92+
93+
Result:
94+
95+
```
96+
# Lint CSS file "/path/to/not_valid_file.css"...
97+
=> CSS file "/path/to/not_valid_file" is not valid:
98+
99+
- Unknown CSS property "bordr-top-style" (line: 8, char: 20)
100+
- Unterminated "selector content" (line: 17, char: 0)
101+
102+
# Lint CSS file "/path/to/valid_file.css"...
103+
=> CSS file "/path/to/valid_file" is valid
104+
```
105+
80106
### Lint a css string
81107

82108
In a terminal, execute:

scripts/php-css-lint

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,29 @@ echo PHP_EOL .
55
'===========================================================' . PHP_EOL . PHP_EOL .
66
' ____ _ ____ ____ ____ _ _ _ ' . PHP_EOL .
77
' | _ \| |__ _ __ / ___/ ___/ ___| | | (_)_ __ | |_ ' . PHP_EOL .
8-
' | |_) | \'_ \| \'_ \ | | \___ \___ \ | | | | \'_ \| __|' . PHP_EOL .
8+
" | |_) | '_ \| '_ \ | | \___ \___ \ | | | | '_ \| __|" . PHP_EOL .
99
' | __/| | | | |_) | | |___ ___) |__) | | |___| | | | | |_ ' . PHP_EOL .
1010
' |_| |_| |_| .__/ \____|____/____/ |_____|_|_| |_|\__|' . PHP_EOL .
1111
' |_| ' . PHP_EOL . PHP_EOL .
1212
'===========================================================' . PHP_EOL . PHP_EOL;
1313

14-
$sComposerAutoloaderWorkingDirectory = getcwd() . '/vendor/autoload.php';
15-
if (is_file($sComposerAutoloaderWorkingDirectory)) {
16-
require_once $sComposerAutoloaderWorkingDirectory;
14+
$composerAutoloaderWorkingDirectory = getcwd() . '/vendor/autoload.php';
15+
if (is_file($composerAutoloaderWorkingDirectory)) {
16+
require_once $composerAutoloaderWorkingDirectory;
1717
}
1818

1919
if (!class_exists('CssLint\CssLint', true)) {
2020
// consider being in bin dir
21-
$sComposerAutoloader = __DIR__ . '/../vendor/autoload.php';
22-
if (!is_file($sComposerAutoloader)) {
21+
$composerAutoloader = __DIR__ . '/../vendor/autoload.php';
22+
if (!is_file($composerAutoloader)) {
2323
// consider being in vendor/neilime/php-css-lint/scripts
24-
$sComposerAutoloader = __DIR__ . '/../../../autoload.php';
24+
$composerAutoloader = __DIR__ . '/../../../autoload.php';
2525
}
2626

27-
require_once $sComposerAutoloader;
27+
require_once $composerAutoloader;
2828
}
2929

30-
$oCssLintCli = new \CssLint\Cli();
31-
$iReturnCode = $oCssLintCli->run($_SERVER['argv']);
30+
$cssLintCli = new \CssLint\Cli();
31+
$returnCode = $cssLintCli->run($_SERVER['argv']);
3232

33-
exit($iReturnCode);
33+
exit($returnCode);

src/CssLint/Cli.php

Lines changed: 128 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace 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

Comments
 (0)