Skip to content

Commit 6b20763

Browse files
author
Zsolt Gál
committed
initial commit
1 parent a8aaecd commit 6b20763

File tree

13 files changed

+487
-5
lines changed

13 files changed

+487
-5
lines changed

.gitignore

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
composer.phar
1+
/bin/
22
/vendor/
3-
4-
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
5-
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
6-
# composer.lock
3+
/.idea/
4+
composer.lock

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,79 @@
11
# shell-exec
22
Assemble commands and execute in shell with PHP easier
3+
4+
# Install
5+
6+
Via composer.
7+
```
8+
composer require technodelight/shell-exec
9+
```
10+
11+
# Usage
12+
13+
```php
14+
<?php
15+
16+
use Technodelight\ShellExec\Command;
17+
use Technodelight\ShellExec\Exec;
18+
19+
$shell = new Exec('which');
20+
$output = $shell->exec(
21+
Command::create()
22+
->withArgument('php')
23+
->withStdErrToStdOut()
24+
->withStdOutTo('/dev/null') // command will be assembled as 'which php 2>'
25+
);
26+
27+
var_dump($output); // will be ["/usr/bin/php"]
28+
```
29+
When an exception happens, an instance of `ShellCommandException` would be thrown.
30+
`ShellCommandException` will still have the command result:
31+
```php
32+
<?php
33+
34+
use Technodelight\ShellExec\Command;
35+
use Technodelight\ShellExec\Exec;
36+
use Technodelight\ShellExec\ShellCommandException;
37+
38+
try {
39+
$shell = new Exec;
40+
$shell->exec(
41+
Command::create('which')
42+
->withArgument('nope') // command will be assembled as 'which nope'
43+
);
44+
} catch(ShellCommandException $e) {
45+
// $e->getCode() will be the shell return code for the executed command.
46+
var_dump($e->getResult());
47+
}
48+
```
49+
There are multiple drivers available
50+
51+
- `Exec`
52+
- `Passthru`
53+
54+
And a special one:
55+
56+
- `TestShell`, which can be used for behavioural testing. For behat please refer to `ShellExec\FixtureContext`.
57+
58+
# License
59+
The MIT License (MIT)
60+
61+
Copyright (c) 2015-2018 Zsolt Gál
62+
63+
Permission is hereby granted, free of charge, to any person obtaining a copy
64+
of this software and associated documentation files (the "Software"), to deal
65+
in the Software without restriction, including without limitation the rights
66+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
67+
copies of the Software, and to permit persons to whom the Software is
68+
furnished to do so, subject to the following conditions:
69+
70+
The above copyright notice and this permission notice shall be included in all
71+
copies or substantial portions of the Software.
72+
73+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
74+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
75+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
76+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
77+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
78+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
79+
SOFTWARE.⏎

behat.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
default:
2+
suites:
3+
default:
4+
contexts:
5+
- ShellExec\FixtureContext

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "technodelight/shell-exec",
3+
"description": "Assemble commands and execute in shell with PHP easier",
4+
"license": "MIT",
5+
"authors": [
6+
{
7+
"name": "Zsolt Gál",
8+
"email": "zenc@zenc.hu"
9+
}
10+
],
11+
"config": {
12+
"bin-dir": "bin/"
13+
},
14+
"autoload": {
15+
"psr-4": {
16+
"Technodelight\\ShellExec\\": "src/"
17+
}
18+
},
19+
"autoload-dev": {
20+
"psr-4": {
21+
"Shell\\": "features/bootstrap/Shell/"
22+
}
23+
},
24+
"require": {},
25+
"require-dev": {
26+
"phpspec/phpspec": "^4.3",
27+
"behat/behat": "^3.4"
28+
}
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace ShellExec;
4+
5+
use Behat\Behat\Context\Context;
6+
use Behat\Gherkin\Node\PyStringNode;
7+
8+
/**
9+
* Setup fixtures in test shell driver
10+
*/
11+
class FixtureContext implements Context
12+
{
13+
/**
14+
* @Given the command :command will return the following output:
15+
*/
16+
public function theCommandWillReturnTheFollowingOutput($command, PyStringNode $output)
17+
{
18+
\Technodelight\ShellExec\TestShell::fixture($command, $output->getRaw());
19+
}
20+
21+
/**
22+
* @AfterScenario
23+
*/
24+
public static function afterScenario()
25+
{
26+
\Technodelight\ShellExec\TestShell::flushFixtures();
27+
}
28+
}

features/it-runs.feature

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Feature: it works!
2+
As a developer
3+
I want easily mockable shell outputs
4+
So I can use Shell\FixtureContext in my own project
5+
6+
Scenario: A command can be executed
7+
Given the command "test" will return the following output:
8+
"""
9+
yay it works!
10+
"""
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace spec\Technodelight\ShellExec;
4+
5+
use PhpSpec\ObjectBehavior;
6+
7+
class CommandSpec extends ObjectBehavior
8+
{
9+
function it_can_be_created_with_argument()
10+
{
11+
$this->withArgument('log');
12+
$this->__toString()->shouldReturn('log');
13+
}
14+
15+
function it_can_be_created_with_option()
16+
{
17+
$this->withOption('v');
18+
$this->__toString()->shouldReturn('-v');
19+
}
20+
21+
function it_can_be_created_with_long_option()
22+
{
23+
$this->withOption('version');
24+
$this->__toString()->shouldReturn('--version');
25+
}
26+
27+
function it_can_squash_options()
28+
{
29+
$this->withOption('v');
30+
$this->withOption('b');
31+
$this->squashOptions();
32+
$this->__toString()->shouldReturn('-vb');
33+
}
34+
35+
function it_can_have_long_option_with_value()
36+
{
37+
$this->withOption('option', 'value');
38+
$this->__toString()->shouldReturn('--option=value');
39+
}
40+
}

src/Command.php

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace Technodelight\ShellExec;
4+
5+
class Command
6+
{
7+
const TYPE_STANDALONE = 1;
8+
const TYPE_OPT = 2;
9+
const TYPE_LONGOPT = 3;
10+
const PREFIX_OPT = '-';
11+
const PREFIX_LONGOPT = '--';
12+
13+
private $exec;
14+
private $args = [];
15+
private $squashOptions = false;
16+
17+
public static function create($exec = null)
18+
{
19+
$command = new self;
20+
$command->exec = $exec;
21+
return $command;
22+
}
23+
24+
public function withExec($exec)
25+
{
26+
$this->exec = $exec;
27+
return $this;
28+
}
29+
30+
public function withArgument($argument)
31+
{
32+
$this->arg(self::TYPE_STANDALONE, $argument);
33+
return $this;
34+
}
35+
36+
public function withOption($option, $value = null)
37+
{
38+
$this->arg(
39+
$this->isLongOpt($option) ? self::TYPE_LONGOPT : self::TYPE_OPT,
40+
$option,
41+
$value
42+
);
43+
44+
return $this;
45+
}
46+
47+
public function withShortOption($option, $value = null)
48+
{
49+
$this->arg(
50+
self::TYPE_OPT,
51+
$option,
52+
$value
53+
);
54+
55+
return $this;
56+
}
57+
58+
public function pipe(Command $command)
59+
{
60+
$this->arg(self::TYPE_STANDALONE, '| ' . $command);
61+
62+
return $this;
63+
}
64+
65+
public function withStdErrTo($destination)
66+
{
67+
$this->arg(self::TYPE_STANDALONE, '2> ' . $destination);
68+
69+
return $this;
70+
}
71+
72+
public function withStdOutTo($destination)
73+
{
74+
$this->arg(self::TYPE_STANDALONE, '> ' . $destination);
75+
76+
return $this;
77+
}
78+
79+
public function withStdErrToStdOut()
80+
{
81+
$this->arg(self::TYPE_STANDALONE, '2>&1');
82+
83+
return $this;
84+
}
85+
86+
public function squashOptions()
87+
{
88+
$this->squashOptions = true;
89+
return $this;
90+
}
91+
92+
public function __toString()
93+
{
94+
$parts = [$this->exec];
95+
$args = $this->args;
96+
if ($this->squashOptions) {
97+
$opts = [];
98+
foreach ($args as $idx => $arg) {
99+
if ($arg['type'] == self::TYPE_OPT) {
100+
$opts[] = $this->render($arg, true);
101+
unset($args[$idx]);
102+
}
103+
}
104+
$parts[] = $this->render(['type' => self::TYPE_OPT, 'name' => join('', $opts), 'value' => null]);
105+
}
106+
107+
foreach ($args as $arg) {
108+
$parts[] = $this->render($arg);
109+
}
110+
111+
return join(' ', array_filter($parts));
112+
}
113+
114+
private function arg($type, $name, $value = null)
115+
{
116+
$this->args[] = [
117+
'type' => $type,
118+
'name' => $name,
119+
'value' => $value,
120+
];
121+
}
122+
123+
private function render(array $arg, $squashOptions = false)
124+
{
125+
if ($arg['type'] == self::TYPE_STANDALONE || ($squashOptions && !$arg['value'])) {
126+
return $arg['name'];
127+
} else {
128+
return sprintf(
129+
'%s%s%s',
130+
$arg['type'] == self::TYPE_LONGOPT ? self::PREFIX_LONGOPT : self::PREFIX_OPT,
131+
$arg['name'],
132+
$arg['value'] ? '=' . $arg['value'] : ''
133+
);
134+
}
135+
}
136+
137+
private function isLongOpt($option)
138+
{
139+
return strlen($option) > 1 ? true : false;
140+
}
141+
}

src/Exec.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Technodelight\ShellExec;
4+
5+
class Exec implements Shell
6+
{
7+
private $executable;
8+
9+
public function __construct($executable = null)
10+
{
11+
$this->executable = $executable;
12+
}
13+
14+
/**
15+
* @param \Technodelight\Jira\Api\Shell\Command $command
16+
* @return array
17+
*/
18+
public function exec(Command $command)
19+
{
20+
if ($this->executable) {
21+
$command->withExec($this->executable);
22+
}
23+
exec((string) $command, $result, $returnVar);
24+
$result = (array) array_filter(array_map('trim', (array) $result));
25+
if (!empty($returnVar)) {
26+
throw ShellCommandException::fromDetails($command, $returnVar, $result);
27+
}
28+
return $result;
29+
}
30+
}

0 commit comments

Comments
 (0)