A developer-friendly wrapper around execution of shell commands.

Overview

ptlis/shell-command

A developer-friendly wrapper around execution of shell commands.

There were several goals that inspired the creation of this package:

  • Use the command pattern to encapsulate the data required to execute a shell command, allowing the command to be passed around and executed later.
  • Maintain a stateless object graph allowing (for example) the spawning of multiple running processes from a single command.
  • Provide clean APIs for synchronous and asynchronous usage.
  • Running processes can be wrapped in promises to allow for easy composition.

Build Status Code Coverage Scrutinizer Quality Score Latest Stable Version

Install

From the terminal:

    $ composer require ptlis/shell-command

Usage

The Builder

The package ships with a command builder, providing a simple and safe method to build commands.

    use ptlis\ShellCommand\CommandBuilder;
    
    $builder = new CommandBuilder();

The builder will attempt to determine your environment when constructed, you can override this by specifying an environment as the first argument:

    use ptlis\ShellCommand\CommandBuilder;
    use ptlis\ShellCommand\UnixEnvironment;
    
    $builder = new CommandBuilder(new UnixEnvironment());

Note: this builder is immutable - method calls must be chained and terminated with a call to buildCommand like so:

    $command = $builder
        ->setCommand('foo')
        ->addArgument('--bar=baz')
        ->buildCommand()

Set Command

First we must provide the command to execute:

    $builder->setCommand('git')             // Executable in $PATH
        
    $builder->setCommand('./local/bin/git') // Relative to current working directory
        
    $builder->setCommand('/usr/bin/git')    // Fully qualified path
    
    $build->setCommand('~/.script.sh')      // Path relative to $HOME

If the command is not locatable a RuntimeException is thrown.

Set Process Timeout

The timeout (in microseconds) sets how long the library will wait on a process before termination. Defaults to -1 which never forces termination.

    $builder
        ->setTimeout(30 * 1000 * 1000)          // Wait 30 seconds

If the process execution time exceeds this value a SIGTERM will be sent; if the process then doesn't terminate after a further 1 second wait then a SIGKILL is sent.

Set Poll Timeout

Set how long to wait (in microseconds) between polling the status of processes. Defaults to 1,000,000 (1 second).

    $builder
        ->setPollTimeout(30 * 1000 * 1000)          // Wait 30 seconds

Set Working Directory

You can set the working directory for a command:

    $builder
        ->setCwd('/path/to/working/directory/')

Add Arguments

Add arguments to invoke the command with (all arguments are escaped):

    $builder
        ->addArgument('--foo=bar')

Conditionally add, depending on the result of an expression:

    $builder
        ->addArgument('--foo=bar', $myVar === 5)

Add several arguments:

    $builder
        ->addArguments([
            '--foo=bar',
            '-xzcf',
            'if=/dev/sda of=/dev/sdb'
        ])

Conditionally add, depending on the result of an expression:

    $builder
        ->addArguments([
            '--foo=bar',
            '-xzcf',
            'if=/dev/sda of=/dev/sdb'
        ], $myVar === 5)

Note: Escaped and raw arguments are added to the command in the order they're added to the builder. This accommodates commands that are sensitive to the order of arguments.

Add Raw Arguments

WARNING: Do not pass user-provided data to these methods! Malicious users could easily execute arbitrary shell commands.

Arguments can also be applied without escaping:

    $builder
        ->addRawArgument("--foo='bar'")

Conditionally, depending on the result of an expression:

    $builder
        ->addRawArgument('--foo=bar', $myVar === 5)

Add several raw arguments:

    $builder
        ->addRawArguments([
            "--foo='bar'",
            '-xzcf',
        ])

Conditionally, depending on the result of an expression:

    $builder
        ->addRawArguments([
            '--foo=bar',
            '-xzcf',
            'if=/dev/sda of=/dev/sdb'
        ], $myVar === 5)

Note: Escaped and raw arguments are added to the command in the order they're added to the builder. This accommodates commands that are sensitive to the order of arguments.

Add Environment Variables

Environment variables can be set when running a command:

    $builder
        ->addEnvironmentVariable('TEST_VARIABLE', '123')

Conditionally, depending on the result of an expression:

    $builder
        ->addEnvironmentVariable('TEST_VARIABLE', '123', $myVar === 5)

Add several environment variables:

    $builder
        ->addEnvironmentVariables([
            'TEST_VARIABLE' => '123',
            'FOO' => 'bar'
        ])

Conditionally, depending on the result of an expression:

    $builder
        ->addEnvironmentVariables([
            'TEST_VARIABLE' => '123',
            'FOO' => 'bar'
        ], $foo === 5)

Add Process Observers

Observers can be attached to spawned processes. In this case we add a simple logger:

    $builder
        ->addProcessObserver(
            new AllLogger(
                new DiskLogger(),
                LogLevel::DEBUG
            )
        )

Build the Command

One the builder has been configured, the command can be retrieved for execution:

    $command = $builder
        // ...
        ->buildCommand();

Synchronous Execution

To run a command synchronously use the runSynchronous method. This returns an object implementing CommandResultInterface, encoding the result of the command.

    $result = $command
        ->runSynchronous(); 

When you need to re-run the same command multiple times you can simply invoke runSynchronous repeatedly; each call will run the command returning the result to your application.

The exit code & output of the command are available as methods on this object:

    $result->getExitCode();         // 0 for success, anything else conventionally indicates an error
    $result->getStdOut();           // The contents of stdout (as a string)
    $result->getStdOutLines();      // The contents of stdout (as an array of lines)
    $result->getStdErr();           // The contents of stderr (as a string)
    $result->getStdErrLines();      // The contents of stderr (as an array of lines)
    $result->getExecutedCommand();  // Get the executed command as a string, including environment variables
    $result->getWorkingDirectory(); // Get the directory the command was executed in 

Asynchronous Execution

Commands can also be executed asynchronously, allowing your program to continue executing while waiting for the result.

Command::runAsynchronous

The runAsynchronous method returns an object implementing the ProcessInterface which provides methods to monitor the state of a process.

    $process = $command->runAsynchronous();

As with the synchronouse API, when you need to re-run the same command multiple times you can simply invoke runAsynchronous repeatedly; each call will run the command returning the object representing the process to your application.

Process API

ProcessInterface provides the methods required to monitor and manipulate the state and lifecycle of a process.

Check whether the process has completed:

    if (!$process->isRunning()) {
        echo 'done' . PHP_EOL;
    }

Force the process to stop:

    $process->stop();

Wait for the process to stop (this blocks execution of your script, effectively making this synchronous):

    $process->wait();

Get the process id (throws a \RuntimeException if the process has ended):

    $process->getPid();

Read output from a stream:

    $stdOut = $process->readStream(ProcessInterface::STDOUT);

Provide input (e.g. via STDIN):

    $process->writeInput('Data to pass to the running process via STDIN');

Get the exit code (throws a \RuntimeException if the process is still running):

    $exitCode = $process->getExitCode();

Send a signal (SIGTERM or SIGKILL) to the process:

    $process->sendSignal(ProcessInterface::SIGTERM);

Get the string representation of the running command:

    $commandString = $process->getCommand();

Process::getPromise

Monitoring of shell command execution can be wrapped in a ReactPHP Promise. This gives us a flexible execution model, allowing chaining (with Promise::then) and aggregation using Promise::all, Promise::some, Promise::race and their friends.

Building promise to execute a command can be done by calling the getPromise method from a Process instance. This returns an instance of \React\Promise\Promise:

    $eventLoop = \React\EventLoop\Factory::create();

    $promise = $command->runAsynchonous()->getPromise($eventLoop);

The ReactPHP EventLoop component is used to periodically poll the running process to see if it has terminated yet; once it has the promise is either resolved or rejected depending on the exit code of the executed command.

The effect of this implementation is that once you've created your promises, chains and aggregates you must invoke EventLoop::run:

    $eventLoop->run();

This will block further execution until the promises are resolved/rejected.

Mocking

Mock implementations of the Command & Builder interfaces are provided to aid testing.

By type hinting against the interfaces, rather than the concrete implementations, these mocks can be injected & used to return pre-configured result objects.

Contributing

You can contribute by submitting an Issue to the issue tracker, improving the documentation or submitting a pull request. For pull requests i'd prefer that the code style and test coverage is maintained, but I am happy to work through any minor issues that may arise so that the request can be merged.

Known limitations

  • Currently supports UNIX environments only.
Comments
  • Add writing to stdin

    Add writing to stdin

    I needed some logic to write to input streams, so I added it and propose it to the upstream repo.

    I'm participating in the hacktoberfest challenge, so I would be really grateful, if you could add the label hacktoberfest-accepted to this PR, so this PR is counted to me completing the challenge.

    However, if you need changes to the PR, just request them. :)

    Cheers DBX12

    hacktoberfest-accepted 
    opened by DBX12 2
  • validateCommand() fails if CWD is set and process is ran in a different directory

    validateCommand() fails if CWD is set and process is ran in a different directory

    Value set using setCwd() is not used in validateCommand(), so validation fails if relative command is not run in the same working directory.

    How to test:

    <?php
    require __DIR__ . '/vendor/autoload.php';
    $builder = (new \ptlis\ShellCommand\CommandBuilder())
            ->setCwd('/')
            ->setCommand('./bin/ls');
    echo $builder->buildCommand();
    

    This will succeed if called in /, but fail if script is called from any other directory:

    PHP Fatal error:  Uncaught RuntimeException: Invalid command "./bin/ls" provided to ptlis\ShellCommand\CommandBuilder. in /app/vendor/ptlis/shell-command/src/CommandBuilder.php:180
    

    Bug is in line 179 of CommandBuilder->buildCommand():

    https://github.com/ptlis/shell-command/blob/5835e2dfecf5cfab6c2c040bf5d921967e2ddf9d/src/CommandBuilder.php#L177-L181

    Hand-made patch:

    -         if (!$this->environment->validateCommand($this->command) { 
    +         if (!$this->environment->validateCommand($this->command, $this->cwd)) { 
    
    opened by flaviovs 2
  • Feature Idea: Set working directory of command

    Feature Idea: Set working directory of command

    Thanks for the package 👏

    I have a feature idea: It would be nice to have an option to change the working directory of a command.

    This way an app could stay in the current working directory, but execute a binary from within another directory. This is often useful when running multiple tools which require different (relative oder absolute) entry points themself.

    opened by pixelbrackets 2
  • Composer v2.0 readiness

    Composer v2.0 readiness

    Composer raises deprecation errors when installing the package. The main concern is that it states that some classes will no longer be autoloader when composer v2.0 hits.

    Not critical, however, as they only look like test classes.

    Deprecation Notice: Class ptlis\ShellCommand\Test\Mocks\MockProcessTest located in ./vendor/ptlis/shell-command/tests/Unit/Mocks/MockProcessTest.php does not comply with psr-4 autoloading standard. It will not autoload anymore in Composer v2.0. in phar:///usr/local/Cellar/composer/1.10.8/bin/composer/src/Composer/Autoload/ClassMapGenerator.php:201 Deprecation Notice: Class ptlis\ShellCommand\Test\Mocks\MockCommandTest located in ./vendor/ptlis/shell-command/tests/Unit/Mocks/MockCommandTest.php does not comply with psr-4 autoloading standard. It will not autoload anymore in Composer v2.0. in phar:///usr/local/Cellar/composer/1.10.8/bin/composer/src/Composer/Autoload/ClassMapGenerator.php:201 Deprecation Notice: Class ptlis\ShellCommand\Test\Mocks\MockCommandBuilderTest located in ./vendor/ptlis/shell-command/tests/Unit/Mocks/MockCommandBuilderTest.php does not comply with psr-4 autoloading standard. It will not autoload anymore in Composer v2.0. in phar:///usr/local/Cellar/composer/1.10.8/bin/composer/src/Composer/Autoload/ClassMapGenerator.php:201 Deprecation Notice: Class ptlis\ShellCommand\Test\ProcessOutputTest located in ./vendor/ptlis/shell-command/tests/Unit/ProcessOutputTest.php does not comply with psr-4 autoloading standard. It will not autoload anymore in Composer v2.0. in phar:///usr/local/Cellar/composer/1.10.8/bin/composer/src/Composer/Autoload/ClassMapGenerator.php:201

    opened by codesmithtech 1
  • Composer psr-4 Warning

    Composer psr-4 Warning

    Class ptlis\ShellCommand\Test\Mocks\MockCommandBuilderTest located in vendor/ptlis/shell-command/tests\Unit\Mocks\MockCommandBuilderTest.php does not comply with psr-4 autoloading standard. Skipping.

    localhost > MAMP > PHP 8.1.0 + Composer 2.2.3

    opened by Parviz-Elite 0
Releases(1.3.0)
  • 1.3.0(Mar 13, 2021)

  • 1.2.0(Nov 5, 2020)

    Add properties to retrieve the executed command and working directory to the ProcessOutputInterface. These are here to make debugging failing commands in systems that execute commands concurrently.

    Source code(tar.gz)
    Source code(zip)
Owner
brian ridley
brian ridley
A simple object oriented interface to execute shell commands in PHP

php-shellcommand php-shellcommand provides a simple object oriented interface to execute shell commands. Installing Prerequisites Your php version mus

Michael Härtl 283 Dec 10, 2022
ReactPHP Shell, based on the Symfony Console component.

Pecan Event-driven, non-blocking shell for ReactPHP. Pecan (/pɪˈkɑːn/) provides a non-blocking alternative to the shell provided in the Symfony Consol

Michael Crumm 43 Sep 4, 2022
YAPS - Yet Another PHP Shell

YAPS - Yet Another PHP Shell Yeah, I know, I know... But that's it. =) As the name reveals, this is yet another PHP reverse shell, one more among hund

Nicholas Ferreira 60 Dec 14, 2022
An Interactive Shell to Lumen Framework.

ABANDONED Please consider to use the official Laravel Tinker, it is also compatible with Lumen: laravel/tinker Lumen Artisan Tinker An Interactive She

Vagner Luz do Carmo 112 Aug 17, 2022
I gues i tried to make a shell that's looks like a terminal in single php file

php-shell-gui Terms of service This tool can only be used for legal purposes. You take full responsibility for any actions performed using this. The o

Squar3 4 Aug 23, 2022
Web Shell Detector – is a php script that helps you find and identify php/cgi(perl)/asp/aspx shells.

Web Shell Detector – is a php script that helps you find and identify php/cgi(perl)/asp/aspx shells. Web Shell Detector has a “web shells” signature database that helps to identify “web shell” up to 99%. By using the latest javascript and css technologies, web shell detector has a light weight and friendly interface.

Maxim 763 Dec 27, 2022
[ABANDONED] PHP library for executing commands on multiple remote machines, via SSH

#Shunt Inspired by Ruby's Capistrano, Shunt is PHP library for executing commands on multiple remote machines, via SSH. Specifically, this library was

The League of Extraordinary Packages 436 Feb 20, 2022
Library for creating CLI commands or applications

Console Motivation: this library purpose is to provide a lighter and more robust API for console commands and/or applications to symfony/console. It c

Théo FIDRY 16 Dec 28, 2022
Handle signals in artisan commands

Using this package you can easily handle signals like SIGINT, SIGTERM in your Laravel app.

Spatie 96 Dec 29, 2022
A package built for lumen that ports most of the make commands from laravel.

A package built for lumen that ports most of the make commands from laravel. For lumen v5.1, but will most likely work for 5.2 as well. I haven't tested. If you have requests, let me know, or do it yourself and make a pull request

Michael Bonds 22 Mar 8, 2022
Simple but yet powerful library for running almost all artisan commands.

:artisan gui Simple but yet powerful library for running some artisan commands. Requirements Laravel 8.* php ^7.3 Installation Just install package: c

null 324 Dec 28, 2022
Supercharge your Symfony console commands!

zenstruck/console-extra A modular set of features to reduce configuration boilerplate for your commands: /** * Creates a user in the database. * *

Kevin Bond 29 Nov 19, 2022
PHP library for executing commands on multiple remote machines, via SSH

#Shunt Inspired by Ruby's Capistrano, Shunt is PHP library for executing commands on multiple remote machines, via SSH. Specifically, this library was

The League of Extraordinary Packages 436 Feb 20, 2022
🤖 GitHub Action to run symfony console commands.

Symfony Console GitHub Action Usage You can use it as a Github Action like this: # .github/workflows/lint.yml name: "Lint" on: pull_request: push

Nucleos 3 Oct 20, 2022
Helper commands for Laravel Beyond Crud

About beyond-crud-helpers This package has many helper commands for the Laravel BEyond CRUD project by Spatie Installation composer require --dev tarr

null 4 Mar 8, 2022
A PocketMine-MP plugin which allows the users to edit no permission message of commands

CommandPermissionMessage A PocketMine-MP plugin which allows the users to edit no permission message of commands Have you ever got bored by the red me

cosmicnebula200 3 May 29, 2022
A Cli tool to save you time, and gives you the power to scaffold all of your models,controllers,commands

A Cli tool to save you time, and gives you the power to scaffold all of your models,controllers,commands... at once Installation You can install the p

Coderflex 16 Nov 11, 2022
Hentai Bash - This is the core of Hentai Terminal, responsible for the basic functions and commands

Hentai Bash - This is the core of Hentai Terminal, responsible for the basic functions and commands. It is mainly used for writing and executing commands.

Hentai Group 1 Jan 26, 2022
Lovely PHP wrapper for using the command-line

ShellWrap What is it? It's a beautiful way to use powerful Linux/Unix tools in PHP. Easily and logically pipe commands together, capture errors as PHP

James Hall 745 Dec 30, 2022