Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,16 @@ Whenever there is output that closure will get called with two parameters:
- `type`: this can be `Symfony\Component\Process\Process::OUT` for regular output and `Symfony\Component\Process\Process::ERR` for error output
- `line`: the output itself

### Windows Target
### Windows Target

If your target is a Windows machine, you can use the `removeBash` method to remove the bash command from the command line.
If your target is a Windows machine, use the `onWindows` method.

```php
Ssh::create('user', 'host')->removeBash();
Ssh::create('user', 'host')->onWindows()->execute('dir');
```

By default this package pipes your commands to `bash` on the remote host. On Windows the remote shell is `cmd.exe`, which handles input differently and always reports a successful exit code, even when a command fails. The `onWindows` method passes your commands as an argument to `ssh` instead, so `cmd.exe` runs them through `cmd.exe /c` and their real exit code (and thus `isSuccessful()`) is preserved.

## Testing

``` bash
Expand Down
59 changes: 53 additions & 6 deletions src/Ssh.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class Ssh

protected bool $addBash;

protected bool $onWindows;

protected Closure $processConfigurationClosure;

protected Closure $onOutput;
Expand All @@ -38,6 +40,8 @@ public function __construct(?string $user, string $host, ?int $port = null, ?str

$this->addBash = true;

$this->onWindows = false;

$this->processConfigurationClosure = fn (Process $process) => null;

$this->onOutput = fn ($type, $line) => null;
Expand Down Expand Up @@ -163,6 +167,13 @@ public function removeBash(): self
return $this;
}

public function onWindows(): self
{
$this->onWindows = true;

return $this;
}

protected function getPasswordCommand(): string
{
if ($this->password !== null) {
Expand All @@ -181,26 +192,62 @@ public function getExecuteCommand($command): string
{
$commands = $this->wrapArray($command);

if ($this->onWindows) {
return $this->getWindowsExecuteCommand($commands);
}

return $this->getBashExecuteCommand($commands);
}

/**
* @param array<int, string> $commands
*/
protected function getBashExecuteCommand(array $commands): string
{
$commandString = implode(PHP_EOL, $commands);

if (in_array($this->host, ['local', 'localhost', '127.0.0.1'])) {
if ($this->isLocalHost()) {
return $commandString;
}

$passwordCommand = $this->getPasswordCommand();
$extraOptions = implode(' ', $this->getExtraOptions());

$target = $this->getTargetForSsh();

$delimiter = 'EOF-SPATIE-SSH';

$bash = $this->addBash ? "'bash -se'" : '';
$delimiter = 'EOF-SPATIE-SSH';

return "{$passwordCommand}ssh {$extraOptions} {$target} {$bash} << \\$delimiter".PHP_EOL
return "{$passwordCommand}ssh {$extraOptions} {$target} {$bash} << \\{$delimiter}".PHP_EOL
.$commandString.PHP_EOL
.$delimiter;
}

/**
* On Windows the remote shell is cmd.exe, which cannot read commands from stdin the way
* `bash -se` does. Passing the commands as an ssh argument runs them through `cmd.exe /c`,
* which returns the real exit code instead of always reporting success.
*
* @param array<int, string> $commands
*/
protected function getWindowsExecuteCommand(array $commands): string
{
$commandString = implode(' && ', $commands);

if ($this->isLocalHost()) {
return $commandString;
}

$passwordCommand = $this->getPasswordCommand();
$extraOptions = implode(' ', $this->getExtraOptions());
$target = $this->getTargetForSsh();

return "{$passwordCommand}ssh {$extraOptions} {$target} \"{$commandString}\"";
}

protected function isLocalHost(): bool
{
return in_array($this->host, ['local', 'localhost', '127.0.0.1']);
}

/**
* @param string|array $command
*
Expand Down
12 changes: 12 additions & 0 deletions tests/SshTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@
assertMatchesSnapshot($command);
});

it('can run a single command on windows', function () {
$command = $this->ssh->onWindows()->getExecuteCommand('whoami');

assertMatchesSnapshot($command);
});

it('can run multiple commands on windows', function () {
$command = $this->ssh->onWindows()->getExecuteCommand(['whoami', 'cd C:\\Windows']);

assertMatchesSnapshot($command);
});

it('does not alter ssh command when setting timeout', function () {
$command = $this->ssh->setTimeout(10)->getExecuteCommand('whoami');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh user@example.com "whoami"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh user@example.com "whoami && cd C:\Windows"
Loading