diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..27959b4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,28 @@ +on: [push, pull_request, release, workflow_dispatch] +name: Test +jobs: + test: + runs-on: ubuntu-latest + container: + image: pluswerk/php-dev:nginx-${{ matrix.php }} + options: -t + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + name: 'PHP ${{ matrix.php }}' + steps: + - run: git config --global --add safe.directory /__w/ShellCommandBuilder/ShellCommandBuilder + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + - run: composer install --prefer-dist --no-progress --no-suggest + - run: vendor/bin/grumphp run + - run: script -q -e -c "composer test" + - run: script -q -e -c "composer infection" + - uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./tests/test-results/coverage.xml diff --git a/.github/workflows/shepherd.yml b/.github/workflows/shepherd.yml deleted file mode 100644 index 9a0e125..0000000 --- a/.github/workflows/shepherd.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Run Shepherd - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest - - - name: Run Psalm - run: composer psalm -- --output-format=github --shepherd diff --git a/.gitignore b/.gitignore index 4182f61..a7cc7d2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /tests/test-results/ /tests/.phpunit.result.cache composer.lock +/var/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 45e7b43..0000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: php - -matrix: - include: - - php: 7.2 - dist: bionic - - php: 7.3 - dist: bionic - - php: 7.4 - dist: bionic - - php: nightly - dist: bionic - env: COMPOSER_OPTS="--ignore-platform-req=php" - -cache: - directories: - - $HOME/.composer/cache - -install: - - travis_retry composer install $COMPOSER_OPTS - - # Where PHPUnit v8 is used, we need to replace the config. - - if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]] - && [ ${TRAVIS_PHP_VERSION:2:1} -lt 4 ]; then - cp -v tests/phpunit.legacy.xml tests/phpunit.xml - ; fi - - # PHPUnit 9 supports 7.3, but Infection PHP 18 can't read - # its config so instead we downgrade to PHPUnit 8.5 here. - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.3" ]]; then - travis_retry composer require -W phpunit/phpunit:^8.5 - ; fi - -script: - - vendor/bin/grumphp run - - composer test - - - if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then composer infection; else - vendor/bin/infection --ansi --threads=4 - --initial-tests-php-options="-d xdebug.mode=coverage" - --only-covered --min-msi=100 --min-covered-msi=100 - ; fi - - - composer psalm - -after_success: bash <(curl -s https://codecov.io/bash) diff --git a/composer.json b/composer.json index 323af8e..45e495b 100644 --- a/composer.json +++ b/composer.json @@ -1,53 +1,49 @@ { - "name": "phpsu/shellcommandbuilder", - "description": "Fluid Builder to create shell commands", - "type": "library", - "config": { - "optimize-autoloader": true, - "process-timeout": 0 - }, - "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.4", - "vimeo/psalm": "^4.1", - "infection/infection": "^0.15.3 || ^0.18.2 || ^0.20.1", - "spatie/phpunit-watcher": "^1.24 || dev-master#071fbbf", - "phpunit/php-invoker": "^2.0 || ^3.1", - "pluswerk/grumphp-config": "^4.0.1" - }, - "license": "MIT", - "authors": [ - { - "name": "Christian Rodriguez Benthake", - "email": "git@cben.co" - } - ], - "autoload": { - "psr-4": { - "PHPSu\\ShellCommandBuilder\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "PHPSu\\ShellCommandBuilder\\Tests\\": "tests" - } - }, - "scripts": { - "test": "vendor/bin/phpunit -c tests/phpunit.xml --testdox --color=always", - "test:watch": "vendor/bin/phpunit-watcher watch -c tests/phpunit.xml --testdox", - "infection": "vendor/bin/infection --threads=4 --only-covered --min-msi=100 --min-covered-msi=100 --ansi", - "psalm": "vendor/bin/psalm" - }, - "minimum-stability": "stable", - "require": { - "php": ">=7.2", - "ext-json": "*" - }, - "extra": { - "pluswerk/grumphp-config": { - "auto-setting": false - }, - "grumphp": { - "config-default-path": "vendor/pluswerk/grumphp-config/grumphp.yml" - } + "name": "phpsu/shellcommandbuilder", + "description": "Fluid Builder to create shell commands", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Christian Rodriguez Benthake", + "email": "git@cben.co" + } + ], + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "ext-json": "*" + }, + "require-dev": { + "infection/infection": "^0.28.1 || ^0.29.14", + "phpunit/phpunit": "^10.0 || ^12.0", + "pluswerk/grumphp-config": "^7 || ^10", + "spatie/phpunit-watcher": "^1.24 || dev-master#071fbbf" + }, + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "PHPSu\\ShellCommandBuilder\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "PHPSu\\ShellCommandBuilder\\Tests\\": "tests" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "infection/extension-installer": true, + "phpro/grumphp": true, + "phpstan/extension-installer": true, + "pluswerk/grumphp-config": true + }, + "optimize-autoloader": true, + "process-timeout": 0 + }, + "scripts": { + "infection": "XDEBUG_MODE=coverage vendor/bin/infection --threads=4 --only-covered --min-msi=99 --min-covered-msi=99 --ansi", + "test": "XDEBUG_MODE=coverage vendor/bin/phpunit -c tests/phpunit.xml --testdox --color=always", + "test:watch": "XDEBUG_MODE=coverage vendor/bin/phpunit-watcher watch -c tests/phpunit.xml --testdox" + } } diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index a4cddad..0000000 --- a/docs/api.md +++ /dev/null @@ -1 +0,0 @@ -> todo: add api documentation diff --git a/grumphp.yml b/grumphp.yml new file mode 100644 index 0000000..3a60a5a --- /dev/null +++ b/grumphp.yml @@ -0,0 +1,16 @@ +imports: + - { resource: vendor/pluswerk/grumphp-config/grumphp.yml } +parameters: + convention.process_timeout: 240 + convention.security_checker_blocking: true + convention.jsonlint_ignore_pattern: { } + convention.xmllint_ignore_pattern: { } + convention.yamllint_ignore_pattern: { } + convention.phpcslint_ignore_pattern: { } + convention.phpcslint_exclude: { } + convention.xlifflint_ignore_pattern: { } + convention.rector_ignore_pattern: { } + convention.rector_enabled: true + convention.rector_config: rector.php + convention.rector_clear-cache: false + convention.phpstan_level: null diff --git a/infection.json b/infection.json index 5fb033a..3b4c828 100644 --- a/infection.json +++ b/infection.json @@ -10,11 +10,7 @@ }, "logs": { "text": "tests/test-results/infection.log", - "summary": "tests/test-results/summary.log", - "perMutator": "tests/test-results/per-mutator.md", - "badge": { - "branch": "master" - } + "html": "tests/test-results/infection.html" }, "mutators": { "@default": true, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..6236ff8 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,19 @@ +parameters: + ignoreErrors: + - + message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 3 + path: packages/grumphp-xliff-task/src/XliffLinter.php + + - + message: '#^Instanceof between DOMElement and DOMElement will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 3 + path: packages/grumphp-xliff-task/src/XliffLinter.php + + - + message: '#^Method Andersundsehr\\RectorP\\PartialCommand\:\:getAllFiles\(\) should return list\ but returns array\\.$#' + identifier: return.type + count: 1 + path: packages/rector-p/src/PartialCommand.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e178ba6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + - vendor/andersundsehr/phpstan-git-files/extension.php + +parameters: + level: 8 + reportUnmatchedIgnoredErrors: false diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..0f267f7 --- /dev/null +++ b/rector.php @@ -0,0 +1,42 @@ +parallel(); + $rectorConfig->importNames(); + $rectorConfig->importShortClasses(); + $rectorConfig->cacheClass(FileCacheStorage::class); + $rectorConfig->cacheDirectory('./var/cache/rector'); + + $rectorConfig->paths( + array_filter(explode("\n", (string)shell_exec("git ls-files | xargs ls -d 2>/dev/null | grep -E '\.(php)$'"))) + ); + + // define sets of rules + $rectorConfig->sets( + [ + ...RectorSettings::sets(true), + ...RectorSettings::setsTypo3(false), + ] + ); + + // remove some rules + // ignore some files + $rectorConfig->skip( + [ + ...RectorSettings::skip(), + ...RectorSettings::skipTypo3(), + + /** + * rector should not touch these files + */ + //__DIR__ . '/src/Example', + //__DIR__ . '/src/Example.php', + ] + ); +}; diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php index 4424600..a3d5a42 100644 --- a/src/Collection/AbstractCollection.php +++ b/src/Collection/AbstractCollection.php @@ -13,22 +13,15 @@ */ abstract class AbstractCollection implements ShellInterface { - /** @var CollectionTuple|null */ - protected $tuple; + protected CollectionTuple|null $tuple = null; - /** - * @param string|ShellInterface $command - * @param string $join - * @return CollectionTuple - * @throws ShellBuilderException - */ - protected function toTuple($command, string $join): CollectionTuple + protected function toTuple(ShellInterface|string $command, string $join): CollectionTuple { return CollectionTuple::create($command, $join); } /** - * @return array> + * @return array> * @throws ShellBuilderException */ public function __toArray(): array @@ -36,6 +29,7 @@ public function __toArray(): array if ($this->tuple === null) { throw new ShellBuilderException('Tuple has not been set yet - collection cannot be parsed to array'); } + return $this->tuple->__toArray(); } diff --git a/src/Collection/CollectionTuple.php b/src/Collection/CollectionTuple.php index ad2af70..1e8d538 100644 --- a/src/Collection/CollectionTuple.php +++ b/src/Collection/CollectionTuple.php @@ -4,7 +4,6 @@ namespace PHPSu\ShellCommandBuilder\Collection; -use PHPSu\ShellCommandBuilder\Exception\ShellBuilderException; use PHPSu\ShellCommandBuilder\ShellInterface; /** @@ -13,27 +12,16 @@ */ final class CollectionTuple implements ShellInterface { - /** @var string */ - protected $join = ''; + private string $join = ''; - /** @var string|ShellInterface */ - protected $value = ''; - /** @var bool */ - private $noSpaceBeforeJoin = false; - /** @var bool */ - private $noSpaceAfterJoin = false; + private ShellInterface|string $value = ''; - /** - * @param string|ShellInterface|mixed $value - * @param string $join - * @return static - * @throws ShellBuilderException - */ - public static function create($value, string $join = ''): self + private bool $noSpaceBeforeJoin = false; + + private bool $noSpaceAfterJoin = false; + + public static function create(ShellInterface|string $value, string $join = ''): self { - if (!(is_string($value) || $value instanceof ShellInterface)) { - throw new ShellBuilderException('Value must be of Type string or an instance of ShellInterface'); - } $tuple = new self(); $tuple->value = $value; $tuple->join = $join; @@ -53,7 +41,7 @@ public function noSpaceAfterJoin(bool $space): self } /** - * @return array> + * @return array> */ public function __toArray(): array { diff --git a/src/Collection/Pipeline.php b/src/Collection/Pipeline.php index 7066a90..da6daae 100644 --- a/src/Collection/Pipeline.php +++ b/src/Collection/Pipeline.php @@ -6,7 +6,6 @@ use PHPSu\ShellCommandBuilder\Definition\ControlOperator; use PHPSu\ShellCommandBuilder\Exception\ShellBuilderException; -use PHPSu\ShellCommandBuilder\ShellCommand; use PHPSu\ShellCommandBuilder\ShellInterface; /** @@ -15,24 +14,14 @@ */ final class Pipeline extends AbstractCollection { - /** - * @param string|ShellInterface $command - * @return $this - * @throws ShellBuilderException - */ - public static function pipe($command): self + public static function pipe(ShellInterface|string $command): self { $pipeline = new self(); $pipeline->tuple = $pipeline->toTuple($command, ControlOperator::PIPELINE); return $pipeline; } - /** - * @param string|ShellInterface $command - * @return $this - * @throws ShellBuilderException - */ - public static function pipeErrorForward($command): self + public static function pipeErrorForward(ShellInterface|string $command): self { $pipeline = new self(); $pipeline->tuple = $pipeline->toTuple($command, ControlOperator::PIPELINE_WITH_STDERR_FORWARD); diff --git a/src/Collection/Redirection.php b/src/Collection/Redirection.php index b0040a2..dbd60c6 100644 --- a/src/Collection/Redirection.php +++ b/src/Collection/Redirection.php @@ -5,7 +5,6 @@ namespace PHPSu\ShellCommandBuilder\Collection; use PHPSu\ShellCommandBuilder\Definition\RedirectOperator; -use PHPSu\ShellCommandBuilder\Exception\ShellBuilderException; use PHPSu\ShellCommandBuilder\ShellInterface; /** @@ -14,63 +13,33 @@ */ final class Redirection extends AbstractCollection { - /** - * @param string|ShellInterface $value - * @param bool $append - * @return $this - * @throws ShellBuilderException - */ - public static function redirectOutput($value, bool $append): self + public static function redirectOutput(ShellInterface|string $value, bool $append): self { $redirect = new self(); $redirect->tuple = CollectionTuple::create($value, $append ? RedirectOperator::STDOUT_LEFT_APPEND : RedirectOperator::STDOUT_LEFT_INSERT); return $redirect; } - /** - * @param string|ShellInterface $value - * @return $this - * @throws ShellBuilderException - */ - public static function redirectInput($value): self + public static function redirectInput(ShellInterface|string $value): self { $redirect = new self(); $redirect->tuple = CollectionTuple::create($value, RedirectOperator::STDIN_RIGHT); return $redirect; } - /** - * @param string|ShellInterface $value - * @return $this - * @throws ShellBuilderException - */ - public static function redirectError($value): self + public static function redirectError(ShellInterface|string $value): self { $redirect = new self(); $redirect->tuple = CollectionTuple::create($value, RedirectOperator::FILE_DESCRIPTOR_ERR . RedirectOperator::STDOUT_LEFT_INSERT); return $redirect; } - /** - * @param string|ShellInterface $value - * @param bool $toLeft - * @return $this - * @throws ShellBuilderException - */ - public static function redirectBetweenFiles($value, bool $toLeft): self + public static function redirectBetweenFiles(ShellInterface|string $value, bool $toLeft): self { return self::redirectBetweenDescriptors($value, $toLeft); } - /** - * @param string|ShellInterface $value - * @param bool $toLeft - * @param int|null $firstDescriptor - * @param int|null $secondDescriptor - * @return static - * @throws ShellBuilderException - */ - public static function redirectBetweenDescriptors($value, bool $toLeft, int $firstDescriptor = null, int $secondDescriptor = null): self + public static function redirectBetweenDescriptors(ShellInterface|string $value, bool $toLeft, ?int $firstDescriptor = null, ?int $secondDescriptor = null): self { $redirect = new self(); $redirect->tuple = CollectionTuple::create($value, sprintf( diff --git a/src/Collection/ShellList.php b/src/Collection/ShellList.php index 5ba39c9..a0d406a 100644 --- a/src/Collection/ShellList.php +++ b/src/Collection/ShellList.php @@ -16,12 +16,8 @@ final class ShellList extends AbstractCollection { /** * Returns something like: || echo "hello world" - * - * @param string|ShellInterface $command - * @return $this - * @throws ShellBuilderException */ - public static function addOr($command): self + public static function addOr(ShellInterface|string $command): self { $list = new self(); $list->tuple = $list->toTuple($command, ControlOperator::OR_OPERATOR); @@ -30,12 +26,8 @@ public static function addOr($command): self /** * Returns something like: && echo "hello world" - * - * @param string|ShellInterface $command - * @return $this - * @throws ShellBuilderException */ - public static function addAnd($command): self + public static function addAnd(ShellInterface|string $command): self { $list = new self(); $list->tuple = $list->toTuple($command, ControlOperator::AND_OPERATOR); @@ -44,12 +36,8 @@ public static function addAnd($command): self /** * Returns something like: ; echo "hello world" - * - * @param string|ShellInterface $command - * @return $this - * @throws ShellBuilderException */ - public static function add($command): self + public static function add(ShellInterface|string $command): self { $list = new self(); $list->tuple = $list->toTuple($command, ControlOperator::COMMAND_DELIMITER); @@ -58,12 +46,8 @@ public static function add($command): self /** * Returns something like: & echo "hello world" - * - * @param string|ShellInterface $command - * @return static - * @throws ShellBuilderException */ - public static function async($command): self + public static function async(ShellInterface|string $command): self { $list = new self(); $list->tuple = $list->toTuple($command, ControlOperator::BASH_AMPERSAND); diff --git a/src/Conditional/ArithmeticExpression.php b/src/Conditional/ArithmeticExpression.php index 078064b..4e619cf 100644 --- a/src/Conditional/ArithmeticExpression.php +++ b/src/Conditional/ArithmeticExpression.php @@ -9,17 +9,15 @@ final class ArithmeticExpression extends BasicExpression { - public static function create(bool $useBashBrackets = true, bool $negateExpression = false): ArithmeticExpression + public static function create(bool $useBashBrackets = true, bool $negateExpression = false): static { return new self($useBashBrackets, $negateExpression); } /** - * @param string|ShellInterface $arg1 - * @param string|ShellInterface $arg2 * @return $this */ - public function equal($arg1, $arg2): self + public function equal(ShellInterface|string $arg1, ShellInterface|string $arg2): self { $this->operator = ConditionalOperator::ARTITH_EQUAL; $this->compare = $arg1; @@ -28,11 +26,9 @@ public function equal($arg1, $arg2): self } /** - * @param string|ShellInterface $arg1 - * @param string|ShellInterface $arg2 * @return $this */ - public function notEqual($arg1, $arg2): self + public function notEqual(ShellInterface|string $arg1, ShellInterface|string $arg2): self { $this->operator = ConditionalOperator::ARTITH_NOT_EQUAL; $this->compare = $arg1; @@ -41,11 +37,9 @@ public function notEqual($arg1, $arg2): self } /** - * @param string|ShellInterface $arg1 - * @param string|ShellInterface $arg2 * @return $this */ - public function less($arg1, $arg2): self + public function less(ShellInterface|string $arg1, ShellInterface|string $arg2): self { $this->operator = ConditionalOperator::ARTITH_LESS_THAN; $this->compare = $arg1; @@ -54,11 +48,9 @@ public function less($arg1, $arg2): self } /** - * @param string|ShellInterface $arg1 - * @param string|ShellInterface $arg2 * @return $this */ - public function greater($arg1, $arg2): self + public function greater(ShellInterface|string $arg1, ShellInterface|string $arg2): self { $this->operator = ConditionalOperator::ARTITH_GREATER_THAN; $this->compare = $arg1; @@ -67,11 +59,9 @@ public function greater($arg1, $arg2): self } /** - * @param string|ShellInterface $arg1 - * @param string|ShellInterface $arg2 * @return $this */ - public function lessEqual($arg1, $arg2): self + public function lessEqual(ShellInterface|string $arg1, ShellInterface|string $arg2): self { $this->operator = ConditionalOperator::ARTITH_LESS_EQUAL; $this->compare = $arg1; @@ -80,11 +70,9 @@ public function lessEqual($arg1, $arg2): self } /** - * @param string|ShellInterface $arg1 - * @param string|ShellInterface $arg2 * @return $this */ - public function greaterEqual($arg1, $arg2): self + public function greaterEqual(ShellInterface|string $arg1, ShellInterface|string $arg2): self { $this->operator = ConditionalOperator::ARTITH_GREATER_EQUAL; $this->compare = $arg1; diff --git a/src/Conditional/BasicExpression.php b/src/Conditional/BasicExpression.php index a211958..50672da 100644 --- a/src/Conditional/BasicExpression.php +++ b/src/Conditional/BasicExpression.php @@ -13,26 +13,23 @@ */ abstract class BasicExpression implements ShellInterface { - /** - * This is not POSIX-compatible (only eg. Korn and Bash), beware before using it - * @var bool - */ - protected $bashEnhancedBrackets = false; - /** @var bool this is always double quoted */ - protected $escapedValue = false; - /** @var bool */ - private $negateExpression; - /** @var string|ShellInterface */ - protected $compare = ''; - /** @var string|ShellInterface */ - protected $compareWith = ''; - /** @var string */ - protected $operator = ''; - - public function __construct(bool $useBashBrackets, bool $negateExpression) - { - $this->negateExpression = $negateExpression; - $this->bashEnhancedBrackets = $useBashBrackets; + /** @var bool this is always double-quoted */ + protected bool $escapedValue = false; + + + protected ShellInterface|string $compare = ''; + + protected ShellInterface|string $compareWith = ''; + + protected string $operator = ''; + + public function __construct( + /** + * This is not POSIX-compatible (only eg. Korn and Bash), beware of using it + */ + protected bool $bashEnhancedBrackets, + private readonly bool $negateExpression + ) { } public function escapeValue(bool $enable): BasicExpression @@ -41,42 +38,29 @@ public function escapeValue(bool $enable): BasicExpression return $this; } - /** - * @todo with min. support of php 7.4 this can be fully implemented here - * @param bool $useBashBrackets - * @param bool $negateExpression - * @return mixed - */ - abstract public static function create(bool $useBashBrackets = false, bool $negateExpression = false); + abstract public static function create(bool $useBashBrackets = false, bool $negateExpression = false): static; /** - * @param string|ShellInterface $value * @return string|array */ - private function getValueDebug($value) + private function getValueDebug(ShellInterface|string $value): array|string { if ($value instanceof ShellInterface) { return $value->__toArray(); } + return $value; } - /** - * @param string|ShellInterface $value - * @return string - */ - private function getValue($value): string + private function getValue(ShellInterface|string $value): string { - $return = $value; - if ($value instanceof ShellInterface) { - $return = $value->__toString(); - } if ($this->escapedValue) { /** @psalm-suppress ImplicitToStringCast */ - $return = sprintf('"%s"', $return); + return sprintf('"%s"', $value); } - return $return; + + return (string)$value; } public function __toString(): string diff --git a/src/Conditional/FileExpression.php b/src/Conditional/FileExpression.php index 594710d..f704350 100644 --- a/src/Conditional/FileExpression.php +++ b/src/Conditional/FileExpression.php @@ -9,228 +9,147 @@ final class FileExpression extends BasicExpression { - protected $escapedValue = true; + protected bool $escapedValue = true; - public static function create(bool $useBashBrackets = true, bool $negateExpression = false): FileExpression + public static function create(bool $useBashBrackets = true, bool $negateExpression = false): static { return new self($useBashBrackets, $negateExpression); } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function exists($file): FileExpression + public function exists(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXISTS; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function existsBlockSpecial($file): FileExpression + public function existsBlockSpecial(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXISTS_BLOCK_SPECIAL; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function existsCharacterSpecial($file): FileExpression + public function existsCharacterSpecial(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXISTS_CHARACTER_SPECIAL; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $directory - * @return $this - */ - public function isDirectory($directory): FileExpression + public function isDirectory(ShellInterface|string $directory): FileExpression { $this->operator = ConditionalOperator::FILE_EXISTS_IS_DIRECTORY; $this->compareWith = $directory; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function isRegularFile($file): FileExpression + public function isRegularFile(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXISTS_REGULAR_FILE; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function hasSetGroupIDBit($file): FileExpression + public function hasSetGroupIDBit(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXISTS_HAS_SET_GROUP_ID; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function symbolicLink($file): FileExpression + public function symbolicLink(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_SYMBOLIC_LINK; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function withStickyBit($file): FileExpression + public function withStickyBit(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_STICKY_BIT; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function isNamedPipe($file): FileExpression + public function isNamedPipe(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_NAMED_PIPE; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function isReadably($file): FileExpression + public function isReadably(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_READABLE; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function notEmpty($file): FileExpression + public function notEmpty(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_NOT_EMPTY; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function openReferringToTerminal($file): FileExpression + public function openReferringToTerminal(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_OPEN_REFERING_TO_TERMINAL; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function hasSetUserIDBit($file): FileExpression + public function hasSetUserIDBit(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_HAS_SET_USER_ID; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function writable($file): FileExpression + public function writable(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_WRITABLE; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function executable($file): FileExpression + public function executable(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_EXECUTABLE; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function ownedByGroupId($file): FileExpression + public function ownedByGroupId(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_OWNED_BY_GROUP_ID; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function modifiedSinceLastRead($file): FileExpression + public function modifiedSinceLastRead(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_MODIFIED_SINCE_LAST_READ; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function ownedByUserId($file): FileExpression + public function ownedByUserId(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_OWNED_BY_USER_ID; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $file - * @return $this - */ - public function isSocket($file): FileExpression + public function isSocket(ShellInterface|string $file): FileExpression { $this->operator = ConditionalOperator::FILE_EXSITS_IS_SOCKET; $this->compareWith = $file; return $this; } - /** - * @param string|ShellInterface $fileA - * @param string|ShellInterface $fileB - * @return $this - */ - public function refersToSameDevice($fileA, $fileB): FileExpression + public function refersToSameDevice(ShellInterface|string $fileA, ShellInterface|string $fileB): FileExpression { $this->operator = ConditionalOperator::FILE_REFERS_TO_SAME_DEVICE; $this->compare = $fileA; @@ -238,12 +157,7 @@ public function refersToSameDevice($fileA, $fileB): FileExpression return $this; } - /** - * @param string|ShellInterface $fileA - * @param string|ShellInterface $fileB - * @return $this - */ - public function isNewerThan($fileA, $fileB): FileExpression + public function isNewerThan(ShellInterface|string $fileA, ShellInterface|string $fileB): FileExpression { $this->operator = ConditionalOperator::FILE_IS_NEWER_THAN; $this->compare = $fileA; @@ -251,12 +165,7 @@ public function isNewerThan($fileA, $fileB): FileExpression return $this; } - /** - * @param string|ShellInterface $fileA - * @param string|ShellInterface $fileB - * @return $this - */ - public function isOlderThan($fileA, $fileB): FileExpression + public function isOlderThan(ShellInterface|string $fileA, ShellInterface|string $fileB): FileExpression { $this->operator = ConditionalOperator::FILE_IS_OLDER_THAN; $this->compare = $fileA; diff --git a/src/Conditional/ShellExpression.php b/src/Conditional/ShellExpression.php index 1b48722..c84499f 100644 --- a/src/Conditional/ShellExpression.php +++ b/src/Conditional/ShellExpression.php @@ -9,18 +9,17 @@ final class ShellExpression extends BasicExpression { - protected $escapedValue = true; + protected bool $escapedValue = true; - public static function create(bool $useBashBrackets = true, bool $negateExpression = false): ShellExpression + public static function create(bool $useBashBrackets = true, bool $negateExpression = false): static { return new self($useBashBrackets, $negateExpression); } /** - * @param string|ShellInterface $optname * @return $this */ - public function isOptnameEnabled($optname): self + public function isOptnameEnabled(ShellInterface|string $optname): self { $this->operator = ConditionalOperator::SHELL_OPTNAME_ENABLED; $this->compareWith = $optname; @@ -28,10 +27,9 @@ public function isOptnameEnabled($optname): self } /** - * @param string|ShellInterface $variable * @return $this */ - public function isVariableSet($variable): self + public function isVariableSet(ShellInterface|string $variable): self { $this->operator = ConditionalOperator::SHELL_VARNAME_SET; $this->compareWith = $variable; @@ -39,10 +37,9 @@ public function isVariableSet($variable): self } /** - * @param string|ShellInterface $variable * @return $this */ - public function isVariableSetWithNamedReference($variable): self + public function isVariableSetWithNamedReference(ShellInterface|string $variable): self { $this->operator = ConditionalOperator::SHELL_VARNAME_SET_NAMED_REFERENCE; $this->compareWith = $variable; diff --git a/src/Conditional/StringExpression.php b/src/Conditional/StringExpression.php index 11952cc..557e806 100644 --- a/src/Conditional/StringExpression.php +++ b/src/Conditional/StringExpression.php @@ -9,18 +9,17 @@ final class StringExpression extends BasicExpression { - protected $escapedValue = true; + protected bool $escapedValue = true; - public static function create(bool $useBashBrackets = true, bool $negateExpression = false): StringExpression + public static function create(bool $useBashBrackets = true, bool $negateExpression = false): static { return new self($useBashBrackets, $negateExpression); } /** - * @param string|ShellInterface $string * @return $this */ - public function lenghtZero($string): self + public function lenghtZero(ShellInterface|string $string): self { $this->operator = ConditionalOperator::STRING_LENGHT_ZERO; $this->compareWith = $string; @@ -28,10 +27,9 @@ public function lenghtZero($string): self } /** - * @param string|ShellInterface $string * @return $this */ - public function lengthNotZero($string): self + public function lengthNotZero(ShellInterface|string $string): self { $this->operator = ConditionalOperator::STRING_LENGHT_NOT_ZERO; $this->compareWith = $string; @@ -39,11 +37,9 @@ public function lengthNotZero($string): self } /** - * @param string|ShellInterface $stringA - * @param string|ShellInterface $stringB * @return $this */ - public function eq($stringA, $stringB): self + public function eq(ShellInterface|string $stringA, ShellInterface|string $stringB): self { $this->operator = ConditionalOperator::STRING_EQUAL; $this->compareWith = $stringB; @@ -52,11 +48,9 @@ public function eq($stringA, $stringB): self } /** - * @param string|ShellInterface $stringA - * @param string|ShellInterface $stringB * @return $this */ - public function equal($stringA, $stringB): self + public function equal(ShellInterface|string $stringA, ShellInterface|string $stringB): self { $this->operator = ConditionalOperator::STRING_EQUAL_BASH; $this->bashEnhancedBrackets = true; @@ -66,11 +60,9 @@ public function equal($stringA, $stringB): self } /** - * @param string|ShellInterface $stringA - * @param string|ShellInterface $stringB * @return $this */ - public function notEqual($stringA, $stringB): self + public function notEqual(ShellInterface|string $stringA, ShellInterface|string $stringB): self { $this->operator = ConditionalOperator::STRING_NOT_EQUAL; $this->compareWith = $stringB; @@ -79,11 +71,9 @@ public function notEqual($stringA, $stringB): self } /** - * @param string|ShellInterface $stringA - * @param string|ShellInterface $stringB * @return $this */ - public function sortsBefore($stringA, $stringB): self + public function sortsBefore(ShellInterface|string $stringA, ShellInterface|string $stringB): self { $this->operator = ConditionalOperator::STRING_SORTS_BEFORE; $this->compareWith = $stringB; @@ -92,11 +82,9 @@ public function sortsBefore($stringA, $stringB): self } /** - * @param string|ShellInterface $stringA - * @param string|ShellInterface $stringB * @return $this */ - public function sortsAfter($stringA, $stringB): self + public function sortsAfter(ShellInterface|string $stringA, ShellInterface|string $stringB): self { $this->operator = ConditionalOperator::STRING_SORTS_AFTER; $this->compareWith = $stringB; diff --git a/src/Definition/ConditionalOperator.php b/src/Definition/ConditionalOperator.php index e711aa5..bc2edaa 100644 --- a/src/Definition/ConditionalOperator.php +++ b/src/Definition/ConditionalOperator.php @@ -12,53 +12,91 @@ final class ConditionalOperator { public const BRACKET_LEFT = '['; + public const BRACKET_RIGHT = ']'; + public const BRACKET_LEFT_BASH = '[['; + public const BRACKET_RIGHT_BASH = ']]'; /* FILE OPERATOR */ public const FILE_EXISTS = '-a'; + public const FILE_EXISTS_BLOCK_SPECIAL = '-b'; + public const FILE_EXISTS_CHARACTER_SPECIAL = '-c'; + public const FILE_EXISTS_IS_DIRECTORY = '-d'; + public const FILE_EXISTS_REGULAR_FILE = '-f'; + public const FILE_EXISTS_HAS_SET_GROUP_ID = '-g'; + public const FILE_EXSITS_SYMBOLIC_LINK = '-h'; + public const FILE_EXSITS_STICKY_BIT = '-k'; + public const FILE_EXSITS_NAMED_PIPE = '-p'; + public const FILE_EXSITS_READABLE = '-r'; + public const FILE_EXSITS_NOT_EMPTY = '-s'; + public const FILE_EXSITS_OPEN_REFERING_TO_TERMINAL = '-t'; + public const FILE_EXSITS_HAS_SET_USER_ID = '-u'; + public const FILE_EXSITS_WRITABLE = '-w'; + public const FILE_EXSITS_EXECUTABLE = '-x'; + public const FILE_EXSITS_OWNED_BY_GROUP_ID = '-G'; + public const FILE_EXSITS_MODIFIED_SINCE_LAST_READ = '-N'; + public const FILE_EXSITS_OWNED_BY_USER_ID = '-O'; + public const FILE_EXSITS_IS_SOCKET = '-S'; + public const FILE_REFERS_TO_SAME_DEVICE = '-ef'; + public const FILE_IS_NEWER_THAN = '-nt'; + public const FILE_IS_OLDER_THAN = '-ot'; /* SHELL OPERATOR */ public const SHELL_OPTNAME_ENABLED = '-o'; + public const SHELL_VARNAME_SET = '-v'; + public const SHELL_VARNAME_SET_NAMED_REFERENCE = '-R'; /* ARITHMETIC / STRING OPERATORS */ public const STRING_LENGHT_ZERO = '-z'; + public const STRING_LENGHT_NOT_ZERO = '-n'; + /** @var string POSIX-compatible, should be used for the `test` command */ public const STRING_EQUAL = '='; + /** @var string this is used within [[ ]] */ public const STRING_EQUAL_BASH = '=='; + public const STRING_NOT_EQUAL = '!='; + public const STRING_SORTS_BEFORE = '<'; + public const STRING_SORTS_AFTER = '>'; + public const ARTITH_EQUAL = '-eq'; + public const ARTITH_NOT_EQUAL = '-ne'; + public const ARTITH_LESS_THAN = '-lt'; + public const ARTITH_GREATER_THAN = '-gt'; + public const ARTITH_LESS_EQUAL = '-le'; + public const ARTITH_GREATER_EQUAL = '-ge'; } diff --git a/src/Definition/ControlOperator.php b/src/Definition/ControlOperator.php index bfa99b4..6646137 100644 --- a/src/Definition/ControlOperator.php +++ b/src/Definition/ControlOperator.php @@ -7,14 +7,24 @@ final class ControlOperator { public const AND_OPERATOR = '&&'; + public const OR_OPERATOR = '||'; + public const BASH_AMPERSAND = '&'; + public const COMMAND_DELIMITER = ';'; + public const DOUBLE_SEMICOLON = ';;'; + public const BLOCK_DEFINITON_OPEN = '('; + public const BLOCK_DEFINITON_CLOSE = ')'; + public const CURLY_BLOCK_DEFINITON_OPEN = '{'; + public const CURLY_BLOCK_DEFINITON_CLOSE = '}'; + public const PIPELINE = '|'; + public const PIPELINE_WITH_STDERR_FORWARD = '|&'; } diff --git a/src/Definition/GroupType.php b/src/Definition/GroupType.php index 3403a51..1b96b63 100644 --- a/src/Definition/GroupType.php +++ b/src/Definition/GroupType.php @@ -7,6 +7,8 @@ final class GroupType { public const NO_GROUP = 0; + public const SUBSHELL_GROUP = 1; + public const SAMESHELL_GROUP = 2; } diff --git a/src/Definition/Pattern.php b/src/Definition/Pattern.php index 253b1e1..373aa8f 100644 --- a/src/Definition/Pattern.php +++ b/src/Definition/Pattern.php @@ -12,8 +12,10 @@ final class Pattern public const SHELLWORD_PATTERN = <<([^\s\\\\\'\"]+)|'([^\']*)'|"((?:[^\"\\\\]|\\\\.)*)"|(\\\\.?)|(\S))(\s|\z)?/m REGEXP; + // @see https://github.com/jimmycuadra/rust-shellwords/blob/master/src/lib.rs#L104 public const METACHAR_PATTERN = /** @lang PhpRegExp */ '/\\\\([$`"\\\\\n])/'; + public const ESCAPE_PATTERN = /** @lang PhpRegExp */ '/\\\\(.)/'; /** @@ -21,8 +23,6 @@ final class Pattern * The pattern being used is based on a combination of the ruby and rust implementation of the same functionality * It derives from the original UNIX Bourne documentation * - * @psalm-pure - * @param string $input * @return array * @throws ShellBuilderException */ @@ -34,18 +34,21 @@ public static function split(string $input): array preg_match_all(self::SHELLWORD_PATTERN, $input . ' ', $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL); /** @var array $match */ foreach ($matches as $match) { - if ($match[5] ?? false) { + if ($match[5] ?? '') { throw new ShellBuilderException('The given input has mismatching Quotes'); } + $doubleQuoted = ''; if (isset($match[3])) { $doubleQuoted = preg_replace(self::METACHAR_PATTERN, '$1', $match[3]); } + $escaped = ''; if (isset($match[4])) { $escaped = preg_replace(self::ESCAPE_PATTERN, '$1', $match[4]); $escaped .= ''; } + $field .= implode('', [$match[1], $match[2] ?? '', $doubleQuoted, $escaped]); $seperator = $match[6] ?? ''; if ($seperator !== '') { @@ -53,6 +56,7 @@ public static function split(string $input): array $field = ''; } } + return $words; } } diff --git a/src/Definition/RedirectOperator.php b/src/Definition/RedirectOperator.php index 51bee33..b43f8ba 100644 --- a/src/Definition/RedirectOperator.php +++ b/src/Definition/RedirectOperator.php @@ -10,17 +10,30 @@ final class RedirectOperator { public const STDOUT_LEFT_INSERT = '>'; + public const STDOUT_LEFT_INSERT_NEWFILE = '>|'; + public const STDOUT_LEFT_APPEND = '>>'; + public const STDIN_RIGHT = '<'; + public const FILE_DESCRIPTOR_IN = 0; + public const FILE_DESCRIPTOR_OUT = 1; + public const FILE_DESCRIPTOR_ERR = 2; + public const REDIRECT_LEFT = '>&'; + public const REDIRECT_RIGHT = '<&'; + public const OPEN_FILEDESCRIPTOR_RW = '<>'; + public const ERR_TO_OUT_REDIRECT = self::FILE_DESCRIPTOR_ERR . self::REDIRECT_LEFT . self::FILE_DESCRIPTOR_OUT; + public const HERE_STRING = '<<<'; + public const HERE_DOCUMENT = '<<'; + public const APPEND_STDOUT_ERR = '&>>'; } diff --git a/src/Exception/ShellBuilderException.php b/src/Exception/ShellBuilderException.php index 48206c9..bd72ba8 100644 --- a/src/Exception/ShellBuilderException.php +++ b/src/Exception/ShellBuilderException.php @@ -4,7 +4,8 @@ namespace PHPSu\ShellCommandBuilder\Exception; -final class ShellBuilderException extends \Exception -{ +use Exception; +final class ShellBuilderException extends Exception +{ } diff --git a/src/Literal/ShellArgument.php b/src/Literal/ShellArgument.php index 7abd839..6d1b275 100644 --- a/src/Literal/ShellArgument.php +++ b/src/Literal/ShellArgument.php @@ -13,23 +13,21 @@ */ final class ShellArgument extends ShellWord { - protected $isArgument = true; - protected $delimiter = ''; - - /** - * ShellArgument constructor. - * @param ShellInterface|string $argument - */ - public function __construct($argument) + protected const IS_ARGUMENT = true; + + protected string $delimiter = ''; + + public function __construct(ShellInterface|string $argument) { parent::__construct('', $argument); } protected function validate(): void { - if (is_string($this->value) && empty($this->value)) { + if (!$this->value) { throw new ShellBuilderException('Argument cant be empty'); } + parent::validate(); } } diff --git a/src/Literal/ShellEnvironmentVariable.php b/src/Literal/ShellEnvironmentVariable.php index 7a2c1cf..1a45a70 100644 --- a/src/Literal/ShellEnvironmentVariable.php +++ b/src/Literal/ShellEnvironmentVariable.php @@ -13,17 +13,17 @@ */ final class ShellEnvironmentVariable extends ShellWord { - protected $isEnvironmentVariable = true; - protected $useAssignOperator = true; - protected $nameUpperCase = true; + protected const IS_ENVIRONMENT_VARIABLE = true; + + protected bool $useAssignOperator = true; + + protected bool $nameUpperCase = true; /** * ShellArgument constructor. - * @param string $option - * @param ShellInterface|string $value * @throws ShellBuilderException */ - public function __construct(string $option, $value) + public function __construct(string $option, ShellInterface|string $value) { parent::__construct($option, $value); } diff --git a/src/Literal/ShellExecutable.php b/src/Literal/ShellExecutable.php index 6681418..dbf8e0f 100644 --- a/src/Literal/ShellExecutable.php +++ b/src/Literal/ShellExecutable.php @@ -10,12 +10,15 @@ */ final class ShellExecutable extends ShellWord { - protected $isArgument = true; - protected $spaceAfterValue = false; - protected $isEscaped = false; - protected $delimiter = ''; - protected $prefix = ''; - protected $suffix = ''; + protected const IS_ARGUMENT = true; + + protected bool $spaceAfterValue = false; + + protected bool $isEscaped = false; + + protected string $delimiter = ''; + + protected string $suffix = ''; public function __construct(string $executable) { diff --git a/src/Literal/ShellOption.php b/src/Literal/ShellOption.php index 83056b0..f2c19a2 100644 --- a/src/Literal/ShellOption.php +++ b/src/Literal/ShellOption.php @@ -13,20 +13,20 @@ */ final class ShellOption extends ShellWord { - protected $isOption = true; - protected $prefix = ShellWord::OPTION_CONTROL; + protected const IS_OPTION = true; + + protected string $prefix = ShellWord::OPTION_CONTROL; /** * ShellArgument constructor. - * @param string $option - * @param ShellInterface|string $value * @throws ShellBuilderException */ - public function __construct(string $option, $value = '') + public function __construct(string $option, ShellInterface|string $value = '') { - if (is_string($value) && empty($value)) { + if (!$value) { $this->delimiter = ''; } + parent::__construct($option, $value); } } diff --git a/src/Literal/ShellShortOption.php b/src/Literal/ShellShortOption.php index d9a9397..44be421 100644 --- a/src/Literal/ShellShortOption.php +++ b/src/Literal/ShellShortOption.php @@ -13,20 +13,20 @@ */ final class ShellShortOption extends ShellWord { - protected $isShortOption = true; - protected $prefix = ShellWord::SHORT_OPTION_CONTROL; + protected const IS_SHORT_OPTION = true; + + protected string $prefix = ShellWord::SHORT_OPTION_CONTROL; /** * ShellArgument constructor. - * @param string $option - * @param ShellInterface|string $value * @throws ShellBuilderException */ - public function __construct(string $option, $value) + public function __construct(string $option, ShellInterface|string $value) { - if (is_string($value) && empty($value)) { + if (!$value) { $this->delimiter = ''; } + parent::__construct($option, $value); } } diff --git a/src/Literal/ShellVariable.php b/src/Literal/ShellVariable.php index 0c1bc0e..0ea9124 100644 --- a/src/Literal/ShellVariable.php +++ b/src/Literal/ShellVariable.php @@ -9,20 +9,21 @@ final class ShellVariable extends ShellWord { - protected $isVariable = true; - protected $useAssignOperator = true; - protected $wrapAsSubcommand = true; - protected $spaceAfterValue = false; - /** @var bool */ - private $noSemicolon = false; + protected const IS_VARIABLE = true; + + protected bool $useAssignOperator = true; + + protected bool $wrapAsSubcommand = true; + + protected bool $spaceAfterValue = false; + + private bool $noSemicolon = false; /** * ShellVariable constructor. - * @param string $option - * @param ShellInterface|string $value * @throws ShellBuilderException */ - public function __construct(string $option, $value) + public function __construct(string $option, ShellInterface|string $value) { parent::__construct($option, $value); if ($this->value instanceof ShellInterface) { diff --git a/src/Literal/ShellWord.php b/src/Literal/ShellWord.php index 1c08b49..28e7025 100644 --- a/src/Literal/ShellWord.php +++ b/src/Literal/ShellWord.php @@ -17,72 +17,55 @@ class ShellWord implements ShellInterface { protected const OPTION_CONTROL = '--'; + protected const SHORT_OPTION_CONTROL = '-'; + protected const EQUAL_CONTROL = '='; - /** - * @var bool - * @psalm-readonly - */ - protected $isShortOption = false; - /** - * @var bool - * @psalm-readonly - */ - protected $isOption = false; - /** - * @var bool - * @psalm-readonly - */ - protected $isArgument = false; - /** - * @var bool - * @psalm-readonly - */ - protected $isEnvironmentVariable = false; - /** - * @var bool - * @psalm-readonly - */ - protected $isVariable = false; - /** @var bool */ - protected $isEscaped = true; - /** @var bool */ - protected $spaceAfterValue = true; - /** @var bool */ - protected $useAssignOperator = false; - /** @var bool */ - protected $nameUpperCase = false; - /** @var bool */ - protected $wrapAsSubcommand = false; - /** @var bool */ - protected $wrapWithBacktricks = false; - /** @var string */ - protected $prefix = ''; - /** @var string */ - protected $suffix = ' '; - /** @var string */ - protected $delimiter = ' '; - /** @var string */ - protected $argument = ''; - /** @var string|ShellInterface */ - protected $value = ''; + protected const IS_SHORT_OPTION = false; + + protected const IS_OPTION = false; + + protected const IS_ARGUMENT = false; + + protected const IS_ENVIRONMENT_VARIABLE = false; + + protected const IS_VARIABLE = false; + + protected bool $isEscaped = true; + + protected bool $spaceAfterValue = true; + + protected bool $useAssignOperator = false; + + protected bool $nameUpperCase = false; + + protected bool $wrapAsSubcommand = false; + + protected bool $wrapWithBacktricks = false; + + protected string $prefix = ''; + + protected string $suffix = ' '; + + protected string $delimiter = ' '; + + protected string $argument; + /** * The constructor is protected, you must choose one of the children - * @param string $argument - * @param string|ShellInterface $value * @throws ShellBuilderException */ - protected function __construct(string $argument, $value = '') + protected function __construct(string $argument, protected ShellInterface|string $value = '') { - if (!empty($argument) && !$this->validShellWord($argument)) { + if ($argument !== '' && $argument !== '0' && !$this->validShellWord($argument)) { throw new ShellBuilderException( 'A Shell Argument has to be a valid Shell word and cannot contain e.g whitespace' ); } + $this->argument = $argument; - $this->value = $value; } public function setEscape(bool $isEscaped): self @@ -111,16 +94,9 @@ public function setNameUppercase(bool $uppercaseName): self protected function validate(): void { - /** @psalm-suppress DocblockTypeContradiction */ - if (!(is_string($this->value) || $this->value instanceof ShellInterface)) { - throw new ShellBuilderException('Value must be an instance of ShellInterface or a string'); - } } /** - * @psalm-pure - * @param string $word - * @return bool * @throws ShellBuilderException */ private function validShellWord(string $word): bool @@ -134,30 +110,34 @@ private function prepare(): void if (!$this->spaceAfterValue) { $this->suffix = ''; } + if ($this->useAssignOperator) { $this->delimiter = self::EQUAL_CONTROL; } - if (!empty($this->argument) && $this->nameUpperCase) { + + if ($this->argument && $this->nameUpperCase) { $this->argument = strtoupper($this->argument); } } /** - * @param bool $debug * @return array|string */ - private function getValue(bool $debug = false) + private function getValue(bool $debug = false): array|string { $word = $this->value; if ($word instanceof ShellInterface) { if ($debug) { return $word->__toArray(); } + $word = (string)$word; } - if ($this->isEscaped && !empty($word)) { - $word = escapeshellarg($word); + + if ($this->isEscaped && ($word !== '' && $word !== '0')) { + return escapeshellarg($word); } + return $word; } @@ -168,11 +148,11 @@ public function __toArray(): array { $this->prepare(); return [ - 'isArgument' => $this->isArgument, - 'isShortOption' => $this->isShortOption, - 'isOption' => $this->isOption, - 'isEnvironmentVariable' => $this->isEnvironmentVariable, - 'isVariable' => $this->isVariable, + 'isArgument' => static::IS_ARGUMENT, + 'isShortOption' => static::IS_SHORT_OPTION, + 'isOption' => static::IS_OPTION, + 'isEnvironmentVariable' => static::IS_ENVIRONMENT_VARIABLE, + 'isVariable' => static::IS_VARIABLE, 'escaped' => $this->isEscaped, 'withAssign' => $this->useAssignOperator, 'spaceAfterValue' => $this->spaceAfterValue, @@ -187,8 +167,9 @@ public function __toString(): string /** @var string $value */ $value = $this->getValue(); if ($this->value instanceof ShellInterface && $this->wrapAsSubcommand) { - $value = $this->wrapWithBacktricks ? "`$value`" : "$($value)"; + $value = $this->wrapWithBacktricks ? sprintf('`%s`', $value) : sprintf('$(%s)', $value); } + return sprintf( '%s%s%s%s%s', $this->prefix, diff --git a/src/ShellBuilder.php b/src/ShellBuilder.php index 5a345ec..1cb89ed 100644 --- a/src/ShellBuilder.php +++ b/src/ShellBuilder.php @@ -4,6 +4,7 @@ namespace PHPSu\ShellCommandBuilder; +use JsonSerializable; use PHPSu\ShellCommandBuilder\Collection\CollectionTuple; use PHPSu\ShellCommandBuilder\Collection\Pipeline; use PHPSu\ShellCommandBuilder\Collection\Redirection; @@ -15,29 +16,27 @@ use PHPSu\ShellCommandBuilder\Literal\ShellVariable; use TypeError; -final class ShellBuilder implements ShellInterface, \JsonSerializable +final class ShellBuilder implements ShellInterface, JsonSerializable { use ShellConditional; /** @var array */ - private $commandList = []; - /** @var int */ - private $groupType; + private array $commandList = []; + /** * name of the coprocess - empty string means anonymous - * @var null|string */ - private $asynchronously; - /** @var bool */ - private $processSubstitution = false; - /** @var bool */ - private $commandSubstitution = false; + private ?string $asynchronously = null; + + private bool $processSubstitution = false; + + private bool $commandSubstitution = false; + /** @var array */ - private $variables = []; + private array $variables = []; /** * This is a shortcut for quicker fluid access to the shell builder - * @return static */ public static function new(): self { @@ -46,17 +45,14 @@ public static function new(): self /** * This is a shortcut for quicker fluid access to the command api - * @param string $executable - * @return ShellCommand */ public static function command(string $executable): ShellCommand { return new ShellCommand($executable, new self()); } - public function __construct(int $groupType = GroupType::NO_GROUP) + public function __construct(private readonly int $groupType = GroupType::NO_GROUP) { - $this->groupType = $groupType; } public function createCommand(string $name, bool $withNewBuilder = false): ShellCommand @@ -71,25 +67,23 @@ public function runAsynchronously(bool $isAsync = true, string $name = ''): self } /** - * @param string $variable - * @param string|ShellInterface $value - * @param bool $useBackticks * @param bool $escape is the value instance of ShellInterface, then this variable is automatically false - * @param bool $noSemicolon * @return $this * @throws ShellBuilderException */ - public function addVariable(string $variable, $value, bool $useBackticks = false, bool $escape = true, bool $noSemicolon = false): self + public function addVariable(string $variable, ShellInterface|string $value, bool $useBackticks = false, bool $escape = true, bool $noSemicolon = false): self { if (isset($this->variables[$variable])) { throw new ShellBuilderException('Variable has already been declared.'); } + $shellVariable = new ShellVariable($variable, $value); $shellVariable->wrapWithBackticks($useBackticks); $shellVariable->setNoSemicolon($noSemicolon); if (is_string($value)) { $shellVariable->setEscape($escape); } + $this->variables[$variable] = $shellVariable; return $this; } @@ -101,97 +95,89 @@ public function removeVariable(string $variable): self } /** - * @param string|ShellInterface ...$commands * @return $this * @throws ShellBuilderException */ - public function add(...$commands): self + public function add(ShellInterface|string ...$commands): self { foreach ($commands as $command) { $this->addSingle($command); } + return $this; } /** - * @param string|ShellInterface $command - * @param bool $raw * @return $this * @throws ShellBuilderException */ - public function addSingle($command, bool $raw = false): self + public function addSingle(ShellInterface|string $command, bool $raw = false): self { $command = $raw ? $command : $this->parseCommand($command, true); - if (empty($this->commandList)) { + if ($this->commandList === []) { $this->commandList[] = $command; return $this; } + $this->commandList[] = ShellList::add($command); return $this; } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function and($command): self + public function and(ShellInterface|string $command): self { $this->commandList[] = ShellList::addAnd($this->parseCommand($command)); return $this; } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function or($command): self + public function or(ShellInterface|string $command): self { $this->commandList[] = ShellList::addOr($this->parseCommand($command)); return $this; } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function async($command = ''): self + public function async(ShellInterface|string $command = ''): self { $this->commandList[] = ShellList::async($this->parseCommand($command)); return $this; } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function pipe($command): self + public function pipe(ShellInterface|string $command): self { $this->commandList[] = Pipeline::pipe($this->parseCommand($command)); return $this; } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function pipeWithForward($command): self + public function pipeWithForward(ShellInterface|string $command): self { $this->commandList[] = Pipeline::pipeErrorForward($this->parseCommand($command)); return $this; } /** - * @param string|ShellInterface $command - * @param bool $append * @return $this * @throws ShellBuilderException */ - public function redirectOutput($command, bool $append = false): self + public function redirectOutput(ShellInterface|string $command, bool $append = false): self { $command = $this->parseCommand($command); $this->commandList[] = Redirection::redirectOutput($command, $append); @@ -199,11 +185,10 @@ public function redirectOutput($command, bool $append = false): self } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function redirectInput($command): self + public function redirectInput(ShellInterface|string $command): self { $command = $this->parseCommand($command); $this->commandList[] = Redirection::redirectInput($command); @@ -211,11 +196,10 @@ public function redirectInput($command): self } /** - * @param string|ShellInterface $command * @return $this * @throws ShellBuilderException */ - public function redirectError($command): self + public function redirectError(ShellInterface|string $command): self { $command = $this->parseCommand($command); $this->commandList[] = Redirection::redirectError($command); @@ -223,12 +207,10 @@ public function redirectError($command): self } /** - * @param string|ShellInterface $command - * @param bool $toLeft * @return $this * @throws ShellBuilderException */ - public function redirect($command, bool $toLeft = true): self + public function redirect(ShellInterface|string $command, bool $toLeft = true): self { $command = $this->parseCommand($command); $this->commandList[] = Redirection::redirectBetweenFiles($command, $toLeft); @@ -236,14 +218,9 @@ public function redirect($command, bool $toLeft = true): self } /** - * @param ShellInterface|string $command - * @param bool $toLeft - * @param null|int $firstDescriptor - * @param null|int $secondDescriptor - * @return static * @throws ShellBuilderException */ - public function redirectDescriptor($command, bool $toLeft, int $firstDescriptor = null, int $secondDescriptor = null): self + public function redirectDescriptor(ShellInterface|string $command, bool $toLeft, ?int $firstDescriptor = null, ?int $secondDescriptor = null): self { $command = $this->parseCommand($command); $this->commandList[] = Redirection::redirectBetweenDescriptors($command, $toLeft, $firstDescriptor, $secondDescriptor); @@ -262,12 +239,7 @@ public function addCondition(BasicExpression $condition): self return $this; } - /** - * @param string|ShellInterface $fileEnding - * @return ShellBuilder - * @throws ShellBuilderException - */ - public function addFileEnding($fileEnding): self + public function addFileEnding(ShellInterface|string $fileEnding): self { $tuple = CollectionTuple::create($fileEnding, '.'); $tuple @@ -298,55 +270,55 @@ public function createCommandSubstition(): self public function hasCommands(): bool { - return empty($this->commandList) === false || empty($this->variables) === false; + return $this->commandList !== [] || $this->variables !== []; } /** - * @param string|ShellInterface $command - * @param bool $allowEmpty - * @return ShellInterface * @throws ShellBuilderException */ - private function parseCommand($command, bool $allowEmpty = false): ShellInterface + private function parseCommand(ShellInterface|string $command, bool $allowEmpty = false): ShellInterface { if (is_string($command)) { $command = $this->createCommand($command); } + try { - $this->validateCommand($command, $allowEmpty); - } catch (TypeError $typeError) { + $this->validateCommand($allowEmpty); + } catch (TypeError) { throw new ShellBuilderException('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); } + return $command; } - /** @noinspection PhpUnusedParameterInspection */ - private function validateCommand(ShellInterface $command, bool $allowEmpty): void + private function validateCommand(bool $allowEmpty): void { - if (!$allowEmpty && empty($this->commandList)) { + if (!$allowEmpty && $this->commandList === []) { throw new ShellBuilderException('You have to first add a command before you can combine it'); } } private function variablesToString(): string { - $variableString = ''; - foreach ($this->variables as $variable) { - $variableString .= $variable; - } + $variableString = implode('', $this->variables); + if ($variableString !== '') { $variableString .= ' '; } + return $variableString; } + /** + * @return list> + */ public function jsonSerialize(): array { return $this->__toArray(); } /** - * @return array + * @return list> */ public function __toArray(): array { @@ -354,6 +326,7 @@ public function __toArray(): array foreach ($this->commandList as $item) { $commands[] = is_string($item) ? $item : $item->__toArray(); } + return $commands; } @@ -363,9 +336,9 @@ public function __toString(): string if ($this->asynchronously !== null) { $result = sprintf('coproc %s%s', $this->asynchronously, $this->asynchronously !== '' ? ' ' : ''); } - foreach ($this->commandList as $command) { - $result .= $command; - } + + $result .= implode('', $this->commandList); + if ($this->groupType === GroupType::SAMESHELL_GROUP) { return sprintf( '%s %s;%s', @@ -374,14 +347,17 @@ public function __toString(): string ControlOperator::CURLY_BLOCK_DEFINITON_CLOSE ); } + if ($this->groupType === GroupType::SUBSHELL_GROUP) { $substitionType = ''; if ($this->commandSubstitution) { $substitionType = '$'; } + if ($this->processSubstitution) { $substitionType = '<'; } + return sprintf( '%s%s%s%s', $substitionType, @@ -390,6 +366,7 @@ public function __toString(): string ControlOperator::BLOCK_DEFINITON_CLOSE ); } + return rtrim(sprintf('%s%s', $this->variablesToString(), $result)); } } diff --git a/src/ShellCommand.php b/src/ShellCommand.php index 3e8751d..322ec73 100644 --- a/src/ShellCommand.php +++ b/src/ShellCommand.php @@ -21,27 +21,26 @@ final class ShellCommand implements ShellInterface use ShellConditional; /** - * @var ShellWord * @psalm-readonly */ - private $executable; + private ShellExecutable $executable; + /** @var array */ - private $arguments = []; + private array $arguments = []; + /** @var array */ - private $environmentVariables = []; - /** @var bool */ - private $isCommandSubstitution = false; - /** @var bool */ - private $isProcessSubstitution = false; - /** @var ShellBuilder|null */ - private $parentBuilder; - /** @var bool */ - private $invertOutput = false; - - public function __construct(string $name, ShellBuilder $builder = null) + private array $environmentVariables = []; + + private bool $isCommandSubstitution = false; + + private bool $isProcessSubstitution = false; + + + private bool $invertOutput = false; + + public function __construct(string $name, private readonly ?ShellBuilder $parentBuilder = null) { $this->executable = new ShellExecutable($name); - $this->parentBuilder = $builder; } public function addToBuilder(): ShellBuilder @@ -49,6 +48,7 @@ public function addToBuilder(): ShellBuilder if ($this->parentBuilder === null) { throw new ShellBuilderException('You need to create a ShellBuilder first before you can use it within a command'); } + return $this->parentBuilder->add($this); } @@ -71,50 +71,29 @@ public function invert(bool $invert = true): self } /** - * @param string $option - * @param ShellInterface|string|mixed $value - * @param bool $escapeArgument - * @param bool $withAssignOperator - * @return self * @throws ShellBuilderException */ - public function addShortOption(string $option, $value = '', bool $escapeArgument = true, bool $withAssignOperator = false): self + public function addShortOption(string $option, ShellInterface|string $value = '', bool $escapeArgument = true, bool $withAssignOperator = false): self { - if (!($value instanceof ShellInterface || is_string($value))) { - throw new ShellBuilderException('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - } $word = new ShellShortOption($option, $value); return $this->add($word, $escapeArgument, $withAssignOperator); } /** - * @param string $option - * @param ShellInterface|string|mixed $value - * @param bool $escapeArgument - * @param bool $withAssignOperator - * @return self * @throws ShellBuilderException */ - public function addOption(string $option, $value = '', bool $escapeArgument = true, bool $withAssignOperator = false): self + public function addOption(string $option, ShellInterface|string $value = '', bool $escapeArgument = true, bool $withAssignOperator = false): self { - if (!($value instanceof ShellInterface || is_string($value))) { - throw new ShellBuilderException('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - } $word = new ShellOption($option, $value); return $this->add($word, $escapeArgument, $withAssignOperator); } /** - * @param ShellInterface|string|mixed $argument - * @param bool $escapeArgument - * @return self * @throws ShellBuilderException + * @return $this */ - public function addArgument($argument, bool $escapeArgument = true): self + public function addArgument(ShellInterface|string $argument, bool $escapeArgument = true): self { - if (!($argument instanceof ShellInterface || is_string($argument))) { - throw new ShellBuilderException('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - } $word = new ShellArgument($argument); return $this->add($word, $escapeArgument); } @@ -123,7 +102,6 @@ public function addArgument($argument, bool $escapeArgument = true): self * This is an alias for argument, that automatically escapes the argument. * It does in the end does not provide any additional functionality * - * @param ShellInterface $argument * @return $this * @throws ShellBuilderException */ @@ -133,20 +111,18 @@ public function addSubCommand(ShellInterface $argument): self } /** - * @param ShellInterface|string|mixed $argument - * @return self * @throws ShellBuilderException */ - public function addNoSpaceArgument($argument): self + public function addNoSpaceArgument(ShellInterface|string $argument): self { - if (!($argument instanceof ShellInterface || is_string($argument))) { - throw new ShellBuilderException('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - } $word = new ShellArgument($argument); $word->setSpaceAfterValue(false); return $this->add($word, false); } + /** + * @return $this + */ private function add(ShellWord $word, bool $escapeArgument, bool $withAssignOperator = false): self { $word->setEscape($escapeArgument); @@ -164,19 +140,13 @@ public function addEnv(string $name, string $value): self private function argumentsToString(): string { - $result = []; - foreach ($this->arguments as $part) { - $result[] = $part; - } + $result = $this->arguments; return trim(implode('', $result)); } private function environmentVariablesToString(): string { - $result = []; - foreach ($this->environmentVariables as $part) { - $result[] = $part; - } + $result = $this->environmentVariables; return implode('', $result); } @@ -189,10 +159,12 @@ public function __toArray(): array foreach ($this->arguments as $item) { $commands[] = $item->__toArray(); } + $envs = []; foreach ($this->environmentVariables as $item) { $envs[] = $item->__toArray(); } + return [ 'executable' => $this->executable->__toString(), 'arguments' => $commands, @@ -207,16 +179,18 @@ public function __toString(): string $result = (sprintf( '%s%s%s%s', $this->invertOutput ? '! ' : '', - empty($this->environmentVariables) ? '' : $this->environmentVariablesToString(), + $this->environmentVariables === [] ? '' : $this->environmentVariablesToString(), $this->executable, - empty($this->arguments) ? '' : ' ' . $this->argumentsToString() + $this->arguments === [] ? '' : ' ' . $this->argumentsToString() )); if ($this->isCommandSubstitution && !$this->isProcessSubstitution) { return sprintf("\$(%s)", $result); } + if ($this->isProcessSubstitution && !$this->isCommandSubstitution) { return sprintf("<(%s)", $result); } + return $result; } } diff --git a/src/ShellConditional.php b/src/ShellConditional.php index 64e580c..c0e4944 100644 --- a/src/ShellConditional.php +++ b/src/ShellConditional.php @@ -6,22 +6,24 @@ trait ShellConditional { - public function if(bool $condition, callable $callback, callable $alternativeCallback = null): self + public function if(bool $condition, callable $callback, ?callable $alternativeCallback = null): self { if ($condition) { $result = $callback($this); assert($result instanceof self); return $result; } + if ($alternativeCallback) { $alternativeResult = $alternativeCallback($this); assert($alternativeResult instanceof self); return $alternativeResult; } + return $this; } - public function ifThis(callable $callOnThis, callable $callback, callable $alternativeCallback = null): self + public function ifThis(callable $callOnThis, callable $callback, ?callable $alternativeCallback = null): self { return $this->if($callOnThis($this) === true, $callback, $alternativeCallback); } diff --git a/src/ShellInterface.php b/src/ShellInterface.php index 99ae2c0..3bb8aec 100644 --- a/src/ShellInterface.php +++ b/src/ShellInterface.php @@ -6,7 +6,10 @@ interface ShellInterface { - /** @internal - debug method */ + /** + * @internal - debug method + * @return array + */ public function __toArray(): array; public function __toString(): string; diff --git a/tests/Collection/CollectionTupleTest.php b/tests/Collection/CollectionTupleTest.php index 8b85211..431af9e 100644 --- a/tests/Collection/CollectionTupleTest.php +++ b/tests/Collection/CollectionTupleTest.php @@ -23,6 +23,7 @@ public function testCollectionWithBuilderTuple(): void { $builder = new ShellBuilder(); $builder->add($builder->createCommand('a')); + $tuple = CollectionTuple::create($builder, ControlOperator::AND_OPERATOR); $this->assertEquals(' && a', (string)$tuple); } @@ -33,12 +34,6 @@ public function testCollectionWithCommandTuple(): void $this->assertEquals(' || a', (string)$tuple); } - public function testWithWrongType(): void - { - $this->expectException(ShellBuilderException::class); - CollectionTuple::create(3892740, ControlOperator::OR_OPERATOR); - } - public function testTupleToArray(): void { $tuple = CollectionTuple::create('a', ControlOperator::OR_OPERATOR); diff --git a/tests/Definiton/PatternTest.php b/tests/Definiton/PatternTest.php index 973a193..52efdd2 100644 --- a/tests/Definiton/PatternTest.php +++ b/tests/Definiton/PatternTest.php @@ -32,7 +32,7 @@ public function testSplitFileName(): void public function testSplitDoubleQuotedStrings(): void { - $this->assertEquals(["a", "\"b\" c", "d"], Pattern::split('a "\"b\" c" d')); + $this->assertEquals(["a", '"b" c', "d"], Pattern::split('a "\"b\" c" d')); } public function testSplitEscapedSpaceStrings(): void @@ -44,7 +44,7 @@ public function testBadDoubleQuotes(): void { $this->expectException(ShellBuilderException::class); $this->expectExceptionMessage('The given input has mismatching Quotes'); - Pattern::split("a \"b c d e"); + Pattern::split('a "b c d e'); } public function testBadSingleQuotes(): void @@ -74,6 +74,6 @@ public function testMultibyte(): void public function testSplitPercentSign(): void { - $this->assertEquals(["abc", "%foo bar%"], Pattern::split('abc \'%foo bar%\'')); + $this->assertEquals(["abc", "%foo bar%"], Pattern::split("abc '%foo bar%'")); } } diff --git a/tests/ShellBuilderTest.php b/tests/ShellBuilderTest.php index 26a9fe6..d3a8bf8 100644 --- a/tests/ShellBuilderTest.php +++ b/tests/ShellBuilderTest.php @@ -4,6 +4,7 @@ namespace PHPSu\ShellCommandBuilder\Tests; +use AssertionError; use PHPSu\ShellCommandBuilder\Conditional\ArithmeticExpression; use PHPSu\ShellCommandBuilder\Conditional\FileExpression; use PHPSu\ShellCommandBuilder\Conditional\StringExpression; @@ -174,30 +175,6 @@ public function testSimpleSshCommand(): void $this->assertEquals($result, (string)$builder); } - public function testFaultyAddCommand(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - $builder = new ShellBuilder(); - $builder->add(false); - } - - public function testFaultyPipeCommand(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - $builder = new ShellBuilder(); - $builder->add('a')->pipe(false); - } - - public function testFaultyOrCommand(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - $builder = new ShellBuilder(); - $builder->add('a')->or(false); - } - public function testFaultyCommandChainNoBaseCommand(): void { $this->expectException(ShellBuilderException::class); @@ -221,7 +198,7 @@ public function testBuilderToArray(): void $this->assertEquals('c | a', (string)$builder); $debug = $builder->__toArray(); $this->assertCount(2, $debug); - $this->assertEquals('c', $debug[0]['executable']); + $this->assertEquals('c', $debug[0]['executable'] ?? null); } public function testRedirectTo(): void @@ -591,7 +568,7 @@ public function testCommandDebugWithPipeAndCondition(): void $debug = $command->__toArray(); $this->assertCount(3, $debug); // checking whether it deeply arrayfies - $this->assertEquals('cat', $debug[0]['compare']['executable']); + $this->assertEquals('cat', $debug[0]['compare']['executable'] ?? null); $this->assertEquals('&&', $debug[1][0]); $this->assertEquals('|', $debug[2][0]); } @@ -602,7 +579,7 @@ public function testJsonSerializeShellBuilder(): void $echo = ShellBuilder::command('echo')->addArgument('hello world'); $grep = ShellBuilder::command('grep')->addShortOption('e', 'world'); $builder = ShellBuilder::new()->add($echo)->pipe($grep); - $this->assertJson(json_encode($builder)); + $this->assertJson(json_encode($builder, JSON_THROW_ON_ERROR)); } public function testShellBuilderProcessSubstitions(): void @@ -709,26 +686,14 @@ public function testCondiditionalArguments(): void { // if false $builder = ShellBuilder::new() - ->if(false, static function (ShellBuilder $builder) { - return $builder->add('echo'); - }) - ->ifThis(static function (ShellBuilder $builder) { - return $builder->hasCommands() === false; - }, static function (ShellBuilder $builder) { - return $builder->add('print'); - }); + ->if(false, static fn(ShellBuilder $builder): ShellBuilder => $builder->add('echo')) + ->ifThis(static fn(ShellBuilder $builder): bool => $builder->hasCommands() === false, static fn(ShellBuilder $builder): ShellBuilder => $builder->add('print')); static::assertEquals('print', (string)$builder); // if true $builder = ShellBuilder::new() - ->if(true, static function (ShellBuilder $builder) { - return $builder->add('echo'); - }) - ->ifThis(static function (ShellBuilder $builder) { - return $builder->hasCommands() === false; - }, static function (ShellBuilder $builder) { - return $builder->add('print'); - }); + ->if(true, static fn(ShellBuilder $builder): ShellBuilder => $builder->add('echo')) + ->ifThis(static fn(ShellBuilder $builder): bool => $builder->hasCommands() === false, static fn(ShellBuilder $builder): ShellBuilder => $builder->add('print')); static::assertEquals('echo', (string)$builder); } @@ -737,45 +702,27 @@ public function testComplexCondiditionalArguments(): void $builder = ShellBuilder::new() ->if( false, - static function (ShellBuilder $builder) { - return $builder->add('echo'); - }, - static function (ShellBuilder $builder) { - return $builder->add('awk'); - } + static fn(ShellBuilder $builder): ShellBuilder => $builder->add('echo'), + static fn(ShellBuilder $builder): ShellBuilder => $builder->add('awk') ) - ->ifThis(static function (ShellBuilder $builder) { - return $builder->hasCommands() === false; - }, static function (ShellBuilder $builder) { - return $builder->add('print'); - }, static function (ShellBuilder $builder) { - return $builder->and('print'); - }); + ->ifThis(static fn(ShellBuilder $builder): bool => $builder->hasCommands() === false, static fn(ShellBuilder $builder): ShellBuilder => $builder->add('print'), static fn(ShellBuilder $builder): ShellBuilder => $builder->and('print')); static::assertEquals('awk && print', (string)$builder); } public function testComplexCondiditionalArgumentsWithWrongArguments(): void { - self::expectException(\AssertionError::class); + self::expectException(AssertionError::class); ShellBuilder::new() - ->ifThis(static function (ShellBuilder $builder) { - return 'world'; - }, static function (ShellBuilder $builder) { - return $builder->add('print'); - }, static function (ShellBuilder $builder) { - return 'bla'; - }); + ->ifThis(static fn(ShellBuilder $builder): string => 'world', static fn(ShellBuilder $builder): ShellBuilder => $builder->add('print'), static fn(ShellBuilder $builder): string => 'bla'); } public function testCondiditionalArgumentsWithWrongArguments(): void { - self::expectException(\AssertionError::class); + self::expectException(AssertionError::class); ShellBuilder::new() ->if( true, - static function (ShellBuilder $builder) { - return 'hello world'; - } + static fn(ShellBuilder $builder): string => 'hello world' ); } diff --git a/tests/ShellCommandTest.php b/tests/ShellCommandTest.php index 06287e7..69f4523 100644 --- a/tests/ShellCommandTest.php +++ b/tests/ShellCommandTest.php @@ -4,6 +4,7 @@ namespace PHPSu\ShellCommandBuilder\Tests; +use DateTime; use PHPSu\ShellCommandBuilder\Definition\GroupType; use PHPSu\ShellCommandBuilder\Exception\ShellBuilderException; use PHPSu\ShellCommandBuilder\ShellBuilder; @@ -31,7 +32,7 @@ public function testShellCommandWithEnvironmentVariables(): void ->addOption('color', 'always') ->addArgument('root') ->addArgument('/etc/passwd', false); - $this->assertEquals('GREP_COLOR=\'1;35\' grep --color \'always\' \'root\' /etc/passwd', (string)$command); + $this->assertEquals("GREP_COLOR='1;35' grep --color 'always' 'root' /etc/passwd", (string)$command); } public function testShellCommandWithCommandSubstitution(): void @@ -75,7 +76,7 @@ public function testShellCommandWithInvertedOutput(): void { $command = new ShellCommand('echo'); $command->invert()->addShortOption('e', 'hello world'); - $this->assertEquals('! echo -e \'hello world\'', (string)$command); + $this->assertEquals("! echo -e 'hello world'", (string)$command); } public function testEscapeOptionWithAssignOperator(): void @@ -98,7 +99,7 @@ public function testShellCommandToArray(): void 'escaped' => true, 'withAssign' => true, 'spaceAfterValue' => true, - 'value' => '\'true\'', + 'value' => "'true'", 'argument' => "color", ] ], $command['arguments']); @@ -152,7 +153,7 @@ public function testShellCommandWithCommandSubstitutionToArray(): void 'escaped' => true, 'withAssign' => true, 'spaceAfterValue' => true, - 'value' => '\'b\'', + 'value' => "'b'", 'argument' => "A", ] ], $command['environmentVariables']); @@ -166,22 +167,18 @@ public function testShellCommandWithCommandSubstitutionToArray(): void 'escaped' => true, 'withAssign' => true, 'spaceAfterValue' => true, - 'value' => '\'true\'', + 'value' => "'true'", 'argument' => "color", ] ], $command['arguments']); } - public function testConditionalArguments() + public function testConditionalArguments(): void { $command = ShellBuilder::command('test') - ->if(1 + 1 === 3, static function (ShellCommand $command) { - return $command->addOption('f', '1 + 1 = 3'); - }) - ->if(1 + 1 === 2, static function (ShellCommand $command) { - return $command->addOption('t', '1 + 1 = 2'); - }); - static::assertEquals((string)$command, 'test --t \'1 + 1 = 2\''); + ->if(false, static fn(ShellCommand $command): ShellCommand => $command->addOption('f', 'false')) + ->if(true, static fn(ShellCommand $command): ShellCommand => $command->addOption('t', 'true')); + static::assertEquals((string)$command, "test --t 'true'"); } public function testUnEscapedOption(): void @@ -195,32 +192,4 @@ public function testUnEscapedNoAssignOperatorOption(): void $command = (new ShellCommand('ls'))->addOption('color', 'true', false, false); $this->assertEquals('ls --color true', (string)$command); } - - public function testShortOptionWithWrongType(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - (new ShellCommand('ls'))->addShortOption('la', false); - } - - public function testOptionWithWrongType(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - (new ShellCommand('ls'))->addOption('la', 124343); - } - - public function testArgumentWithWrongType(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - (new ShellCommand('ls'))->addArgument(new \DateTime()); - } - - public function testNoSpaceArgumentWithWrongType(): void - { - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Provided the wrong type - only ShellCommand and ShellBuilder allowed'); - (new ShellCommand('ls'))->addNoSpaceArgument(new GroupType()); - } } diff --git a/tests/ShellWordTest.php b/tests/ShellWordTest.php index b89fe8b..37df447 100644 --- a/tests/ShellWordTest.php +++ b/tests/ShellWordTest.php @@ -102,7 +102,7 @@ public function testShellVariableToString(): void { $word = new ShellVariable('hello', 'world'); $word->setNoSemicolon(true); - $this->assertEquals('hello=\'world\'', $word->__toString()); + $this->assertEquals("hello='world'", $word->__toString()); $word = new ShellVariable('hello', ShellBuilder::command('echo')); $this->assertEquals('hello=$(echo);', $word->__toString()); @@ -142,6 +142,7 @@ public function testShellWordSubCommandAsShellInterfaceToDebugArray(): void { $word = new ShellArgument((new ShellCommand('hello'))->toggleCommandSubstitution()); $word->setSpaceAfterValue(false); + $array = $word->__toArray(); $this->assertEquals( [ @@ -187,22 +188,6 @@ public function testArgumentToUppercase(): void ); } - public function testShellWordAsFaultyValueTypeArgument(): void - { - $word = new ShellOption('hallo', 12345); - $this->expectException(ShellBuilderException::class); - $this->expectExceptionMessage('Value must be an instance of ShellInterface or a string'); - $word->__toString(); - } - - public function testShellWordWithWrongArgumentType(): void - { - $word = new ShellArgument(12345); - $this->expectExceptionMessage('Value must be an instance of ShellInterface or a string'); - $this->expectException(ShellBuilderException::class); - $word->__toString(); - } - public function testShellWordWithEmptyArgument(): void { $word = new ShellArgument(''); diff --git a/tests/phpunit-bootstrap.php b/tests/phpunit-bootstrap.php index de1f40d..462e1bb 100644 --- a/tests/phpunit-bootstrap.php +++ b/tests/phpunit-bootstrap.php @@ -9,12 +9,13 @@ // Currently phpunit's default error handling doesn't properly catch warnings / errors from data providers // https://github.com/sebastianbergmann/phpunit/issues/2449 set_error_handler( - static function ($severity, $message, $file, $line) { + static function ($severity, string $message, $file, $line): bool { if (!(error_reporting() & $severity)) { // This error code is not included in error_reporting, so let it fall // through to the standard PHP error handler return false; } + throw new ErrorException(__FILE__ . __FUNCTION__ . ': ' . $message, 0, $severity, $file, $line); } ); diff --git a/tests/phpunit.legacy.xml b/tests/phpunit.legacy.xml deleted file mode 100644 index 79941c1..0000000 --- a/tests/phpunit.legacy.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - ../tests/ - - - - - ../src/ - - - - - - - - - - - diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 45ea586..efe4936 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,39 +1,34 @@ - - - ../src/ - - - - - - - - - - ../tests/ - - - - - - - + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.1/phpunit.xsd" + bootstrap="phpunit-bootstrap.php" + beStrictAboutOutputDuringTests="true" + beStrictAboutChangesToGlobalState="true" + + enforceTimeLimit="true" + timeoutForLargeTests="1" + timeoutForMediumTests="1" + colors="true"> + + + ../src + + + + + + + + + + + + ../tests/ + + + + + + +