Browse Source

Code formatting

tags/v9.1.0
Cédric Belin 1 month ago
parent
commit
02ac5c061e
27 changed files with 501 additions and 494 deletions
  1. +6
    -3
      .editorconfig
  2. +1
    -1
      .github/workflows/build.yaml
  3. +5
    -5
      .vscode/settings.json
  4. +2
    -2
      bin/which
  5. +51
    -51
      composer.json
  6. +1
    -1
      doc/index.md
  7. +6
    -6
      doc/installation.md
  8. +11
    -11
      doc/usage/api.md
  9. +15
    -15
      doc/usage/cli.md
  10. +9
    -5
      etc/mkdocs.yaml
  11. +21
    -21
      etc/phpdoc.xml
  12. +3
    -3
      etc/phpstan.neon
  13. +13
    -13
      etc/phpunit.xml
  14. +8
    -8
      example/main.php
  15. +30
    -30
      src/Cli/Command.php
  16. +1
    -1
      src/Cli/version.g.php
  17. +144
    -144
      src/Finder.php
  18. +31
    -31
      src/FinderException.php
  19. +16
    -16
      src/which.php
  20. +51
    -51
      test/FinderTest.php
  21. +57
    -57
      test/WhichTest.php
  22. +3
    -3
      tool/build.ps1
  23. +3
    -3
      tool/clean.ps1
  24. +1
    -1
      tool/coverage.ps1
  25. +1
    -1
      tool/doc.ps1
  26. +1
    -1
      tool/upgrade.ps1
  27. +10
    -10
      tool/watch.ps1

+ 6
- 3
.editorconfig View File

@@ -3,11 +3,14 @@ root = true

[*]
charset = utf-8
indent_size = 2
indent_style = space
indent_style = tab
insert_final_newline = true
tab_width = 2
trim_trailing_whitespace = true

[*.md]
indent_size = 4
trim_trailing_whitespace = false

[*.{yaml,yml}]
indent_size = 2
indent_style = space

+ 1
- 1
.github/workflows/build.yaml View File

@@ -3,7 +3,7 @@ on:
pull_request:
push:
schedule:
- cron: '0 0 1 * *'
- cron: "0 0 1 * *"
jobs:
test:
runs-on: ubuntu-latest


+ 5
- 5
.vscode/settings.json View File

@@ -1,7 +1,7 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
"editor.insertSpaces": false,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
}

+ 2
- 2
bin/which View File

@@ -5,12 +5,12 @@ use Symfony\Component\Console\{Application};
use Which\Cli\{Command};

// Load the dependencies.
$autoloader = new SplFileInfo(__DIR__.'/../../../autoload.php');
$autoloader = new SplFileInfo(__DIR__."/../../../autoload.php");
$rootPath = (new SplFileInfo(__DIR__))->getPath();
require_once $autoloader->isFile() ? $autoloader->getPathname() : "$rootPath/vendor/autoload.php";

// Start the application.
$application = new Application('Which.php', require "$rootPath/src/Cli/version.g.php");
$application = new Application("Which.php", require "$rootPath/src/Cli/version.g.php");
$command = (new Command)->setProcessTitle($application->getName());
$application->add($command);
$application->setDefaultCommand($command->getName(), true)->run();

+ 51
- 51
composer.json View File

@@ -1,53 +1,53 @@
{
"description": "Find the instances of an executable in the system path. Like the `which` Linux command.",
"homepage": "https://docs.belin.io/which.php",
"license": "MIT",
"name": "cedx/which",
"type": "library",
"version": "9.0.0",
"authors": [
{"email": "cedric@belin.io", "homepage": "https://belin.io", "name": "Cédric Belin"}
],
"autoload": {
"files": ["src/which.php"],
"psr-4": {"Which\\": "src/"}
},
"autoload-dev": {
"psr-4": {"Which\\": "test/"}
},
"bin": [
"bin/which"
],
"config": {
"optimize-autoloader": true
},
"funding": [
{"type": "patreon", "url": "https://www.patreon.com/cedx"}
],
"keywords": [
"find",
"path",
"system",
"utility",
"which"
],
"require": {
"php": ">=7.4.0",
"ext-mbstring": "*",
"ext-spl": "*",
"symfony/console": "^5.0.8",
"webmozart/path-util": "^2.3.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.25",
"phpunit/phpunit": "^9.1.4"
},
"scripts": {
"coverage": "composer global exec coveralls var/coverage.xml",
"test": "phpunit --configuration=etc/phpunit.xml"
},
"support": {
"docs": "https://api.belin.io/which.php",
"issues": "https://git.belin.io/cedx/which.php/issues"
}
"description": "Find the instances of an executable in the system path. Like the `which` Linux command.",
"homepage": "https://docs.belin.io/which.php",
"license": "MIT",
"name": "cedx/which",
"type": "library",
"version": "9.0.0",
"authors": [
{"email": "cedric@belin.io", "homepage": "https://belin.io", "name": "Cédric Belin"}
],
"autoload": {
"files": ["src/which.php"],
"psr-4": {"Which\\": "src/"}
},
"autoload-dev": {
"psr-4": {"Which\\": "test/"}
},
"bin": [
"bin/which"
],
"config": {
"optimize-autoloader": true
},
"funding": [
{"type": "patreon", "url": "https://www.patreon.com/cedx"}
],
"keywords": [
"find",
"path",
"system",
"utility",
"which"
],
"require": {
"php": ">=7.4.0",
"ext-mbstring": "*",
"ext-spl": "*",
"symfony/console": "^5.0.8",
"webmozart/path-util": "^2.3.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.25",
"phpunit/phpunit": "^9.1.4"
},
"scripts": {
"coverage": "composer global exec coveralls var/coverage.xml",
"test": "phpunit --configuration=etc/phpunit.xml"
},
"support": {
"docs": "https://api.belin.io/which.php",
"issues": "https://git.belin.io/cedx/which.php/issues"
}
}

+ 1
- 1
doc/index.md View File

@@ -6,7 +6,7 @@ Find the instances of an executable in the system path, in [PHP](https://www.php
## Quick start
Install the latest version of **Which for PHP** with [Composer](https://getcomposer.org):

```shell
``` shell
composer require cedx/which
```



+ 6
- 6
doc/installation.md View File

@@ -3,10 +3,10 @@
## Requirements
Before installing **Which for PHP**, you need to make sure you have [PHP](https://www.php.net)
and [Composer](https://getcomposer.org), the PHP package manager, up and running.
You can verify if you're already good to go with the following commands:

```shell
``` shell
php --version
# PHP 7.4.5 (cli) (built: Apr 14 2020 16:17:19) ( NTS Visual C++ 2017 x64 )

@@ -15,22 +15,22 @@ composer --version
```

!!! info
If you plan to play with the package sources, you will also need the latest versions of
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).
If you plan to play with the package sources, you will also need the latest versions of
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).

## Installing with Composer package manager

### 1. Install it
From a command prompt, run:

```shell
``` shell
composer require cedx/which
```

### 2. Import it
Now in your [PHP](https://www.php.net) code, you can use:

```php
``` php
<?php
use function Which\{which};
use Which\{Finder, FinderException};


+ 11
- 11
doc/usage/api.md View File

@@ -6,19 +6,19 @@ source: src/which.php
# Application programming interface
This package provides a single function, `Which\which()`, allowing to locate a command in the system path:

```php
``` php
<?php
use function Which\{which};
use Which\{FinderException};

try {
// `$path` is the absolute path to the executable.
$path = which('foobar');
echo 'The command "foobar" is located at: ', $path;
// `$path` is the absolute path to the executable.
$path = which('foobar');
echo 'The command "foobar" is located at: ', $path;
}

catch (FinderException $e) {
echo 'The command "', $e->getCommand(), '" was not found';
echo 'The command "', $e->getCommand(), '" was not found';
}
```

@@ -33,7 +33,7 @@ A value indicating whether to return all executables found, instead of just the

If you pass `true` as parameter value, the function will return an array of strings providing all paths found, instead of a single string:

```php
``` php
<?php
$paths = which('foobar', true);
echo 'The command "foobar" was found at these locations:', PHP_EOL;
@@ -43,7 +43,7 @@ foreach ($paths as $path) echo $path, PHP_EOL;
### callable **$onError**
By default, when the specified command cannot be located, a `Which\FinderException` is thrown. You can disable this exception by providing your own error handler:

```php
``` php
<?php
$path = which('foobar', false, fn($command) => '');
if (!mb_strlen($path)) echo 'The command "foobar" was not found.';
@@ -58,19 +58,19 @@ The options to be passed to the underlying finder:
#### "extensions" => string | string[]
The executable file extensions, provided as a string or a list of file extensions. Defaults to the list of extensions provided by the `PATHEXT` environment variable.

```php
``` php
<?php
which('foobar', false, null, ['extensions' => '.FOO;.EXE;.CMD']);
which('foobar', false, null, ['extensions' => ['.foo', '.exe', '.cmd']]);
```

!!! tip
The `extensions` option is only meaningful on the Windows platform, where the executability of a file is determined from its extension.
The `extensions` option is only meaningful on the Windows platform, where the executability of a file is determined from its extension.

#### "path" => string | string[]
The system path, provided as a string or a list of directories. Defaults to the list of paths provided by the `PATH` environment variable.

```php
``` php
<?php
which('foobar', false, null, ['path' => '/usr/local/bin:/usr/bin']);
which('foobar', false, null, ['path' => ['/usr/local/bin', '/usr/bin']]);
@@ -79,7 +79,7 @@ which('foobar', false, null, ['path' => ['/usr/local/bin', '/usr/bin']]);
#### "pathSeparator" => string
The character used to separate paths in the system path. Defaults to the platform path separator (e.g. `";"` on Windows, `":"` on other platforms).

```php
``` php
<?php
which('foobar', false, null, ['pathSeparator' => '#']);
// For example: "/usr/local/bin#/usr/bin"


+ 15
- 15
doc/usage/cli.md View File

@@ -6,41 +6,41 @@ source: bin/which
# Command line interface
From a command prompt, install the `which` executable:

```shell
``` shell
composer global require cedx/which
```

!!! tip
Consider adding the [`composer global`](https://getcomposer.org/doc/03-cli.md#global) executables directory to your system path.
Consider adding the [`composer global`](https://getcomposer.org/doc/03-cli.md#global) executables directory to your system path.

Then use it to find the instances of an executable command:

```shell
``` shell
$ which --help

Description:
Find the instances of an executable in the system path.
Find the instances of an executable in the system path.

Usage:
which [options] [--] <executable>
which [options] [--] <executable>

Arguments:
executable The executable to find
executable The executable to find

Options:
-a, --all List all instances of executables found, instead of just the first one
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
-a, --all List all instances of executables found, instead of just the first one
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
```

For example:

```shell
``` shell
which --all php
# /usr/bin/php
```

+ 9
- 5
etc/mkdocs.yaml View File

@@ -8,19 +8,23 @@ site_dir: ../www

repo_name: git.belin.io
repo_url: https://git.belin.io/cedx/which.php
edit_uri: ''
edit_uri: ""

copyright: Copyright &copy; 2017 - 2020 Cédric Belin
extra:
social:
- icon: fontawesome/solid/globe
link: 'https://belin.io'
link: "https://belin.io"
name: Belin.io
- icon: fontawesome/brands/github
link: 'https://github.com/cedx'
link: "https://github.com/cedx"
name: GitHub
- icon: fontawesome/brands/twitter
link: 'https://twitter.com/cedxbelin'
link: "https://twitter.com/cedxbelin"
name: Twitter
- icon: fontawesome/brands/linkedin
link: 'https://linkedin.com/in/cedxbelin'
link: "https://linkedin.com/in/cedxbelin"
name: LinkedIn

markdown_extensions:
- admonition


+ 21
- 21
etc/phpdoc.xml View File

@@ -1,25 +1,25 @@
<?xml version="1.0"?>
<phpdocumentor
configVersion="3"
xmlns="http://www.phpdoc.org"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://docs.phpdoc.org/latest/phpdoc.xsd">
configVersion="3"
xmlns="http://www.phpdoc.org"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://docs.phpdoc.org/latest/phpdoc.xsd">

<title>Which for PHP</title>
<paths>
<cache>../var/phpdoc</cache>
<output>../doc/api</output>
</paths>
<version number="9.0.0">
<api>
<markers>
<marker>TODO</marker>
</markers>
<source dsn="..">
<path>src</path>
</source>
<visibility>protected</visibility>
<visibility>public</visibility>
</api>
</version>
<title>Which for PHP</title>
<paths>
<cache>../var/phpdoc</cache>
<output>../doc/api</output>
</paths>
<version number="9.0.0">
<api>
<markers>
<marker>TODO</marker>
</markers>
<source dsn="..">
<path>src</path>
</source>
<visibility>protected</visibility>
<visibility>public</visibility>
</api>
</version>
</phpdocumentor>

+ 3
- 3
etc/phpstan.neon View File

@@ -1,4 +1,4 @@
parameters:
level: max
paths: [../src, ../test]
treatPhpDocTypesAsCertain: false
level: max
paths: [../src, ../test]
treatPhpDocTypesAsCertain: false

+ 13
- 13
etc/phpunit.xml View File

@@ -1,18 +1,18 @@
<?xml version="1.0"?>
<phpunit bootstrap="../vendor/autoload.php" cacheResult="false" testdox="true">
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../src</directory>
</whitelist>
</filter>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../src</directory>
</whitelist>
</filter>

<logging>
<log type="coverage-clover" target="../var/coverage.xml"/>
</logging>
<logging>
<log type="coverage-clover" target="../var/coverage.xml"/>
</logging>

<testsuites>
<testsuite name="all">
<directory suffix="Test.php">../test</directory>
</testsuite>
</testsuites>
<testsuites>
<testsuite name="all">
<directory suffix="Test.php">../test</directory>
</testsuite>
</testsuites>
</phpunit>

+ 8
- 8
example/main.php View File

@@ -5,13 +5,13 @@ use function Which\{which};

/** Finds the instances of an executable. */
function main(): void {
try {
// `$path` is the absolute path to the executable.
$path = which('foobar');
echo 'The command "foobar" is located at: ', $path;
}
try {
// `$path` is the absolute path to the executable.
$path = which("foobar");
echo "The command 'foobar' is located at: ", $path;
}

catch (FinderException $e) {
echo 'The command "', $e->getCommand(), '" was not found';
}
catch (FinderException $e) {
echo "The command '", $e->getCommand(), "' was not found";
}
}

+ 30
- 30
src/Cli/Command.php View File

@@ -9,39 +9,39 @@ use function Which\{which};

/** The console command. */
class Command extends \Symfony\Component\Console\Command\Command {
/** @var string The command name. */
protected static $defaultName = 'which';
/** @var string The command name. */
protected static $defaultName = "which";

/** Configures the current command. */
protected function configure(): void {
$this
->setDescription('Find the instances of an executable in the system path.')
->addArgument('executable', InputArgument::REQUIRED, 'The executable to find')
->addOption('all', 'a', InputOption::VALUE_NONE, 'List all instances of executables found, instead of just the first one');
}
/** Configures the current command. */
protected function configure(): void {
$this
->setDescription("Find the instances of an executable in the system path.")
->addArgument("executable", InputArgument::REQUIRED, "The executable to find")
->addOption("all", "a", InputOption::VALUE_NONE, "List all instances of executables found, instead of just the first one");
}

/**
* Executes the current command.
* @param InputInterface $input The input arguments and options.
* @param OutputInterface $output The console output.
* @return int The exit code.
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
/** @var string $command */
$command = $input->getArgument('executable');
/**
* Executes the current command.
* @param InputInterface $input The input arguments and options.
* @param OutputInterface $output The console output.
* @return int The exit code.
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
/** @var string $command */
$command = $input->getArgument("executable");

/** @var bool $all */
$all = $input->getOption('all');
/** @var bool $all */
$all = $input->getOption("all");

try {
$executables = which($command, $all);
if (!is_array($executables)) $executables = [$executables];
$output->writeln($executables);
return 0;
}
try {
$executables = which($command, $all);
if (!is_array($executables)) $executables = [$executables];
$output->writeln($executables);
return 0;
}

catch (FinderException $e) { return 1; }
catch (\Throwable $e) { throw new RuntimeException($e->getMessage(), 2); }
}
catch (FinderException $e) { return 1; }
catch (\Throwable $e) { throw new RuntimeException($e->getMessage(), 2); }
}
}

+ 1
- 1
src/Cli/version.g.php View File

@@ -1,4 +1,4 @@
<?php declare(strict_types=1);

// The version number of the package.
return $packageVersion = '9.0.0';
return $packageVersion = "9.0.0";

+ 144
- 144
src/Finder.php View File

@@ -6,148 +6,148 @@ use Webmozart\PathUtil\{Path};
/** Finds the instances of an executable in the system path. */
class Finder {

/** @var \ArrayObject<int, string> The list of executable file extensions. */
private \ArrayObject $extensions;
/** @var \ArrayObject<int, string> The list of system paths. */
private \ArrayObject $path;
/** @var string The character used to separate paths in the system path. */
private string $pathSeparator;
/**
* Creates a new finder.
* @param string|string[] $path The system path. Defaults to the `PATH` environment variable.
* @param string|string[] $extensions The executable file extensions. Defaults to the `PATHEXT` environment variable.
* @param string $pathSeparator The character used to separate paths in the system path. Defaults to the `PATH_SEPARATOR` constant.
*/
function __construct($path = [], $extensions = [], string $pathSeparator = '') {
assert(is_string($path) || is_array($path));
assert(is_string($extensions) || is_array($extensions));
$this->pathSeparator = mb_strlen($pathSeparator) ? $pathSeparator : (static::isWindows() ? ';' : PATH_SEPARATOR);
if (!is_array($path))
$path = array_filter(explode($this->pathSeparator, $path) ?: [], fn($item) => mb_strlen($item) > 0);
if (!count($path)) {
$pathEnv = (string) getenv('PATH');
if (mb_strlen($pathEnv)) $path = explode($this->pathSeparator, $pathEnv) ?: [];
}
if (!is_array($extensions))
$extensions = array_filter(explode($this->pathSeparator, $extensions) ?: [], fn($item) => mb_strlen($item) > 0);
if (!count($extensions) && static::isWindows()) {
$pathExt = (string) getenv('PATHEXT');
$extensions = mb_strlen($pathExt) ? (explode($this->pathSeparator, $pathExt) ?: []) : ['.exe', '.cmd', '.bat', '.com'];
}
$this->extensions = new \ArrayObject(array_map('mb_strtolower', $extensions));
$this->path = new \ArrayObject(array_map(fn($directory) => trim($directory, '"'), $path));
}
/**
* Gets a value indicating whether the current platform is Windows.
* @return bool `true` if the current platform is Windows, otherwise `false`.
*/
static function isWindows(): bool {
if (PHP_OS_FAMILY == 'Windows') return true;
$osType = (string) getenv('OSTYPE');
return $osType == 'cygwin' || $osType == 'msys';
}
/**
* Finds the instances of an executable in the system path.
* @param string $command The command to be resolved.
* @return iterable<\SplFileInfo> The paths of the executables found.
*/
function find(string $command): iterable {
foreach ($this->getPath() as $directory) yield from $this->findExecutables($directory, $command);
}
/**
* Gets the list of executable file extensions.
* @return \ArrayObject<int, string> The list of executable file extensions.
*/
function getExtensions(): \ArrayObject {
return $this->extensions;
}
/**
* Gets the list of system paths.
* @return \ArrayObject<int, string> The list of system paths.
*/
function getPath(): \ArrayObject {
return $this->path;
}
/**
* Gets the character used to separate paths in the system path.
* @return string The character used to separate paths in the system path.
*/
function getPathSeparator(): string {
return $this->pathSeparator;
}
/**
* Gets a value indicating whether the specified file is executable.
* @param string $file The path of the file to be checked.
* @return bool `true` if the specified file is executable, otherwise `false`.
*/
function isExecutable(string $file): bool {
assert(mb_strlen($file) > 0);
$fileInfo = new \SplFileInfo($file);
if (!$fileInfo->isFile()) return false;
if ($fileInfo->isExecutable()) return true;
return static::isWindows() ? $this->checkFileExtension($fileInfo) : $this->checkFilePermissions($fileInfo);
}
/**
* Checks that the specified file is executable according to the executable file extensions.
* @param \SplFileInfo $fileInfo The file to be checked.
* @return bool Value indicating whether the specified file is executable.
*/
private function checkFileExtension(\SplFileInfo $fileInfo): bool {
$extension = mb_strtolower($fileInfo->getExtension());
return mb_strlen($extension) ? in_array(".$extension", (array) $this->getExtensions()) : false;
}
/**
* Checks that the specified file is executable according to its permissions.
* @param \SplFileInfo $fileInfo The file to be checked.
* @return bool Value indicating whether the specified file is executable.
*/
private function checkFilePermissions(\SplFileInfo $fileInfo): bool {
// Others.
$perms = $fileInfo->getPerms();
if ($perms & 0001) return true;
// Group.
$gid = function_exists('posix_getgid') ? posix_getgid() : -1;
if ($perms & 0010) return $gid == $fileInfo->getGroup();
// Owner.
$uid = function_exists('posix_getuid') ? posix_getuid() : -1;
if ($perms & 0100) return $uid == $fileInfo->getOwner();
// Root.
return $perms & (0100 | 0010) ? $uid == 0 : false;
}
/**
* Finds the instances of an executable in the specified directory.
* @param string $directory The directory path.
* @param string $command The command to be resolved.
* @return iterable<\SplFileInfo> The paths of the executables found.
*/
private function findExecutables(string $directory, string $command): iterable {
assert(mb_strlen($directory) > 0);
assert(mb_strlen($command) > 0);
$basePath = (string) getcwd();
foreach (['', ...(array) $this->getExtensions()] as $extension) {
$resolvedPath = Path::makeAbsolute(Path::join($directory, $command).mb_strtolower($extension), $basePath);
if ($this->isExecutable($resolvedPath)) yield new \SplFileInfo(str_replace('/', DIRECTORY_SEPARATOR, $resolvedPath));
}
}
/** @var \ArrayObject<int, string> The list of executable file extensions. */
private \ArrayObject $extensions;
/** @var \ArrayObject<int, string> The list of system paths. */
private \ArrayObject $path;
/** @var string The character used to separate paths in the system path. */
private string $pathSeparator;
/**
* Creates a new finder.
* @param string|string[] $path The system path. Defaults to the `PATH` environment variable.
* @param string|string[] $extensions The executable file extensions. Defaults to the `PATHEXT` environment variable.
* @param string $pathSeparator The character used to separate paths in the system path. Defaults to the `PATH_SEPARATOR` constant.
*/
function __construct($path = [], $extensions = [], string $pathSeparator = "") {
assert(is_string($path) || is_array($path));
assert(is_string($extensions) || is_array($extensions));
$this->pathSeparator = mb_strlen($pathSeparator) ? $pathSeparator : (static::isWindows() ? ";" : PATH_SEPARATOR);
if (!is_array($path))
$path = array_filter(explode($this->pathSeparator, $path) ?: [], fn($item) => mb_strlen($item) > 0);
if (!count($path)) {
$pathEnv = (string) getenv("PATH");
if (mb_strlen($pathEnv)) $path = explode($this->pathSeparator, $pathEnv) ?: [];
}
if (!is_array($extensions))
$extensions = array_filter(explode($this->pathSeparator, $extensions) ?: [], fn($item) => mb_strlen($item) > 0);
if (!count($extensions) && static::isWindows()) {
$pathExt = (string) getenv("PATHEXT");
$extensions = mb_strlen($pathExt) ? (explode($this->pathSeparator, $pathExt) ?: []) : [".exe", ".cmd", ".bat", ".com"];
}
$this->extensions = new \ArrayObject(array_map("mb_strtolower", $extensions));
$this->path = new \ArrayObject(array_map(fn($directory) => trim($directory, '"'), $path));
}
/**
* Gets a value indicating whether the current platform is Windows.
* @return bool `true` if the current platform is Windows, otherwise `false`.
*/
static function isWindows(): bool {
if (PHP_OS_FAMILY == "Windows") return true;
$osType = (string) getenv("OSTYPE");
return $osType == "cygwin" || $osType == "msys";
}
/**
* Finds the instances of an executable in the system path.
* @param string $command The command to be resolved.
* @return iterable<\SplFileInfo> The paths of the executables found.
*/
function find(string $command): iterable {
foreach ($this->getPath() as $directory) yield from $this->findExecutables($directory, $command);
}
/**
* Gets the list of executable file extensions.
* @return \ArrayObject<int, string> The list of executable file extensions.
*/
function getExtensions(): \ArrayObject {
return $this->extensions;
}
/**
* Gets the list of system paths.
* @return \ArrayObject<int, string> The list of system paths.
*/
function getPath(): \ArrayObject {
return $this->path;
}
/**
* Gets the character used to separate paths in the system path.
* @return string The character used to separate paths in the system path.
*/
function getPathSeparator(): string {
return $this->pathSeparator;
}
/**
* Gets a value indicating whether the specified file is executable.
* @param string $file The path of the file to be checked.
* @return bool `true` if the specified file is executable, otherwise `false`.
*/
function isExecutable(string $file): bool {
assert(mb_strlen($file) > 0);
$fileInfo = new \SplFileInfo($file);
if (!$fileInfo->isFile()) return false;
if ($fileInfo->isExecutable()) return true;
return static::isWindows() ? $this->checkFileExtension($fileInfo) : $this->checkFilePermissions($fileInfo);
}
/**
* Checks that the specified file is executable according to the executable file extensions.
* @param \SplFileInfo $fileInfo The file to be checked.
* @return bool Value indicating whether the specified file is executable.
*/
private function checkFileExtension(\SplFileInfo $fileInfo): bool {
$extension = mb_strtolower($fileInfo->getExtension());
return mb_strlen($extension) ? in_array(".$extension", (array) $this->getExtensions()) : false;
}
/**
* Checks that the specified file is executable according to its permissions.
* @param \SplFileInfo $fileInfo The file to be checked.
* @return bool Value indicating whether the specified file is executable.
*/
private function checkFilePermissions(\SplFileInfo $fileInfo): bool {
// Others.
$perms = $fileInfo->getPerms();
if ($perms & 0001) return true;
// Group.
$gid = function_exists("posix_getgid") ? posix_getgid() : -1;
if ($perms & 0010) return $gid == $fileInfo->getGroup();
// Owner.
$uid = function_exists("posix_getuid") ? posix_getuid() : -1;
if ($perms & 0100) return $uid == $fileInfo->getOwner();
// Root.
return $perms & (0100 | 0010) ? $uid == 0 : false;
}
/**
* Finds the instances of an executable in the specified directory.
* @param string $directory The directory path.
* @param string $command The command to be resolved.
* @return iterable<\SplFileInfo> The paths of the executables found.
*/
private function findExecutables(string $directory, string $command): iterable {
assert(mb_strlen($directory) > 0);
assert(mb_strlen($command) > 0);
$basePath = (string) getcwd();
foreach (["", ...(array) $this->getExtensions()] as $extension) {
$resolvedPath = Path::makeAbsolute(Path::join($directory, $command).mb_strtolower($extension), $basePath);
if ($this->isExecutable($resolvedPath)) yield new \SplFileInfo(str_replace("/", DIRECTORY_SEPARATOR, $resolvedPath));
}
}
}

+ 31
- 31
src/FinderException.php View File

@@ -4,39 +4,39 @@ namespace Which;
/** An exception caused by a `Finder` in a command lookup. */
class FinderException extends \RuntimeException {

/** @var string The looked up command. */
private string $command;
/** @var string The looked up command. */
private string $command;

/** @var Finder The finder used to lookup the command. */
private Finder $finder;
/** @var Finder The finder used to lookup the command. */
private Finder $finder;

/**
* Creates a new finder exception.
* @param string $command The looked up command.
* @param Finder $finder The finder used to lookup the command.
* @param string $message A message describing the error.
* @param \Throwable|null $previous The previous exception used for the exception chaining.
*/
function __construct(string $command, Finder $finder, string $message = '', ?\Throwable $previous = null) {
assert(mb_strlen($command) > 0);
parent::__construct($message, 0, $previous);
$this->command = $command;
$this->finder = $finder;
}
/**
* Creates a new finder exception.
* @param string $command The looked up command.
* @param Finder $finder The finder used to lookup the command.
* @param string $message A message describing the error.
* @param \Throwable|null $previous The previous exception used for the exception chaining.
*/
function __construct(string $command, Finder $finder, string $message = "", ?\Throwable $previous = null) {
assert(mb_strlen($command) > 0);
parent::__construct($message, 0, $previous);
$this->command = $command;
$this->finder = $finder;
}

/**
* Gets the name of the looked up command.
* @return string The looked up command.
*/
function getCommand(): string {
return $this->command;
}
/**
* Gets the name of the looked up command.
* @return string The looked up command.
*/
function getCommand(): string {
return $this->command;
}

/**
* Gets the instance of the finder used to lookup the command.
* @return Finder The finder used to lookup the command.
*/
function getFinder(): Finder {
return $this->finder;
}
/**
* Gets the instance of the finder used to lookup the command.
* @return Finder The finder used to lookup the command.
*/
function getFinder(): Finder {
return $this->finder;
}
}

+ 16
- 16
src/which.php View File

@@ -11,24 +11,24 @@ namespace Which;
* @throws FinderException The specified command was not found.
*/
function which(string $command, bool $all = false, callable $onError = null, array $options = []) {
assert(mb_strlen($command) > 0);
assert(mb_strlen($command) > 0);

$finder = new Finder(
$options['path'] ?? [],
$options['extensions'] ?? [],
$options['pathSeparator'] ?? ''
);
$finder = new Finder(
$options["path"] ?? [],
$options["extensions"] ?? [],
$options["pathSeparator"] ?? ""
);

$list = [];
foreach ($finder->find($command) as $executable) {
if (!$all) return $executable->getPathname();
$list[] = $executable->getPathname();
}
$list = [];
foreach ($finder->find($command) as $executable) {
if (!$all) return $executable->getPathname();
$list[] = $executable->getPathname();
}

if (!count($list)) {
if ($onError) return call_user_func($onError, $command);
throw new FinderException($command, $finder, "Command '$command' not found");
}
if (!count($list)) {
if ($onError) return call_user_func($onError, $command);
throw new FinderException($command, $finder, "Command '$command' not found");
}

return array_unique($list);
return array_unique($list);
}

+ 51
- 51
test/FinderTest.php View File

@@ -7,55 +7,55 @@ use function PHPUnit\Framework\{assertThat, countOf, equalTo, isFalse, logicalNo
/** @testdox Which\Finder */
class FinderTest extends TestCase {

/** @testdox constructor */
function testConstructor(): void {
// It should set the `path` property to the value of the `PATH` environment variable by default.
$pathEnv = (string) getenv('PATH');
$path = mb_strlen($pathEnv) ? explode(PATH_SEPARATOR, $pathEnv) : [];
assertThat((array) (new Finder)->getPath(), equalTo($path));
// It should split the input path using the path separator.
$path = ['/usr/local/bin', '/usr/bin'];
assertThat((array) (new Finder(implode(PATH_SEPARATOR, $path)))->getPath(), equalTo($path));
// It should set the `extensions` property to the value of the `PATHEXT` environment variable by default.
$pathExt = (string) getenv('PATHEXT');
$extensions = mb_strlen($pathExt) ? array_map('mb_strtolower', explode(PATH_SEPARATOR, $pathExt)) : [];
assertThat((array) (new Finder)->getExtensions(), equalTo($extensions));
// It should split the extension list using the path separator.
$extensions = ['.EXE', '.CMD', '.BAT'];
assertThat((array) (new Finder('', implode(PATH_SEPARATOR, $extensions)))->getExtensions(), equalTo(['.exe', '.cmd', '.bat']));
// It should set the `pathSeparator` property to the value of the `PATH_SEPARATOR` constant by default.
assertThat((new Finder)->getPathSeparator(), equalTo(PATH_SEPARATOR));
// It should properly set the path separator.
assertThat((new Finder('', '', '#'))->getPathSeparator(), equalTo('#'));
}
/** @testdox ->find() */
function testFind(): void {
// It should return the path of the `executable.cmd` file on Windows.
$executables = [...(new Finder('test/fixtures'))->find('executable')];
assertThat($executables, countOf(Finder::isWindows() ? 1 : 0));
if (Finder::isWindows()) assertThat($executables[0]->getPathname(), stringEndsWith('\\test\\fixtures\\executable.cmd'));
// It should return the path of the `executable.sh` file on POSIX.
$executables = [...(new Finder('test/fixtures'))->find('executable.sh')];
assertThat($executables, countOf(Finder::isWindows() ? 0 : 1));
if (!Finder::isWindows()) assertThat($executables[0]->getPathname(), stringEndsWith('/test/fixtures/executable.sh'));
}
/** @testdox ->isExecutable() */
function testIsExecutable(): void {
// It should return `false` for a non-executable file.
assertThat((new Finder)->isExecutable(__FILE__), isFalse());
// It should return `false` for a POSIX executable, when test is run on Windows.
assertThat((new Finder)->isExecutable('test/fixtures/executable.sh'), logicalNot(equalTo(Finder::isWindows())));
// It should return `false` for a Windows executable, when test is run on POSIX.
assertThat((new Finder)->isExecutable('test/fixtures/executable.cmd'), equalTo(Finder::isWindows()));
}
/** @testdox constructor */
function testConstructor(): void {
// It should set the `path` property to the value of the `PATH` environment variable by default.
$pathEnv = (string) getenv("PATH");
$path = mb_strlen($pathEnv) ? explode(PATH_SEPARATOR, $pathEnv) : [];
assertThat((array) (new Finder)->getPath(), equalTo($path));
// It should split the input path using the path separator.
$path = ["/usr/local/bin", "/usr/bin"];
assertThat((array) (new Finder(implode(PATH_SEPARATOR, $path)))->getPath(), equalTo($path));
// It should set the `extensions` property to the value of the `PATHEXT` environment variable by default.
$pathExt = (string) getenv("PATHEXT");
$extensions = mb_strlen($pathExt) ? array_map("mb_strtolower", explode(PATH_SEPARATOR, $pathExt)) : [];
assertThat((array) (new Finder)->getExtensions(), equalTo($extensions));
// It should split the extension list using the path separator.
$extensions = [".EXE", ".CMD", ".BAT"];
assertThat((array) (new Finder("", implode(PATH_SEPARATOR, $extensions)))->getExtensions(), equalTo([".exe", ".cmd", ".bat"]));
// It should set the `pathSeparator` property to the value of the `PATH_SEPARATOR` constant by default.
assertThat((new Finder)->getPathSeparator(), equalTo(PATH_SEPARATOR));
// It should properly set the path separator.
assertThat((new Finder("", "", "#"))->getPathSeparator(), equalTo("#"));
}
/** @testdox ->find() */
function testFind(): void {
// It should return the path of the `executable.cmd` file on Windows.
$executables = [...(new Finder("test/fixtures"))->find("executable")];
assertThat($executables, countOf(Finder::isWindows() ? 1 : 0));
if (Finder::isWindows()) assertThat($executables[0]->getPathname(), stringEndsWith("\\test\\fixtures\\executable.cmd"));
// It should return the path of the `executable.sh` file on POSIX.
$executables = [...(new Finder("test/fixtures"))->find("executable.sh")];
assertThat($executables, countOf(Finder::isWindows() ? 0 : 1));
if (!Finder::isWindows()) assertThat($executables[0]->getPathname(), stringEndsWith("/test/fixtures/executable.sh"));
}
/** @testdox ->isExecutable() */
function testIsExecutable(): void {
// It should return `false` for a non-executable file.
assertThat((new Finder)->isExecutable(__FILE__), isFalse());
// It should return `false` for a POSIX executable, when test is run on Windows.
assertThat((new Finder)->isExecutable("test/fixtures/executable.sh"), logicalNot(equalTo(Finder::isWindows())));
// It should return `false` for a Windows executable, when test is run on POSIX.
assertThat((new Finder)->isExecutable("test/fixtures/executable.cmd"), equalTo(Finder::isWindows()));
}
}

+ 57
- 57
test/WhichTest.php View File

@@ -7,70 +7,70 @@ use function PHPUnit\Framework\{assertThat, countOf, equalTo, isInstanceOf, isTy
/** @testdox Which\which */
class WhichTest extends TestCase {

/** @testdox which() */
function testWhich(): void {
// It should return the path of the `executable.cmd` file on Windows.
try {
$executable = which('executable', false, null, ['path' => 'test/fixtures']);
if (Finder::isWindows()) assertThat($executable, stringEndsWith('\\test\\fixtures\\executable.cmd'));
else Assert::fail('Exception not thrown');
}
/** @testdox which() */
function testWhich(): void {
// It should return the path of the `executable.cmd` file on Windows.
try {
$executable = which("executable", false, null, ["path" => "test/fixtures"]);
if (Finder::isWindows()) assertThat($executable, stringEndsWith("\\test\\fixtures\\executable.cmd"));
else Assert::fail("Exception not thrown");
}

catch (\Throwable $e) {
if (Finder::isWindows()) Assert::fail($e->getMessage());
else assertThat($e, isInstanceOf(FinderException::class));
}
catch (\Throwable $e) {
if (Finder::isWindows()) Assert::fail($e->getMessage());
else assertThat($e, isInstanceOf(FinderException::class));
}

// It should return all the paths of the `executable.cmd` file on Windows.
try {
$executables = which('executable', true, null, ['path' => 'test/fixtures']);
if (!Finder::isWindows()) Assert::fail('Exception not thrown');
else {
assertThat($executables, logicalAnd(isType('array'), countOf(1)));
assertThat($executables[0], stringEndsWith('\\test\\fixtures\\executable.cmd'));
}
}
// It should return all the paths of the `executable.cmd` file on Windows.
try {
$executables = which("executable", true, null, ["path" => "test/fixtures"]);
if (!Finder::isWindows()) Assert::fail("Exception not thrown");
else {
assertThat($executables, logicalAnd(isType("array"), countOf(1)));
assertThat($executables[0], stringEndsWith("\\test\\fixtures\\executable.cmd"));
}
}

catch (\Throwable $e) {
if (Finder::isWindows()) Assert::fail($e->getMessage());
else assertThat($e, isInstanceOf(FinderException::class));
}
catch (\Throwable $e) {
if (Finder::isWindows()) Assert::fail($e->getMessage());
else assertThat($e, isInstanceOf(FinderException::class));
}

// It should return the path of the `executable.sh` file on POSIX.
try {
$executable = which('executable.sh', false, null, ['path' => 'test/fixtures']);
if (Finder::isWindows()) Assert::fail('Exception not thrown');
else assertThat($executable, stringEndsWith('/test/fixtures/executable.sh'));
}
// It should return the path of the `executable.sh` file on POSIX.
try {
$executable = which("executable.sh", false, null, ["path" => "test/fixtures"]);
if (Finder::isWindows()) Assert::fail("Exception not thrown");
else assertThat($executable, stringEndsWith("/test/fixtures/executable.sh"));
}

catch (\Throwable $e) {
if (Finder::isWindows()) assertThat($e, isInstanceOf(FinderException::class));
else Assert::fail($e->getMessage());
}
catch (\Throwable $e) {
if (Finder::isWindows()) assertThat($e, isInstanceOf(FinderException::class));
else Assert::fail($e->getMessage());
}

// It should return all the paths of the `executable.sh` file on POSIX.
try {
$executables = which('executable.sh', true, null, ['path' => 'test/fixtures']);
if (Finder::isWindows()) Assert::fail('Exception not thrown');
else {
assertThat($executables, logicalAnd(isType('array'), countOf(1)));
assertThat($executables[0], stringEndsWith('/test/fixtures/executable.sh'));
}
}
// It should return all the paths of the `executable.sh` file on POSIX.
try {
$executables = which("executable.sh", true, null, ["path" => "test/fixtures"]);
if (Finder::isWindows()) Assert::fail("Exception not thrown");
else {
assertThat($executables, logicalAnd(isType("array"), countOf(1)));
assertThat($executables[0], stringEndsWith("/test/fixtures/executable.sh"));
}
}

catch (\Throwable $e) {
if (Finder::isWindows()) assertThat($e, isInstanceOf(FinderException::class));
else Assert::fail($e->getMessage());
}
catch (\Throwable $e) {
if (Finder::isWindows()) assertThat($e, isInstanceOf(FinderException::class));
else Assert::fail($e->getMessage());
}

// It should return the value of the `onError` handler.
$executable = which('executable', false, fn() => 'foo', ['path' => 'test/fixtures']);
if (!Finder::isWindows()) assertThat($executable, equalTo('foo'));
// It should return the value of the `onError` handler.
$executable = which("executable", false, fn() => "foo", ["path" => "test/fixtures"]);
if (!Finder::isWindows()) assertThat($executable, equalTo("foo"));

$executables = which('executable.sh', true, fn() => ['foo'], ['path' => 'test/fixtures']);
if (Finder::isWindows()) {
assertThat($executables, logicalAnd(isType('array'), countOf(1)));
assertThat($executables[0], equalTo('foo'));
}
}
$executables = which("executable.sh", true, fn() => ["foo"], ["path" => "test/fixtures"]);
if (Finder::isWindows()) {
assertThat($executables, logicalAnd(isType("array"), countOf(1)));
assertThat($executables[0], equalTo("foo"));
}
}
}

+ 3
- 3
tool/build.ps1 View File

@@ -4,9 +4,9 @@ Set-Location (Split-Path $PSScriptRoot)

$version = (Get-Content composer.json | ConvertFrom-Json).version
$lines = @(
'<?php declare(strict_types=1);', '',
'// The version number of the package.',
"return `$packageVersion = '$version';"
"<?php declare(strict_types=1);", "",
"// The version number of the package.",
"return `$packageVersion = ""$version"";"
)

Set-Content src/Cli/version.g.php ($lines -join [Environment]::NewLine)

+ 3
- 3
tool/clean.ps1 View File

@@ -2,10 +2,10 @@
Set-StrictMode -Version Latest
Set-Location (Split-Path $PSScriptRoot)

foreach ($item in 'build', 'doc/api', 'www') {
if (Test-Path $item) { Remove-Item $item -Recurse }
foreach ($item in "build", "doc/api", "www") {
if (Test-Path $item) { Remove-Item $item -Recurse }
}

foreach ($item in Get-ChildItem var -Exclude .gitkeep) {
Remove-Item $item -Recurse
Remove-Item $item -Recurse
}

+ 1
- 1
tool/coverage.ps1 View File

@@ -2,5 +2,5 @@
Set-StrictMode -Version Latest
Set-Location (Split-Path $PSScriptRoot)

$composer = $IsWindows ? 'php "C:/Program Files/PHP/share/composer.phar"' : 'composer'
$composer = $IsWindows ? 'php "C:/Program Files/PHP/share/composer.phar"' : "composer"
Invoke-Expression "$composer global exec coveralls var/coverage.xml"

+ 1
- 1
tool/doc.ps1 View File

@@ -2,7 +2,7 @@
Set-StrictMode -Version Latest
Set-Location (Split-Path $PSScriptRoot)

$phpdoc = $IsWindows ? 'php "C:/Program Files/PHP/share/phpDocumentor.phar"' : 'phpdoc';
$phpdoc = $IsWindows ? 'php "C:/Program Files/PHP/share/phpDocumentor.phar"' : "phpdoc";
Invoke-Expression "$phpdoc --config=etc/phpdoc.xml"

if (-not (Test-Path doc/api/images)) { New-Item doc/api/images -ItemType Directory | Out-Null }


+ 1
- 1
tool/upgrade.ps1 View File

@@ -6,5 +6,5 @@ git reset --hard
git fetch --all --prune
git pull --rebase

$composer = $IsWindows ? 'php "C:/Program Files/PHP/share/composer.phar"' : 'composer'
$composer = $IsWindows ? 'php "C:/Program Files/PHP/share/composer.phar"' : "composer"
Invoke-Expression "$composer update --no-interaction"

+ 10
- 10
tool/watch.ps1 View File

@@ -4,26 +4,26 @@ Set-Location (Split-Path $PSScriptRoot)
[Console]::TreatControlCAsInput = $true

$action = {
if ($EventArgs.Name -notlike '*.g.php') {
$changeType = [String] $EventArgs.ChangeType
Write-Host "'$($EventArgs.Name)' was $($changeType.ToLower()): starting a new build..."
$timeSpan = Measure-Command { tool/build.ps1 }
Write-Host "> Finished the build after $($timeSpan.TotalSeconds) seconds."
}
if ($EventArgs.Name -notlike "*.g.php") {
$changeType = [String] $EventArgs.ChangeType
Write-Host "'$($EventArgs.Name)' was $($changeType.ToLower()): starting a new build..."
$timeSpan = Measure-Command { tool/build.ps1 }
Write-Host "> Finished the build after $($timeSpan.TotalSeconds) seconds."
}
}

$watcher = New-Object System.IO.FileSystemWatcher (Resolve-Path src).Path
$watcher.EnableRaisingEvents = $true
$watcher.IncludeSubdirectories = $true

foreach ($event in 'Changed', 'Created', 'Deleted', 'Renamed') {
Register-ObjectEvent $watcher $event -Action $action | Out-Null
foreach ($event in "Changed", "Created", "Deleted", "Renamed") {
Register-ObjectEvent $watcher $event -Action $action | Out-Null
}

$console = $Host.UI.RawUI;
while ($true) {
if ($console.KeyAvailable -and ($console.ReadKey('AllowCtrlC,IncludeKeyUp,NoEcho').Character -eq 3)) { break }
Start-Sleep -Milliseconds 200
if ($console.KeyAvailable -and ($console.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character -eq 3)) { break }
Start-Sleep -Milliseconds 200
}

Get-EventSubscriber | Unregister-Event