Skip to main content
Version: Unreleased

Core Lua API

ptool exposes these core runtime helpers directly under ptool and p.

ptool run <lua_file> runs a Lua script and injects the global variable ptool (or its alias p; for example, p.run is equivalent to ptool.run). For files ending in .lua, ptool <lua_file> is a CLI shortcut with the same behavior.

The embedded Lua runtime keeps the base Lua globals and exposes only these standard libraries by default:

  • table
  • string
  • math
  • utf8

Host-facing built-in modules such as io, os, and package are intentionally not available. Use ptool APIs such as ptool.fs, ptool.os, ptool.path, and ptool.run for filesystem, environment, process, network, and other runtime operations instead.

If you want to pass arguments to a Lua script, you can do it like this:

ptool run script.lua --name alice -v a.txt b.txt
ptool script.lua --name alice -v a.txt b.txt

The arguments can then be parsed with ptool.args.parse(...).

Here is an example script:

ptool.use("v0.1.0")

ptool.run("echo", {"hello", "world"})

Shebang is supported, so you can add this to the top of the file:

#!/usr/bin/env ptool

ptool.use

v0.1.0 - Introduced.

ptool.use declares the minimum required ptool version for a script.

ptool.use("v0.1.0")
  • The argument is a semantic version string (SemVer) and supports an optional v prefix, such as v0.1.0 or 0.1.0.
  • If the required version is higher than the current ptool version, the script exits immediately with an error saying that the current ptool version is too old.

ptool.unindent

v0.1.0 - Introduced.

ptool.unindent processes multi-line strings by removing the | prefix after leading indentation on each line and trimming leading and trailing blank lines.

local str = ptool.unindent([[
| line 1
| line 2
]])

This is equivalent to:

local str = [[line 1
line 2]]

ptool.inspect

v0.1.0 - Introduced.

ptool.inspect(value[, options]) renders a Lua value as a readable Lua-style string. It is primarily intended for debugging and displaying table contents.

  • value (any, required): The Lua value to inspect.
  • options (table, optional): Rendering options. Supported fields:
    • indent (string, optional): Indentation used for each nesting level. Defaults to two spaces.
    • multiline (boolean, optional): Whether tables are rendered across multiple lines. Defaults to true.
    • max_depth (integer, optional): Maximum nesting depth to render. Deeper values are replaced with <max-depth>.
  • Returns: string.

Behavior:

  • Array-like entries (1..n) are rendered first.
  • Remaining table fields are rendered after the array part in stable key order.
  • Identifier-like string keys are rendered as key = value; other keys are rendered as [key] = value.
  • Recursive table references are rendered as <cycle>.
  • Functions, threads, and userdata are rendered as placeholder values such as <function> and <userdata>.

Example:

local value = {
"hello",
user = { name = "alice", tags = {"dev", "ops"} },
}
value.self = value

print(ptool.inspect(value))
print(ptool.inspect(value, { multiline = false }))

ptool.ask

v0.1.0 - Introduced. v0.5.0 - Added prompt validation options and prompt subcommands.

ptool.ask provides interactive prompts. You can call it directly for text input, or use its prompt subcommands for confirmation, selection, multi-select, and secret input.

Common behavior:

  • All ptool.ask prompts require an interactive TTY. Running them in a non-interactive environment raises an error.
  • If the user cancels a prompt, the script raises an error.
  • Unknown option names or invalid option value types raise an error.

ptool.ask

ptool.ask(prompt[, options]) asks the user for a line of text and returns the answer.

  • prompt (string, required): The prompt shown to the user.
  • options (table, optional): Prompt options. Supported fields:
    • default (string, optional): Default value used when the user submits an empty answer.
    • help (string, optional): Extra help text shown below the prompt.
    • placeholder (string, optional): Placeholder text shown before the user starts typing.
    • required (boolean, optional): Whether the answer must be non-empty.
    • allow_empty (boolean, optional): Whether to accept an empty answer. Defaults to true.
    • trim (boolean, optional): Whether to trim leading and trailing whitespace from the answer before returning it.
    • min_length (integer, optional): Minimum accepted character length.
    • max_length (integer, optional): Maximum accepted character length.
    • pattern (string, optional): Regular expression the answer must match.
  • Returns: string.

Example:

local project = ptool.ask("Project name?", {
placeholder = "my-tool",
help = "Lowercase letters, digits, and dashes only",
required = true,
trim = true,
pattern = "^[a-z0-9-]+$",
})

ptool.ask.confirm

v0.5.0 - Introduced.

ptool.ask.confirm(prompt[, options]) asks the user for a yes/no answer.

  • prompt (string, required): The prompt shown to the user.
  • options (table, optional): Prompt options. Supported fields:
    • default (boolean, optional): Default answer used when the user presses Enter without typing.
    • help (string, optional): Extra help text shown below the prompt.
  • Returns: boolean.

Example:

local confirmed = ptool.ask.confirm("Continue?", {
default = true,
})

ptool.ask.select

v0.5.0 - Introduced.

ptool.ask.select(prompt, items[, options]) asks the user to choose one item from a list.

  • prompt (string, required): The prompt shown to the user.
  • items (table, required): Candidate items. Each entry may be:
    • A string, which is used as both the display label and the returned value.
    • A table like { label = "Patch", value = "patch" }.
  • options (table, optional): Prompt options. Supported fields:
    • help (string, optional): Extra help text shown below the prompt.
    • page_size (integer, optional): Maximum number of rows shown at once.
    • default_index (integer, optional): 1-based index of the initially selected item.
  • Returns: string.

Example:

local bump = ptool.ask.select("Select bump type", {
{ label = "Patch", value = "patch" },
{ label = "Minor", value = "minor" },
{ label = "Major", value = "major" },
}, {
default_index = 2,
})

ptool.ask.multiselect

v0.5.0 - Introduced.

ptool.ask.multiselect(prompt, items[, options]) asks the user to choose zero or more items from a list.

  • prompt (string, required): The prompt shown to the user.
  • items (table, required): Candidate items. The format is the same as ptool.ask.select.
  • options (table, optional): Prompt options. Supported fields:
    • help (string, optional): Extra help text shown below the prompt.
    • page_size (integer, optional): Maximum number of rows shown at once.
    • default_indexes (table, optional): 1-based indexes selected by default.
    • min_selected (integer, optional): Minimum number of items that must be selected.
    • max_selected (integer, optional): Maximum number of items that may be selected.
  • Returns: table.

Example:

local targets = ptool.ask.multiselect("Select targets", {
"linux",
"macos",
"windows",
}, {
default_indexes = { 1, 2 },
min_selected = 1,
})

ptool.ask.secret

v0.5.0 - Introduced.

ptool.ask.secret(prompt[, options]) asks the user for secret input such as a token or password.

  • prompt (string, required): The prompt shown to the user.
  • options (table, optional): Prompt options. Supported fields:
    • help (string, optional): Extra help text shown below the prompt.
    • required (boolean, optional): Whether the answer must be non-empty.
    • allow_empty (boolean, optional): Whether to accept an empty answer. Defaults to false.
    • confirm (boolean, optional): Whether to ask the user to type the secret twice. Defaults to false.
    • confirm_prompt (string, optional): Custom prompt for the confirmation step.
    • mismatch_message (string, optional): Custom error message shown when the two answers do not match.
    • display_toggle (boolean, optional): Whether to allow temporarily showing the typed secret.
    • min_length (integer, optional): Minimum accepted character length.
    • max_length (integer, optional): Maximum accepted character length.
    • pattern (string, optional): Regular expression the answer must match.
  • Returns: string.

Example:

local token = ptool.ask.secret("API token?", {
confirm = true,
min_length = 20,
})

ptool.config

v0.1.0 - Introduced.

ptool.config sets runtime configuration for the script.

Currently supported fields:

  • run (table, optional): Default configuration for ptool.run. Supported fields:
    • echo (boolean, optional): Default echo switch. Defaults to true.
    • check (boolean, optional): Whether failures raise an error by default. Defaults to false.
    • confirm (boolean, optional): Whether to require confirmation before execution by default. Defaults to false.
    • retry (boolean, optional): Whether to ask the user whether to retry after a failed execution when check = true. Defaults to false.

Example:

ptool.config({
run = {
echo = false,
check = true,
confirm = false,
retry = false,
},
})

ptool.cd

v0.1.0 - Introduced.

ptool.cd(path) updates ptool's runtime current directory.

  • path (string, required): Target directory path, absolute or relative.

Behavior:

  • Relative paths are resolved from the current ptool runtime directory.
  • The target must exist and must be a directory.
  • This updates ptool runtime state and affects APIs that use runtime cwd (such as ptool.run, ptool.path.abspath, and ptool.path.relpath).

Example:

ptool.cd("foobar")
local res = ptool.run({ cmd = "pwd", stdout = "capture" })
print(res.stdout)

ptool.script_path

v0.4.0 - Introduced.

ptool.script_path() returns the absolute path of the current entry script.

  • Returns: string|nil.

Behavior:

  • When running through ptool run <file>, this returns the entry script path as an absolute, normalized path.
  • The returned path is fixed when the runtime starts and does not change after ptool.cd(...).
  • In ptool repl, this returns nil.

Example:

local script_path = ptool.script_path()
local script_dir = ptool.path.dirname(script_path)
local project_root = ptool.path.dirname(script_dir)

ptool.try

v0.4.0 - Introduced.

ptool.try(fn) runs fn and converts raised errors into return values.

  • fn (function, required): Callback to execute.
  • Returns: ok, value, err.

Return value rules:

  • On success, ok = true, err = nil, and value contains the callback result.
  • If the callback returns no values, value is nil.
  • If the callback returns one value, value is that value.
  • If the callback returns multiple values, value is an array-like table.
  • On failure, ok = false, value = nil, and err is a table.

Structured error fields:

  • kind (string): Stable error category such as io_error, command_failed, invalid_argument, http_error, or lua_error.
  • message (string): Human-readable error message.
  • op (string, optional): API or operation name such as ptool.fs.read.
  • detail (string, optional): Extra detail for the failure.
  • path (string, optional): Path involved in a filesystem failure.
  • input (string, optional): Original input that failed to parse or validate.
  • cmd (string, optional): Command name for command failures.
  • status (integer, optional): Exit status or HTTP status when available.
  • stderr (string, optional): Captured stderr for command failures.
  • url (string, optional): URL involved in an HTTP failure.
  • cwd (string, optional): Effective working directory for command failures.
  • target (string, optional): SSH target for SSH-related command failures.
  • retryable (boolean): Whether retrying may make sense. Defaults to false.

Behavior:

  • ptool APIs raise structured errors. ptool.try converts them into the err table above so callers can branch on err.kind and related fields.
  • Plain Lua errors are also caught. In that case, err.kind is lua_error, and only message is guaranteed.
  • ptool.try is the recommended way to handle errors from APIs such as ptool.fs.read, ptool.http.request, ptool.run(..., { check = true }), and res:assert_ok().

Example:

local ok, content, err = ptool.try(function()
return ptool.fs.read("missing.txt")
end)

if not ok and err.kind == "io_error" then
print(err.op, err.path)
end

local ok2, _, err2 = ptool.try(function()
local res = ptool.run({
cmd = "sh",
args = {"-c", "echo bad >&2; exit 7"},
stderr = "capture",
})
res:assert_ok()
end)

if not ok2 and err2.kind == "command_failed" then
print(err2.cmd, err2.status, err2.stderr)
end

ptool.run

v0.1.0 - Introduced.

ptool.run executes external commands from Rust.

The following call forms are supported:

ptool.run("echo hello world")
ptool.run("echo", "hello world")
ptool.run("echo", {"hello", "world"})
ptool.run("echo hello world", { echo = true })
ptool.run("echo", {"hello", "world"}, { echo = true })
ptool.run({ cmd = "echo", args = {"hello", "world"} })
ptool.run({ cmd = "echo", args = {"hello"}, stdout = "capture" })

Argument rules:

  • ptool.run(cmdline): cmdline is split using shell-style (shlex) rules. The first item is treated as the command and the rest as arguments.
  • ptool.run(cmd, argsline): cmd is used directly as the command, and argsline is split into an argument list using shell-style (shlex) rules.
  • ptool.run(cmd, args): cmd is a string and args is an array of strings.
  • ptool.run(cmdline, options): options overrides settings for this invocation, such as echo.
  • ptool.run(cmd, args, options): args can be either a string or an array of strings, and options overrides settings for this invocation, such as echo.
  • ptool.run(options): options is a table.
  • When the second argument is a table: if it is an array (consecutive integer keys 1..n), it is treated as args; otherwise it is treated as options.

Return value rules:

  • A table is always returned with the following fields:
    • ok (boolean): Whether the exit code is 0.
    • code (integer|nil): The process exit code. If the process was terminated by a signal, this is nil.
    • cmd (string): Command name used for the execution.
    • cwd (string): Effective working directory used for the execution.
    • stdout (string, optional): Present when stdout = "capture".
    • stderr (string, optional): Present when stderr = "capture".
    • assert_ok(self) (function): Raises a structured error when ok = false. The error kind is command_failed, and the error may include cmd, status, stderr, and cwd.
  • The default value of check comes from ptool.config({ run = { check = ... } }). If not configured, it defaults to false. When check = false, callers can inspect ok themselves or call res:assert_ok().
  • When both check = true and retry = true, ptool.run asks whether the failed command should be retried before raising the final error.
  • When check = true, ptool.run raises the same structured command_failed error that res:assert_ok() raises. Use ptool.try(...) if you want to catch and inspect it from Lua.

Example:

ptool.config({ run = { echo = false } })

ptool.run("echo from ptool")
ptool.run("echo", "from ptool")
ptool.run("echo", {"from", "ptool"})
ptool.run("echo from ptool", { echo = true })
ptool.run("echo", {"from", "ptool"}, { echo = true })
ptool.run("pwd")

local res = ptool.run({
cmd = "sh",
args = {"-c", "echo bad >&2; exit 7"},
stderr = "capture",
})
print(res.ok, res.code)
res:assert_ok()

ptool.run(options) is also supported, where options is a table with the following fields:

  • cmd (string, required): The command name or executable path.
  • args (string[], optional): The argument list.
  • cwd (string, optional): The child process working directory.
  • env (table, optional): Additional environment variables, where keys are variable names and values are variable values.
  • echo (boolean, optional): Whether to echo command information for this execution. If omitted, the value from ptool.config({ run = { echo = ... } }) is used; if that is also unset, the default is true.
  • check (boolean, optional): Whether to raise an error immediately when the exit code is not 0. If omitted, the value from ptool.config({ run = { check = ... } }) is used; if that is also unset, the default is false.
  • confirm (boolean, optional): Whether to ask the user for confirmation before execution. If omitted, the value from ptool.config({ run = { confirm = ... } }) is used; if that is also unset, the default is false.
  • retry (boolean, optional): Whether to ask the user whether to retry after a failed execution when check = true. If omitted, the value from ptool.config({ run = { retry = ... } }) is used; if that is also unset, the default is false.
  • stdout (string, optional): Stdout handling strategy. Supported values:
    • "inherit": Inherit to the current terminal (default).
    • "capture": Capture into res.stdout.
    • "null": Discard the output.
  • stderr (string, optional): Stderr handling strategy. Supported values:
    • "inherit": Inherit to the current terminal (default).
    • "capture": Capture into res.stderr.
    • "null": Discard the output.
  • When confirm = true:
    • If the user refuses the execution, an error is raised immediately.
    • If the current environment is not interactive (no TTY), an error is raised immediately.
  • When retry = true and check = true:
    • If the command fails, ptool.run asks whether to retry the same command.
    • If the current environment is not interactive (no TTY), an error is raised immediately instead of prompting for retry.

Example:

ptool.run({
cmd = "echo",
args = {"hello"},
env = { FOO = "bar" },
})

local res = ptool.run({
cmd = "sh",
args = {"-c", "printf 'out'; printf 'err' >&2; exit 7"},
stdout = "capture",
stderr = "capture",
check = false,
})
print(res.ok, res.code)
print(res.stdout)
print(res.stderr)
res:assert_ok()

ptool.run_capture

Unreleased - Introduced.

ptool.run_capture executes external commands from Rust with the same call forms, argument rules, return value rules, and options as ptool.run.

The difference is only the default stream handling:

  • stdout defaults to "capture".
  • stderr defaults to "capture".

You can still override either field explicitly in options.

Example:

local res = ptool.run_capture("echo hello world")
print(res.stdout)

local res2 = ptool.run_capture({
cmd = "sh",
args = {"-c", "printf 'out'; printf 'err' >&2"},
})
print(res2.stdout)
print(res2.stderr)

local res3 = ptool.run_capture("echo hello", {
stderr = "inherit",
})
print(res3.stdout)