8000 feat(shell): introduce error output behavior feature by azjezz · Pull Request #334 · azjezz/psl · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(shell): introduce error output behavior feature #334

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments. Retry
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@
* **BC** - `Psl\Result\ResultInterface::getException()` method has been renamed to `Psl\Result\ResultInterface::getThrowable()`
* **BC** - `Psl\Result\wrap` function now catches all `Throwable`s instead of only `Exception`s
* introduced a new `Psl\Result\reflect` function
* **BC** - `Psl\Shell\escape_argument` function has been removed, `Shell\execute` arguments are now always escaped.
* **BC** - `$escape_arguments` argument of `Shell\execute` function has been removed.
* introduced a new `Psl\Shell\ErrorOutputBehavior` enum
* added a new `$error_output_behavior` argument to `Shell\execute` function, which can be used to return the command error output content, as well as the standard output content.
1 change: 1 addition & 0 deletions config/.phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<exclude-pattern>*/src/Psl/Hash/Hmac/Algorithm.php</exclude-pattern>
<exclude-pattern>*/src/Psl/OS/OperatingSystemFamily.php</exclude-pattern>
<exclude-pattern>*/src/Psl/Password/Algorithm.php</exclude-pattern>
<exclude-pattern>*/src/Psl/Shell/ErrorOutputBehavior.php</exclude-pattern>

<arg name="basepath" value="."/>
<arg name="colors"/>
Expand Down
6 changes: 5 additions & 1 deletion docs/component/shell.md
10000
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

#### `Functions`

- [escape_argument](./../../src/Psl/Shell/escape_argument.php#L17)
- [execute](./../../src/Psl/Shell/execute.php#L42)
- [unpack](./../../src/Psl/Shell/unpack.php#L20)

#### `Enums`

- [ErrorOutputBehavior](./../../src/Psl/Shell/ErrorOutputBehavior.php#L7)


5 changes: 4 additions & 1 deletion src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,9 @@ final class Loader
'Psl\Encoding\Base64\decode',
'Psl\Encoding\Hex\encode',
'Psl\Encoding\Hex\decode',
'Psl\Shell\escape_argument',
'Psl\Shell\execute',
'Psl\Shell\unpack',
'Psl\Shell\Internal\escape_argument',
'Psl\Html\encode',
'Psl\Html\encode_special_characters',
'Psl\Html\decode',
Expand Down Expand Up @@ -634,6 +635,7 @@ final class Loader
'Psl\Shell\Exception\RuntimeException',
'Psl\Shell\Exception\PossibleAttackException',
'Psl\Shell\Exception\TimeoutException',
'Psl\Shell\Exception\InvalidArgumentException',
'Psl\Math\Exception\ArithmeticException',
'Psl\Math\Exception\DivisionByZeroException',
'Psl\Filesystem\Exception\RuntimeException',
Expand Down Expand Up @@ -743,6 +745,7 @@ final class Loader
'Psl\Hash\Hmac\Algorithm',
'Psl\OS\OperatingSystemFamily',
'Psl\Password\Algorithm',
'Psl\Shell\ErrorOutputBehavior'
];

public const TYPE_CONSTANTS = 1;
Expand Down
56 changes: 56 additions & 0 deletions src/Psl/Shell/ErrorOutputBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Psl\Shell;

enum ErrorOutputBehavior {
/**
* Discard the standard error output.
*
* Example:
*
* $stdout = Shell\execute('cmd', ['arg1', 'arg2'], error_output_behavior: ErrorOutputBehavior::Discard);
*/
case Discard;

/**
* Append the standard error output content to the standard output content.
*
* Example:
*
* $stdout_followed_by_stderr = Shell\execute('cmd', ['arg1', 'arg2'], error_output_behavior: ErrorOutputBehavior::Append);
*/
case Append;

/**
* Prepend the standard error output content to the standard output content.
*
* Example:
*
* $stderr_followed_by_stdout = Shell\execute('cmd', ['arg1', 'arg2'], error_output_behavior: ErrorOutputBehavior::Prepend);
*/
case Prepend;

/**
* Replace the standard output content with the standard error output content.
*
* Example:
*
* $stderr = Shell\execute('cmd', ['arg1', 'arg2'], error_output_behavior: ErrorOutputBehavior::Replace);
*/
case Replace;

/**
* Pack the standard output content with the standard error output content, enabling
* you to split them later on, using `Shell\unpack`.
*
* Example:
*
* $result = Shell\execute('cmd', ['arg1', 'arg2'], error_output_behavior: ErrorOutputBehavior::Packed);
* [$stdout, $stderr] = Shell\unpack($result);
*
* @note The packing format is not guaranteed to be BC, you should always use `Shell\unpack` instead of attempting to unpack the result manually.
*/
case Packed;
}
11 changes: 11 additions & 0 deletions src/Psl/Shell/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Shell\Exception;

use Psl\Exception;

final class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace P B422 sl\Shell;
namespace Psl\Shell\Internal;

use Psl\Regex;
use Psl\Str\Byte;
Expand All @@ -13,6 +13,8 @@
* Escape a string to be used as a shell argument.
*
* @psalm-taint-escape shell
*
* @internal
*/
function escape_argument(string $argument): string
{
Expand Down
46 changes: 29 additions & 17 deletions src/Psl/Shell/execute.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Psl\Vec;

use function is_resource;
use function pack;
use function proc_close;
use function proc_open;
use function strpbrk;
Expand All @@ -30,7 +31,6 @@
* use the default value ( the current directory )
* @param array<string, string> $environment A dict with the environment variables for the command that
* will be run.
* @param bool $escape_arguments If set to true ( default ), all $arguments will be escaped using `escape_argument`.
*
* @psalm-taint-sink shell $command
*
Expand All @@ -44,23 +44,10 @@ function execute(
array $arguments = [],
?string $working_directory = null,
array $environment = [],
bool $escape_arguments = true,
ErrorOutputBehavior $error_output_behavior = ErrorOutputBehavior::Discard,
?float $timeout = null
): string {
if ($escape_arguments) {
$arguments = Vec\map(
$arguments,
/**
* @param string $argument
*
* @return string
*
* @pure
*/
static fn(string $argument): string => escape_argument($argument)
);
}

$arguments = Vec\map($arguments, Internal\escape_argument(...));
$commandline = Str\join([$command, ...$arguments], ' ');

/** @psalm-suppress MissingThrowsDocblock - safe ( $offset is within-of-bounds ) */
Expand Down Expand Up @@ -201,5 +188,30 @@ static function (array $m) use (
throw new Exception\FailedExecutionException($commandline, $stdout_content, $stderr_content, $code);
}

return $stdout_content;
if (ErrorOutputBehavior::Packed === $error_output_behavior) {
$result = '';
$stdout_length = Str\Byte\length($stdout_content);
$stderr_length = Str\Byte\length($stderr_content);

if ($stdout_length) {
$stdout_header = pack('C1N1', 1, $stdout_length);

$result .= $stdout_header . $stdout_content;
}

if ($stderr_length) {
$stderr_header = pack('C1N1', 2, $stderr_length);

$result .= $stderr_header . $stderr_content;
}

return $result;
}

return match ($error_output_behavior) {
ErrorOutputBehavior::Prepend => $stderr_content . $stdout_content,
ErrorOutputBehavior::Append => $stdout_content . $stderr_content,
ErrorOutputBehavior::Replace => $stderr_content,
ErrorOutputBehavior::Packed, ErrorOutputBehavior::Discard => $stdout_content,
};
}
51 changes: 51 additions & 0 deletions src/Psl/Shell/unpack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Psl\Shell;

use Psl\Str;

use function unpack as byte_unpack;

/**
* Unpack the result of `Shell\execute()` when using `ErrorOutputBehavior::Packed` error output behavior.
*
* @param string $content
*
* @throws Exception\InvalidArgumentException If $content is invalid.
*
* @return array{0: string, 1: string} - A tuple, containing the standard output content as it's first element, followed by the standard error output content.
*/
function unpack(string $content): array
{
$result = ['', ''];
while ($content !== '') {
if (Str\Byte\length($content) < 5) {
throw new Exception\InvalidArgumentException('$content contains an invalid header value.');
}

$headers = byte_unpack('C1type/N1size', Str\Byte\slice($content, 0, 5));
/** @var int<0, max> $type */
$type = (int) $headers['type'];
/** @var int<0, max> $size */
$size = (int) $headers['size'];

if ($size > (Str\Byte\length($content) - 5)) {
throw new Exception\InvalidArgumentException('$content contains an invalid header value.');
}

$chunk = Str\Byte\slice($content, 5, $size);
$content = Str\Byte\slice($content, $size + 5);

if ($type === 1) {
$result[0] .= $chunk;
} elseif ($type === 2) {
$result[1] .= $chunk;
} else {
throw new Exception\InvalidArgumentException('$content contains an invalid header value.');
}
}

return $result;
}
41 changes: 0 additions & 41 deletions tests/unit/Shell/EscapeArgumentTest.php

This file was deleted.

38 changes: 38 additions & 0 deletions tests/unit/Shell/ExecuteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,42 @@ public function testItThrowsWhenWorkingDirectoryDoesntExist(): void

Shell\execute(PHP_BINARY, ['-r', 'echo getcwd();'], $dir);
}

public function testErrorOutputIsDiscarded(): void
{
$result = Shell\execute(PHP_BINARY, ['-r', 'fwrite(STDOUT, "hello"); fwrite(STDERR, " world");']);

static::assertSame('hello', $result);
}

public function testErrorOutputIsAppended(): void
{
$result = Shell\execute(PHP_BINARY, ['-r', 'fwrite(STDOUT, "hello"); fwrite(STDERR, " world");'], error_output_behavior: Shell\ErrorOutputBehavior::Append);

static::assertSame('hello world', $result);
}

public function testErrorOutputIsPrepended(): void
{
$result = Shell\execute(PHP_BINARY, ['-r', 'fwrite(STDOUT, "hello"); fwrite(STDERR, " world");'], error_output_behavior: Shell\ErrorOutputBehavior::Prepend);

static::assertSame(' worldhello', $result);
}

public function testErrorOutputIsReplacingStandardOutput(): void
{
$result = Shell\execute(PHP_BINARY, ['-r', 'fwrite(STDOUT, "hello"); fwrite(STDERR, " world");'], error_output_behavior: Shell\ErrorOutputBehavior::Replace);

static::assertSame(' world', $result);
}

public function testErrorOutputIsPacked(): void
{
$result = Shell\execute(PHP_BINARY, ['-r', 'fwrite(STDOUT, "hello"); fwrite(STDERR, " world");'], error_output_behavior: Shell\ErrorOutputBehavior::Packed);

[$stdout, $stderr] = Shell\unpack($result);

static::assertSame('hello', $stdout);
static::assertSame(' world', $stderr);
}
}
Loading
0