SSH API
SSH connection, remote execution, and file transfer helpers are available under ptool.ssh and p.ssh.
ptool.ssh.connect
v0.1.0- Introduced.
ptool.ssh.connect(target_or_options) prepares an SSH connection handle backed
by the system ssh command and returns a Connection object.
ssh must be available on PATH.
Arguments:
target_or_options(string|table, required):- When a string is provided, it is treated as an SSH target.
- When a table is provided, it currently supports:
target(string, optional): SSH target string such as"[email protected]"or"[email protected]:2222".host(string, optional): Hostname or IP address.user(string, optional): SSH username. Defaults to$USER, or"root"if$USERis unavailable.port(integer, optional): SSH port. Defaults to22.auth(table, optional): Authentication settings.host_key(table, optional): Host key verification settings.connect_timeout_ms(integer, optional): Timeout in milliseconds. Defaults to10000.keepalive_interval_ms(integer, optional): Keepalive interval in milliseconds.
Supported target string examples:
local a = ptool.ssh.connect("[email protected]")
local b = ptool.ssh.connect("[email protected]:2222")
local c = ptool.ssh.connect("[2001:db8::10]:2222")
auth fields:
private_key_file(string, optional): Path to a private key file.private_key_passphrase(string, optional): Passphrase for the private key. This is currently not supported.password(string, optional): Password-based authentication. This is currently not supported.
Authentication behavior:
- If
auth.private_key_fileis provided,ptoolinvokessshwith that key via-iand also setsIdentitiesOnly=yes. - If
auth.private_key_passphraseorauth.passwordis provided,ptool.ssh.connect(...)fails because this API does not pass those secrets to the systemsshcommand. - Otherwise, authentication is delegated to the local OpenSSH setup, including
settings and mechanisms such as
IdentityFile,ProxyJump,ProxyCommand,ssh-agent, and certificates. - Relative key paths are resolved from the current
ptoolruntime directory, so they followptool.cd(...). ~and~/...are expanded in key paths.
host_key fields:
verify(string, optional): Host key verification mode. Supported values:"known_hosts": Verify against a known_hosts file (default)."ignore": Skip host key verification.
known_hosts_file(string, optional): Path to a known_hosts file. Used only whenverify = "known_hosts".
Host key behavior:
- If
verify = "ignore",ptoolinvokessshwithStrictHostKeyChecking=noandUserKnownHostsFile=/dev/null. - If
verify = "known_hosts"andknown_hosts_fileis provided,ptoolinvokessshwithStrictHostKeyChecking=yesand thatUserKnownHostsFile. - If
verify = "known_hosts"andknown_hosts_fileis omitted, or whenhost_keyis omitted entirely, host key handling is delegated to the local OpenSSH configuration and defaults. - Relative
known_hosts_filepaths are resolved from the currentptoolruntime directory. ~and~/...are expanded inknown_hosts_file.- When
known_hosts_fileis provided explicitly, it overrides the defaultUserKnownHostsFileused by the localsshcommand for this connection.
Example:
local ssh = ptool.ssh.connect({
host = "example.com",
user = "deploy",
port = 22,
auth = {
private_key_file = "~/.ssh/id_ed25519",
},
host_key = {
verify = "known_hosts",
},
})
Connection
v0.1.0- Introduced.
Connection represents an OpenSSH-backed connection handle returned by
ptool.ssh.connect().
It is implemented as a Lua userdata.
Fields and methods:
- Fields:
conn.host(string)conn.user(string)conn.port(integer)conn.target(string)
- Methods:
conn:run(...)->tableconn:run_capture(...)->tableconn:path(path)->RemotePathconn:exists(path)->booleanconn:is_file(path)->booleanconn:is_dir(path)->booleanconn:upload(local_path, remote_path[, options])->tableconn:download(remote_path, local_path[, options])->tableconn:close()->nil
run
v0.1.0- Introduced.
Canonical API name: ptool.ssh.Connection:run.
conn:run(...) executes a remote command through the current SSH connection.
The following call forms are supported:
conn:run("hostname")
conn:run("echo", "hello world")
conn:run("echo", {"hello", "world"})
conn:run("hostname", { stdout = "capture" })
conn:run("echo", {"hello", "world"}, { stdout = "capture" })
conn:run({ cmd = "git", args = {"rev-parse", "HEAD"} })
Argument rules:
conn:run(cmdline):cmdlineis sent as the remote command string.conn:run(cmd, argsline):cmdis treated as the command, andargslineis split using shell-style (shlex) rules.conn:run(cmd, args):cmdis a string andargsis an array of strings. Arguments are shell-quoted before remote execution.conn:run(cmdline, options):optionsoverrides this invocation.conn:run(cmd, args, options):optionsoverrides this invocation.conn:run(options):optionsis a table.- When the second argument is a table: if it is an array (consecutive integer
keys
1..n), it is treated asargs; otherwise it is treated asoptions.
When conn:run(options) is used, options currently supports:
cmd(string, required): The command name or executable path.args(string[], optional): The argument list.cwd(string, optional): Remote working directory. This is applied by prependingcd ... &&to the generated remote shell command.env(table, optional): Remote environment variables, where keys and values are strings. This is applied by prependingexport ... &&to the generated remote shell command.stdin(string, optional): String sent to the remote process stdin.echo(boolean, optional): Whether to echo the remote command before execution. Defaults totrue.check(boolean, optional): Whether to raise an error immediately when the exit status is not0. Defaults tofalse.stdout(string, optional): Stdout handling strategy. Supported values:"inherit": Inherit to the current terminal (default)."capture": Capture intores.stdout."null": Discard the output.
stderr(string, optional): Stderr handling strategy. Supported values:"inherit": Inherit to the current terminal (default)."capture": Capture intores.stderr."null": Discard the output.
When the shortcut forms are used, the options table supports only:
stdin(string, optional): String sent to the remote process stdin.echo(boolean, optional): Whether to echo the remote command before execution. Defaults totrue.check(boolean, optional): Whether to raise an error immediately when the exit status is not0. Defaults tofalse.stdout(string, optional): Stdout handling strategy. Supported values:"inherit": Inherit to the current terminal (default)."capture": Capture intores.stdout."null": Discard the output.
stderr(string, optional): Stderr handling strategy. Supported values:"inherit": Inherit to the current terminal (default)."capture": Capture intores.stderr."null": Discard the output.
Return value rules:
- A table is always returned with the following fields:
ok(boolean): Whether the remote exit status is0.code(integer|nil): The remote exit status. If the remote process exits by signal, this isnil.target(string): The SSH target string in the formuser@host:port.stdout(string, optional): Present whenstdout = "capture".stderr(string, optional): Present whenstderr = "capture".assert_ok(self)(function): Raises an error whenok = false.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local res = ssh:run("uname -a", { stdout = "capture" })
print(res.target)
print(res.stdout)
local res2 = ssh:run({
cmd = "git",
args = {"rev-parse", "HEAD"},
cwd = "/srv/app",
env = {
FOO = "bar",
},
stdout = "capture",
check = true,
})
print(res2.stdout)
run_capture
Unreleased- Introduced.
Canonical API name: ptool.ssh.Connection:run_capture.
conn:run_capture(...) executes a remote command through the current SSH
connection.
It accepts the same call forms, argument rules, return value rules, and options
as conn:run(...).
The difference is only the default stream handling:
stdoutdefaults to"capture".stderrdefaults to"capture".
You can still override either field explicitly in options.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local res = ssh:run_capture("uname -a")
print(res.stdout)
local res2 = ssh:run_capture({
cmd = "sh",
args = {"-c", "printf 'out'; printf 'err' >&2"},
cwd = "/srv/app",
})
print(res2.stdout)
print(res2.stderr)
local res3 = ssh:run_capture("echo hello", {
stderr = "inherit",
})
print(res3.stdout)
path
v0.1.0- Introduced.
Canonical API name: ptool.ssh.Connection:path.
conn:path(path) creates a reusable RemotePath object bound to the current
SSH connection.
path(string, required): The remote path.- Returns: A
RemotePathobject that can be passed toconn:upload(...),conn:download(...), andptool.fs.copy(...).
Example:
local ssh = ptool.ssh.connect("[email protected]")
local remote_release = ssh:path("/srv/app/releases/current.tar.gz")
ssh:download(remote_release, "./tmp/current.tar.gz")
exists
v0.2.0- Introduced.
Canonical API name: ptool.ssh.Connection:exists.
conn:exists(path) checks whether a remote path exists.
path(string|remote path, required): The remote path to check. It can be a string or a value created byconn:path(...).- Returns:
truewhen the remote path exists, otherwisefalse.
Example:
local ssh = ptool.ssh.connect("[email protected]")
print(ssh:exists("/srv/app"))
print(ssh:path("/srv/app/releases/current.tar.gz"):exists())
is_file
v0.2.0- Introduced.
Canonical API name: ptool.ssh.Connection:is_file.
conn:is_file(path) checks whether a remote path exists and is a regular file.
path(string|remote path, required): The remote path to check. It can be a string or a value created byconn:path(...).- Returns:
truewhen the remote path is a file, otherwisefalse.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local remote_tarball = ssh:path("/srv/app/releases/current.tar.gz")
if ssh:is_file(remote_tarball) then
print("release tarball exists")
end
is_dir
v0.2.0- Introduced.
Canonical API name: ptool.ssh.Connection:is_dir.
conn:is_dir(path) checks whether a remote path exists and is a directory.
path(string|remote path, required): The remote path to check. It can be a string or a value created byconn:path(...).- Returns:
truewhen the remote path is a directory, otherwisefalse.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local releases = ssh:path("/srv/app/releases")
if releases:is_dir() then
print("releases directory is ready")
end
upload
v0.1.0- Introduced.
Canonical API name: ptool.ssh.Connection:upload.
conn:upload(local_path, remote_path[, options]) uploads a local file or
directory to the remote host.
local_path(string, required): The local file or directory to upload.remote_path(string|remote path, required): The destination path on the remote host. It can be a string or a value created byconn:path(...).options(table, optional): Transfer options.- Returns: A table with the following fields:
bytes(integer): The number of regular-file bytes uploaded. When a directory is uploaded, this is the sum of the uploaded file sizes.from(string): The local source path.to(string): The remote destination path.
Supported transfer options:
parents(boolean, optional): Create the parent directory ofremote_pathbefore uploading. Defaults tofalse.overwrite(boolean, optional): Whether an existing destination file may be replaced. Defaults totrue.echo(boolean, optional): Whether to print the transfer before executing it. Defaults tofalse.
Directory behavior:
- When
local_pathis a file, the behavior is unchanged. - When
local_pathis a directory andremote_pathdoes not exist,remote_pathbecomes the destination directory root. - When
local_pathis a directory andremote_pathalready exists as a directory, the source directory is created under it using the source directory basename. overwrite = falserejects an already-existing destination directory for the final directory root.- Directory uploads require
tarto be available on the remote host.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local remote_tarball = ssh:path("/srv/app/releases/current.tar.gz")
local res = ssh:upload("./dist/app.tar.gz", remote_tarball, {
parents = true,
overwrite = true,
echo = true,
})
print(res.bytes)
print(res.to)
Directory example:
local ssh = ptool.ssh.connect("[email protected]")
local res = ssh:upload("./dist/assets", "/srv/app/releases", {
parents = true,
overwrite = true,
echo = true,
})
print(res.bytes)
print(res.to) -- [email protected]:22:/srv/app/releases
download
v0.1.0- Introduced.
Canonical API name: ptool.ssh.Connection:download.
conn:download(remote_path, local_path[, options]) downloads a remote file or
directory to a local path.
remote_path(string|remote path, required): The source path on the remote host. It can be a string or a value created byconn:path(...).local_path(string, required): The local destination path.options(table, optional): Transfer options.- Returns: A table with the following fields:
bytes(integer): The number of regular-file bytes downloaded. When a directory is downloaded, this is the sum of the downloaded file sizes.from(string): The remote source path.to(string): The local destination path.
Supported transfer options:
parents(boolean, optional): Create the parent directory oflocal_pathbefore downloading. Defaults tofalse.overwrite(boolean, optional): Whether an existing destination file may be replaced. Defaults totrue.echo(boolean, optional): Whether to print the transfer before executing it. Defaults tofalse.
Directory behavior:
- When
remote_pathis a file, the behavior is unchanged. - When
remote_pathis a directory andlocal_pathdoes not exist,local_pathbecomes the destination directory root. - When
remote_pathis a directory andlocal_pathalready exists as a directory, the remote source directory is created under it using the remote directory basename. overwrite = falserejects an already-existing destination directory for the final directory root.- Directory downloads require
tarto be available on the remote host.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local res = ssh:download("/srv/app/logs/app.log", "./tmp/app.log", {
parents = true,
overwrite = false,
echo = true,
})
print(res.bytes)
print(res.from)
Directory example:
local ssh = ptool.ssh.connect("[email protected]")
local res = ssh:download("/srv/app/releases/assets", "./tmp/releases", {
parents = true,
overwrite = true,
echo = true,
})
print(res.bytes)
print(res.from)
close
v0.1.0- Introduced.
Canonical API name: ptool.ssh.Connection:close.
conn:close() closes the SSH connection handle.
Behavior:
- After closing, the connection can no longer be used.
- Closing an already-closed connection is allowed and has no effect.
Example:
local ssh = ptool.ssh.connect("[email protected]")
ssh:close()
RemotePath
v0.1.0- Introduced.
RemotePath represents a remote path bound to a Connection and returned by
conn:path(path).
It is implemented as a Lua userdata.
Methods:
remote:exists()->booleanremote:is_file()->booleanremote:is_dir()->boolean
exists
remote:exists() checks whether the remote path exists.
- Returns:
truewhen the remote path exists, otherwisefalse.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local remote_release = ssh:path("/srv/app/releases/current.tar.gz")
print(remote_release:exists())
is_file
remote:is_file() checks whether the remote path exists and is a regular file.
- Returns:
truewhen the remote path is a file, otherwisefalse.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local remote_tarball = ssh:path("/srv/app/releases/current.tar.gz")
if remote_tarball:is_file() then
print("release tarball exists")
end
is_dir
remote:is_dir() checks whether the remote path exists and is a directory.
- Returns:
truewhen the remote path is a directory, otherwisefalse.
Example:
local ssh = ptool.ssh.connect("[email protected]")
local releases = ssh:path("/srv/app/releases")
if releases:is_dir() then
print("releases directory is ready")
end