Skip to content

feat: support file glob pattern as input #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@

return (new Config())
->setRules([
'@PSR12' => true,
'array_indentation' => true,
'@PHP83Migration' => true,
'@PER-CS' => true,
'@PHP84Migration' => true,
'fully_qualified_strict_types' => [
'import_symbols' => true,
],
'global_namespace_import' => true,

])
->setFinder($finder)
->setUsingCache(true)
->setCacheFile(__DIR__ . '/tools/cache/.php-cs-fixer.cache')
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());
38 changes: 32 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Result:
Usage:
------

php-css-lint [--options='{ }'] css_file_or_string_to_lint
php-css-lint [--options='{ }'] input_to_lint

Arguments:
----------
Expand All @@ -40,17 +40,20 @@ Arguments:
* "nonStandards": { "property" => bool }: will merge with the current property
Example: --options='{ "constructors": {"o" : false}, "allowedIndentationChars": ["\t"] }'

css_file_or_string_to_lint
The CSS file path (absolute or relative) or a CSS string to be linted
input_to_lint
The CSS file path (absolute or relative)
a glob pattern of file(s) to be linted
or a CSS string to be linted
Example:
./path/to/css_file_path_to_lint.css
"./path/to/css_file_path_to_lint.css"
"./path/to/css_file_path_to_lint/*.css"
".test { color: red; }"

Examples:
---------

Lint a CSS file:
php-css-lint ./path/to/css_file_path_to_lint.css
php-css-lint "./path/to/css_file_path_to_lint.css"

Lint a CSS string:
php-css-lint ".test { color: red; }"
Expand All @@ -64,7 +67,7 @@ Examples:
In a terminal, execute:

```sh
php vendor/bin/php-css-lint /path/to/not_valid_file.css
php vendor/bin/php-css-lint "/path/to/not_valid_file.css"
```

Result:
Expand All @@ -77,6 +80,29 @@ Result:
- Unterminated "selector content" (line: 17, char: 0)
```

### Lint file(s) matching a glob pattern

See <https://www.php.net/manual/en/function.glob.php> for supported patterns.

In a terminal, execute:

```sh
php vendor/bin/php-css-lint "/path/to/*.css"
```

Result:

```
# Lint CSS file "/path/to/not_valid_file.css"...
=> CSS file "/path/to/not_valid_file" is not valid:

- Unknown CSS property "bordr-top-style" (line: 8, char: 20)
- Unterminated "selector content" (line: 17, char: 0)

# Lint CSS file "/path/to/valid_file.css"...
=> CSS file "/path/to/valid_file" is valid
```

### Lint a css string

In a terminal, execute:
Expand Down
22 changes: 11 additions & 11 deletions scripts/php-css-lint
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,29 @@ echo PHP_EOL .
'===========================================================' . PHP_EOL . PHP_EOL .
' ____ _ ____ ____ ____ _ _ _ ' . PHP_EOL .
' | _ \| |__ _ __ / ___/ ___/ ___| | | (_)_ __ | |_ ' . PHP_EOL .
' | |_) | \'_ \| \'_ \ | | \___ \___ \ | | | | \'_ \| __|' . PHP_EOL .
" | |_) | '_ \| '_ \ | | \___ \___ \ | | | | '_ \| __|" . PHP_EOL .
' | __/| | | | |_) | | |___ ___) |__) | | |___| | | | | |_ ' . PHP_EOL .
' |_| |_| |_| .__/ \____|____/____/ |_____|_|_| |_|\__|' . PHP_EOL .
' |_| ' . PHP_EOL . PHP_EOL .
'===========================================================' . PHP_EOL . PHP_EOL;

$sComposerAutoloaderWorkingDirectory = getcwd() . '/vendor/autoload.php';
if (is_file($sComposerAutoloaderWorkingDirectory)) {
require_once $sComposerAutoloaderWorkingDirectory;
$composerAutoloaderWorkingDirectory = getcwd() . '/vendor/autoload.php';
if (is_file($composerAutoloaderWorkingDirectory)) {
require_once $composerAutoloaderWorkingDirectory;
}

if (!class_exists('CssLint\CssLint', true)) {
// consider being in bin dir
$sComposerAutoloader = __DIR__ . '/../vendor/autoload.php';
if (!is_file($sComposerAutoloader)) {
$composerAutoloader = __DIR__ . '/../vendor/autoload.php';
if (!is_file($composerAutoloader)) {
// consider being in vendor/neilime/php-css-lint/scripts
$sComposerAutoloader = __DIR__ . '/../../../autoload.php';
$composerAutoloader = __DIR__ . '/../../../autoload.php';
}

require_once $sComposerAutoloader;
require_once $composerAutoloader;
}

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

exit($iReturnCode);
exit($returnCode);
182 changes: 128 additions & 54 deletions src/CssLint/Cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace CssLint;

use RuntimeException;
use Throwable;

/**
* @phpstan-import-type Errors from \CssLint\Linter
* @package CssLint
Expand All @@ -24,58 +27,21 @@
public function run(array $arguments): int
{
$cliArgs = $this->parseArguments($arguments);
if ($cliArgs->filePathOrCssString === null || $cliArgs->filePathOrCssString === '' || $cliArgs->filePathOrCssString === '0') {
if ($cliArgs->input === null || $cliArgs->input === '' || $cliArgs->input === '0') {
$this->printUsage();
return self::RETURN_CODE_SUCCESS;
}

$properties = new \CssLint\Properties();
if ($cliArgs->options !== null && $cliArgs->options !== '' && $cliArgs->options !== '0') {
$options = json_decode($cliArgs->options, true);

if (json_last_error() !== 0) {
$errorMessage = json_last_error_msg();
$this->printError('Unable to parse option argument: ' . $errorMessage);
return self::RETURN_CODE_ERROR;
}

if (!$options) {
$this->printError('Unable to parse empty option argument');
return self::RETURN_CODE_ERROR;
}
try {
$properties = $this->getPropertiesFromOptions($cliArgs->options);

if (!is_array($options)) {
$this->printError('Unable to parse option argument: must be a json object');
return self::RETURN_CODE_ERROR;
}

$properties->setOptions($options);
}
$cssLinter = new Linter($properties);

$cssLinter = new \CssLint\Linter($properties);

$filePathOrCssString = $cliArgs->filePathOrCssString;
if (!file_exists($filePathOrCssString)) {
return $this->lintString($cssLinter, $filePathOrCssString);
}

$filePath = $filePathOrCssString;
if (!is_readable($filePath)) {
$this->printError('File "' . $filePath . '" is not readable');
return $this->lintInput($cssLinter, $cliArgs->input);
} catch (Throwable $throwable) {
$this->printError($throwable->getMessage());
return self::RETURN_CODE_ERROR;
}

return $this->lintFile($cssLinter, $filePath);
}

/**
* Retrieve the parsed Cli arguments from given arguments array
* @param string[] $arguments arguments to be parsed (@see $_SERVER['argv'])
* @return \CssLint\CliArgs an instance of Cli arguments object containing parsed arguments
*/
private function parseArguments(array $arguments): \CssLint\CliArgs
{
return new \CssLint\CliArgs($arguments);
}

/**
Expand All @@ -86,7 +52,7 @@
$this->printLine('Usage:' . PHP_EOL .
'------' . PHP_EOL .
PHP_EOL .
' ' . self::SCRIPT_NAME . " [--options='{ }'] css_file_or_string_to_lint" . PHP_EOL .
' ' . self::SCRIPT_NAME . " [--options='{ }'] input_to_lint" . PHP_EOL .
PHP_EOL .
'Arguments:' . PHP_EOL .
'----------' . PHP_EOL .
Expand All @@ -100,35 +66,143 @@
' Example: --options=\'{ "constructors": {"o" : false}, "allowedIndentationChars": ["\t"] }\'' .
PHP_EOL .
PHP_EOL .
' css_file_or_string_to_lint' . PHP_EOL .
' The CSS file path (absolute or relative) or a CSS string to be linted' . PHP_EOL .
' input_to_lint' . PHP_EOL .
' The CSS file path (absolute or relative)' . PHP_EOL .
' a glob pattern of file(s) to be linted' . PHP_EOL .
' or a CSS string to be linted' . PHP_EOL .
' Example:' . PHP_EOL .
' ./path/to/css_file_path_to_lint.css' . PHP_EOL .
' "./path/to/css_file_path_to_lint.css"' . PHP_EOL .
' "./path/to/css_file_path_to_lint/*.css"' . PHP_EOL .
' ".test { color: red; }"' . PHP_EOL .
PHP_EOL .
'Examples:' . PHP_EOL .
'---------' . PHP_EOL .
PHP_EOL .
' Lint a CSS file:' . PHP_EOL .
' ' . self::SCRIPT_NAME . ' ./path/to/css_file_path_to_lint.css' . PHP_EOL . PHP_EOL .
' ' . self::SCRIPT_NAME . ' "./path/to/css_file_path_to_lint.css"' . PHP_EOL . PHP_EOL .
' Lint a CSS string:' . PHP_EOL .
' ' . self::SCRIPT_NAME . ' ".test { color: red; }"' . PHP_EOL . PHP_EOL .
' ' . self::SCRIPT_NAME . ' ".test { color: red; }"' . PHP_EOL . PHP_EOL .
' Lint with only tabulation as indentation:' . PHP_EOL .
' ' . self::SCRIPT_NAME .
' --options=\'{ "allowedIndentationChars": ["\t"] }\' ".test { color: red; }"' . PHP_EOL .
PHP_EOL . PHP_EOL);
}

/**
* Retrieve the parsed Cli arguments from given arguments array
* @param string[] $arguments arguments to be parsed (@see $_SERVER['argv'])
* @return CliArgs an instance of Cli arguments object containing parsed arguments
*/
private function parseArguments(array $arguments): CliArgs
{
return new CliArgs($arguments);
}

/**
* Retrieve the properties from the given options
* @param string $options the options to be parsed
*/
private function getPropertiesFromOptions(?string $options): Properties
{
$properties = new Properties();
if ($options === null || $options === '' || $options === '0') {
return $properties;
}

$options = json_decode($options, true);

if (json_last_error() !== 0) {
$errorMessage = json_last_error_msg();
throw new RuntimeException('Unable to parse option argument: ' . $errorMessage);
}

if (!$options) {
throw new RuntimeException('Unable to parse empty option argument');

Check warning on line 120 in src/CssLint/Cli.php

View check run for this annotation

Codecov / codecov/patch

src/CssLint/Cli.php#L120

Added line #L120 was not covered by tests
}

if (!is_array($options)) {
throw new RuntimeException('Unable to parse option argument: must be a json object');

Check warning on line 124 in src/CssLint/Cli.php

View check run for this annotation

Codecov / codecov/patch

src/CssLint/Cli.php#L124

Added line #L124 was not covered by tests
}

$properties->setOptions($options);

return $properties;
}

private function lintInput(Linter $cssLinter, string $input): int
{
if (file_exists($input)) {
return $this->lintFile($cssLinter, $input);
}

if ($this->isGlobPattern($input)) {
return $this->lintGlob($input);
}

return $this->lintString($cssLinter, $input);
}

/**
* Checks if a given string is a glob pattern.
*
* A glob pattern typically includes wildcard characters:
* - '*' matches any sequence of characters.
* - '?' matches any single character.
* - '[]' matches any one character in the specified set.
*
* Optionally, if using the GLOB_BRACE flag, brace patterns like {foo,bar} are also valid.
*
* @param string $pattern The string to evaluate.
* @return bool True if the string is a glob pattern, false otherwise.
*/
private function isGlobPattern(string $pattern): bool
{
// Must be one line, no unscaped spaces
if (preg_match('/\s/', $pattern)) {
return false;
}

// Check for basic wildcard characters.
if (str_contains($pattern, '*') || str_contains($pattern, '?') || str_contains($pattern, '[')) {
return true;
}

// Optionally check for brace patterns, used with GLOB_BRACE.
return str_contains($pattern, '{') || str_contains($pattern, '}');

Check warning on line 171 in src/CssLint/Cli.php

View check run for this annotation

Codecov / codecov/patch

src/CssLint/Cli.php#L171

Added line #L171 was not covered by tests
}

private function lintGlob(string $glob): int
{
$cssLinter = new Linter();
$files = glob($glob);
if ($files === [] || $files === false) {
$this->printError('No files found for glob "' . $glob . '"');
return self::RETURN_CODE_ERROR;
}

$returnCode = self::RETURN_CODE_SUCCESS;
foreach ($files as $file) {
$returnCode = max($returnCode, $this->lintFile($cssLinter, $file));
}

return $returnCode;
}

/**
* Performs lint on a given file path
* @param \CssLint\Linter $cssLinter the instance of the linter
* @param Linter $cssLinter the instance of the linter
* @param string $filePath the path of the file to be linted
* @return int the return code related to the execution of the linter
*/
private function lintFile(\CssLint\Linter $cssLinter, string $filePath): int
private function lintFile(Linter $cssLinter, string $filePath): int
{
$this->printLine('# Lint CSS file "' . $filePath . '"...');

if (!is_readable($filePath)) {
$this->printError('File "' . $filePath . '" is not readable');
return self::RETURN_CODE_ERROR;

Check warning on line 203 in src/CssLint/Cli.php

View check run for this annotation

Codecov / codecov/patch

src/CssLint/Cli.php#L202-L203

Added lines #L202 - L203 were not covered by tests
}

if ($cssLinter->lintFile($filePath)) {
$this->printLine("\033[32m => CSS file \"" . $filePath . "\" is valid\033[0m" . PHP_EOL);
return self::RETURN_CODE_SUCCESS;
Expand All @@ -142,11 +216,11 @@

/**
* Performs lint on a given string
* @param \CssLint\Linter $cssLinter the instance of the linter
* @param Linter $cssLinter the instance of the linter
* @param string $stringValue the CSS string to be linted
* @return int the return code related to the execution of the linter
*/
private function lintString(\CssLint\Linter $cssLinter, string $stringValue): int
private function lintString(Linter $cssLinter, string $stringValue): int
{
$this->printLine('# Lint CSS string...');

Expand Down
Loading
Loading