Run a command
SshClient.RunCommandAsync runs a command on the server, waits for it to finish, and returns its standard output, standard error and
exit status. It is the SSH equivalent of Process.Start with redirected streams, for commands whose output fits in memory.
When to use
- The command finishes on its own and prints a bounded amount of output:
systemctl status,df,git rev-parse, a script. - You need the exit status to decide what happens next.
For output that is large, endless or needed while the command runs, use StartCommandAsync instead. For a
program that asks questions (a password prompt, a device CLI), see Interactive shells.
Read stdout, stderr and the exit status
var result = await client.RunCommandAsync("df -h /", cancellationToken);
Console.WriteLine($"exit status: {result.ExitStatus}"); // null when the command was killed by a signal
Console.WriteLine(result.StandardOutputText); // stdout, decoded as UTF-8
Console.Error.WriteLine(result.StandardErrorText); // stderr, kept apart from stdout
stdout and stderr arrive separately, as they do from ssh host command. StandardOutputText and StandardErrorText decode them
as UTF-8; StandardOutput and StandardError are the raw bytes:
var result = await client.RunCommandAsync("cat /etc/machine-id | sha256sum", cancellationToken);
ReadOnlyMemory<byte> raw = result.StandardOutput; // the bytes, for output that is not UTF-8 text
var text = Encoding.Latin1.GetString(raw.Span);
Act on the result
A command that fails on the server is a result, not an exception: check ExitStatus. When the program was killed by a signal
there is no exit status, and ExitSignal says which signal:
var result = await client.RunCommandAsync("systemctl is-active nginx", cancellationToken);
switch (result)
{
case { ExitStatus: 0 }:
Console.WriteLine("nginx is running");
break;
case { ExitSignal: { } signal }:
Console.WriteLine($"killed by SIG{signal.Name}{(signal.CoreDumped ? " (core dumped)" : "")}");
break;
default:
Console.WriteLine($"exit {result.ExitStatus}: {result.StandardOutputText.Trim()}");
break;
}
Pass arguments safely
The command text is a shell command line, parsed by the user's login shell on the server, exactly as with ssh host 'command'.
Quote every value you did not write yourself:
/// <summary>Quotes one argument for a POSIX shell (sh, bash): single quotes, with each ' written as '\''.</summary>
public static string Quote(string value) => "'" + value.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
// The command line is parsed by the server's shell: quote anything you did not write yourself.
var result = await client.RunCommandAsync($"ls -l -- {Quote(userSuppliedPath)}", cancellationToken);
Warning
Never build a command line by concatenating unquoted input: a file name such as x; rm -rf ~ runs as two commands. The quoting
above is for POSIX shells (sh, bash, zsh). A Windows server whose default shell is cmd.exe or PowerShell parses
differently.
Run several commands at once
One connection carries many channels, so independent commands can run concurrently. Each RunCommandAsync is its own session on
the server:
// One connection, three channels at once: each command gets its own session.
var commands = new[] { "uptime", "free -m", "df -h /" };
var results = await Task.WhenAll(commands.Select(c => client.RunCommandAsync(c, cancellationToken)));
foreach (var (command, result) in commands.Zip(results))
{
Console.WriteLine($"$ {command}\n{result.StandardOutputText}");
}
The server limits how many sessions one connection may have open (sshd's MaxSessions, 10 by default). Commands are independent:
a cd in one does not affect the next. Chain dependent steps in one command line (cd /srv/app && ./deploy.sh).
Notes and pitfalls
- Output limit. Output is collected in memory up to
SshClientOptions.MaxCommandOutputBytes(128 MiB). Beyond that,RunCommandAsyncthrowsInvalidOperationException: stream such output withStartCommandAsync. - No terminal. Commands run without a pseudo-terminal, so they do not prompt, colorize or page. Programs that insist on a
terminal (
sudowithoutNOPASSWD,top) needStartCommandAsyncwith a terminal. - Environment. The server runs the command through the user's shell non-interactively (as
shell -c command), so startup files that only interactive shells read do not run, andPATHmay differ from what you see in a terminal. Use full paths in scripts. - Cancellation. Cancelling the token closes the command's channel; the connection stays usable. See Cancellation and timeouts for stopping the program on the server too.
- Errors.
SshExceptionmeans the command could not be run at all (the server refused it, or the connection failed), not that the command failed.
See also
- Stream large output
- Commands and shells (in-depth guide)
- RunCommandAsync and SshCommandResult in the API reference