--- /dev/null
+<?php
+
+namespace Robo\Task\Remote;
+
+use Robo\Contract\CommandInterface;
+use Robo\Exception\TaskException;
+use Robo\Task\BaseTask;
+use Robo\Contract\SimulatedInterface;
+
+/**
+ * Runs multiple commands on a remote server.
+ * Per default, commands are combined with &&, unless stopOnFail is false.
+ *
+ * ```php
+ * <?php
+ *
+ * $this->taskSshExec('remote.example.com', 'user')
+ * ->remoteDir('/var/www/html')
+ * ->exec('ls -la')
+ * ->exec('chmod g+x logs')
+ * ->run();
+ *
+ * ```
+ *
+ * You can even exec other tasks (which implement CommandInterface):
+ *
+ * ```php
+ * $gitTask = $this->taskGitStack()
+ * ->checkout('master')
+ * ->pull();
+ *
+ * $this->taskSshExec('remote.example.com')
+ * ->remoteDir('/var/www/html/site')
+ * ->exec($gitTask)
+ * ->run();
+ * ```
+ *
+ * You can configure the remote directory for all future calls:
+ *
+ * ```php
+ * \Robo\Task\Remote\Ssh::configure('remoteDir', '/some-dir');
+ * ```
+ */
+class Ssh extends BaseTask implements CommandInterface, SimulatedInterface
+{
+ use \Robo\Common\CommandReceiver;
+ use \Robo\Common\ExecOneCommand;
+
+ /**
+ * @var null|string
+ */
+ protected $hostname;
+
+ /**
+ * @var null|string
+ */
+ protected $user;
+
+ /**
+ * @var bool
+ */
+ protected $stopOnFail = true;
+
+ /**
+ * @var array
+ */
+ protected $exec = [];
+
+ /**
+ * Changes to the given directory before running commands.
+ *
+ * @var string
+ */
+ protected $remoteDir;
+
+ /**
+ * @param null|string $hostname
+ * @param null|string $user
+ */
+ public function __construct($hostname = null, $user = null)
+ {
+ $this->hostname = $hostname;
+ $this->user = $user;
+ }
+
+ /**
+ * @param string $hostname
+ *
+ * @return $this
+ */
+ public function hostname($hostname)
+ {
+ $this->hostname = $hostname;
+ return $this;
+ }
+
+ /**
+ * @param string $user
+ *
+ * @return $this
+ */
+ public function user($user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Whether or not to chain commands together with && and stop the chain if one command fails.
+ *
+ * @param bool $stopOnFail
+ *
+ * @return $this
+ */
+ public function stopOnFail($stopOnFail = true)
+ {
+ $this->stopOnFail = $stopOnFail;
+ return $this;
+ }
+
+ /**
+ * Changes to the given directory before running commands.
+ *
+ * @param string $remoteDir
+ *
+ * @return $this
+ */
+ public function remoteDir($remoteDir)
+ {
+ $this->remoteDir = $remoteDir;
+ return $this;
+ }
+
+ /**
+ * @param string $filename
+ *
+ * @return $this
+ */
+ public function identityFile($filename)
+ {
+ $this->option('-i', $filename);
+
+ return $this;
+ }
+
+ /**
+ * @param int $port
+ *
+ * @return $this
+ */
+ public function port($port)
+ {
+ $this->option('-p', $port);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function forcePseudoTty()
+ {
+ $this->option('-t');
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function quiet()
+ {
+ $this->option('-q');
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function verbose()
+ {
+ $this->option('-v');
+
+ return $this;
+ }
+
+ /**
+ * @param string|string[]|CommandInterface $command
+ *
+ * @return $this
+ */
+ public function exec($command)
+ {
+ if (is_array($command)) {
+ $command = implode(' ', array_filter($command));
+ }
+
+ $this->exec[] = $command;
+
+ return $this;
+ }
+
+ /**
+ * Returns command that can be executed.
+ * This method is used to pass generated command from one task to another.
+ *
+ * @return string
+ */
+ public function getCommand()
+ {
+ $commands = [];
+ foreach ($this->exec as $command) {
+ $commands[] = $this->receiveCommand($command);
+ }
+
+ $remoteDir = $this->remoteDir ? $this->remoteDir : $this->getConfigValue('remoteDir');
+ if (!empty($remoteDir)) {
+ array_unshift($commands, sprintf('cd "%s"', $remoteDir));
+ }
+ $command = implode($this->stopOnFail ? ' && ' : ' ; ', $commands);
+
+ return $this->sshCommand($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run()
+ {
+ $this->validateParameters();
+ $command = $this->getCommand();
+ return $this->executeCommand($command);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function simulate($context)
+ {
+ $command = $this->getCommand();
+ $this->printTaskInfo("Running {command}", ['command' => $command] + $context);
+ }
+
+ protected function validateParameters()
+ {
+ if (empty($this->hostname)) {
+ throw new TaskException($this, 'Please set a hostname');
+ }
+ if (empty($this->exec)) {
+ throw new TaskException($this, 'Please add at least one command');
+ }
+ }
+
+ /**
+ * Returns an ssh command string running $command on the remote.
+ *
+ * @param string|CommandInterface $command
+ *
+ * @return string
+ */
+ protected function sshCommand($command)
+ {
+ $command = $this->receiveCommand($command);
+ $sshOptions = $this->arguments;
+ $hostSpec = $this->hostname;
+ if ($this->user) {
+ $hostSpec = $this->user . '@' . $hostSpec;
+ }
+
+ return "ssh{$sshOptions} {$hostSpec} '{$command}'";
+ }
+}