About
wardlib is an external extension of Ward’s “standard library”.
Ward ships with a small core set of modules. wardlib lives outside the Ward repository and exists to:
- provide additional batteries (modules, helpers, utilities),
- experiment and iterate faster than core,
- give the community a shared place to publish and maintain extensions.
Cookbook
This page collects pragmatic, end-to-end examples of Ward scripts built from Ward core modules and wardlib wrappers.
The examples are intentionally “real life”: they show complete workflows (execute commands, parse output, apply changes) rather than isolated API calls.
Git: detect repo root and show short status
local Git = require("wardlib.app.git").Git
local out = require("wardlib.tools.out")
local dir = "/home/me/project"
-- Repo root as a string.
local root = out.cmd(Git.root({ dir = dir }))
:label("git root")
:trim()
:line()
-- Short status lines.
local status = out.cmd(Git.status({ dir = dir, short = true, branch = true }))
:label("git status")
:lines()
print("root:", root)
for _, line in ipairs(status) do
print(line)
end
Git: clone if missing
local fs = require("ward.fs")
local Git = require("wardlib.app.git").Git
local url = "https://example.com/repo.git"
local dest = "/home/me/src/repo"
if not fs.is_exists(dest) then
Git.clone(url, dest, { depth = 1 }):run()
end
systemd: ensure a unit is enabled and active
This pattern is common for both system services and --user services.
local Systemd = require("wardlib.app.systemd").Systemd
local out = require("wardlib.tools.out")
local unit = "nginx.service"
-- Enable and start.
Systemd.enable(unit, { now = true }):run()
-- Parse state (allow non-zero exit codes; state is still printed).
local state = out.cmd(Systemd.is_active(unit))
:label("systemctl is-active")
:allow_fail()
:trim()
:line()
if state ~= "active" then
error(unit .. " is not active: " .. state)
end
systemd: fetch recent logs as JSON Lines
local Systemd = require("wardlib.app.systemd").Systemd
local out = require("wardlib.tools.out")
local unit = "nginx.service"
local entries = out.cmd(Systemd.journal(unit, { output = "json", lines = 50 }))
:label("journalctl -o json")
:json_lines()
for _, e in ipairs(entries) do
-- Common journal fields include MESSAGE, PRIORITY, _PID, _SYSTEMD_UNIT.
if e.MESSAGE then
print(e.MESSAGE)
end
end
Networking: list IPv4 addresses with ip -j
local Ip = require("wardlib.app.ip").Ip
local out = require("wardlib.tools.out")
local ifaces = out.cmd(Ip.raw({ "addr", "show" }, { json = true }))
:label("ip -j addr show")
:json()
local v4 = {}
for _, iface in ipairs(ifaces) do
local ai = iface.addr_info or {}
for _, a in ipairs(ai) do
if a.family == "inet" and a.local then
v4[#v4 + 1] = { ifname = iface.ifname, addr = a.local }
end
end
end
for _, x in ipairs(v4) do
print(x.ifname .. ":" .. x.addr)
end
Download, verify, and extract
This is a typical “fetch a release artifact” workflow.
local fs = require("ward.fs")
local out = require("wardlib.tools.out")
local Curl = require("wardlib.app.curl").Curl
local Sha256sum = require("wardlib.app.sha256sum").Sha256sum
local Unzip = require("wardlib.app.unzip").Unzip
local url = "https://example.com/releases/tool.zip"
local zip = "/tmp/tool.zip"
local expected_sha = "0123456789abcdef..."
local dest = "/opt/tool"
-- Download.
Curl.download(url, zip):run()
-- Verify sha256 (sha256sum prints: <hex> <path>).
local sha = out.cmd(Sha256sum.sum(zip))
:label("sha256sum")
:trim()
:match("^(%x+)")
if sha ~= expected_sha then
error("checksum mismatch: expected " .. expected_sha .. ", got " .. sha)
end
-- Extract.
fs.mkdir(dest, { recursive = true })
Unzip.extract(zip, { overwrite = true, to = dest }):run()
Cross-distro package install (pattern)
Use wardlib.tools.platform and the distro’s package manager wrapper.
local platform = require("wardlib.tools.platform")
local AptGet = require("wardlib.app.aptget").AptGet
local Pacman = require("wardlib.app.pacman").Pacman
local Dnf = require("wardlib.app.dnf").Dnf
local pkgs = { "git", "curl" }
local osr = platform.os_release() or {}
local id = osr.id
if id == "debian" or id == "ubuntu" then
AptGet.update({ sudo = true, assume_yes = true }):run()
AptGet.install(pkgs, { sudo = true, assume_yes = true }):run()
elseif id == "arch" then
Pacman.sync({ sudo = true }):run()
Pacman.install(pkgs, { sudo = true, needed = true }):run()
elseif id == "fedora" then
Dnf.install(pkgs, { sudo = true, assume_yes = true }):run()
else
error("unsupported distro: " .. tostring(id))
end
Dotfiles: apply a minimal preset
local dotfiles = require("wardlib.tools.dotfiles")
local def = dotfiles.define("Minimal dotfiles", {
description = "Small starter config",
steps = {
dotfiles.content(".config/myapp/config.toml", [[
enabled = true
]]),
dotfiles.link(".config/fish", "~/.dotfiles/fish", { recursive = true }),
},
})
def:apply("/home/me", { force = true })
apk
app.apk is a thin wrapper around Alpine’s apk that constructs
ward.process.cmd(...) invocations.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Apk = require("wardlib.app.apk").Apk
Running with elevated privileges
Package management typically requires root. Instead of passing { sudo = true } to
this module (not supported), use wardlib.tools.with to install a process
middleware.
local w = require("wardlib.tools.with")
local Apk = require("wardlib.app.apk").Apk
-- sudo -n apk upgrade
w.with(w.middleware.sudo(), Apk.upgrade()):run()
API
All functions return a ward.Cmd.
Apk.cmd(subcmd, argv, opts)
Generic helper that builds: apk <subcmd> [argv...].
Apk.update(opts)
Builds: apk update.
Apk.upgrade(opts)
Builds: apk upgrade.
Apk.add(pkgs, opts)
Builds: apk add <opts...> <pkgs...>.
Apk.del(pkgs, opts)
Builds: apk del <opts...> <pkgs...>.
Apk.search(pattern, opts)
Builds: apk search <pattern>.
Apk.info(pkg, opts)
Builds: apk info [pkg].
If pkg is nil, shows all installed packages.
Options
ApkCommonOpts
Modeled fields:
extra(string[]): extra args appended after modeled options
ApkAddOpts
Extends ApkCommonOpts.
Modeled fields:
no_cache(boolean):--no-cacheupdate_cache(boolean):--update-cachevirtual(string):--virtual <name>
ApkDelOpts
Extends ApkCommonOpts.
Modeled fields:
rdepends(boolean):--rdepends
Examples
local Apk = require("wardlib.app.apk").Apk
-- apk update
Apk.update():run()
-- apk add --no-cache curl git
Apk.add({ "curl", "git" }, { no_cache = true }):run()
-- apk search curl
local r = Apk.search("curl"):output()
apt-get
app.aptget is a thin wrapper around Debian/Ubuntu’s apt-get that constructs
ward.process.cmd(...) invocations.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local AptGet = require("wardlib.app.aptget").AptGet
Running with elevated privileges
Most apt-get operations require root. Instead of passing { sudo = true } to
this module (not supported), use wardlib.tools.with to install a process
middleware.
local w = require("wardlib.tools.with")
local AptGet = require("wardlib.app.aptget").AptGet
-- sudo -n apt-get update
w.with(w.middleware.sudo(), AptGet.update()):run()
API
All functions return a ward.Cmd.
AptGet.cmd(subcmd, argv, opts)
Generic helper that builds: apt-get <common opts...> <subcmd> [argv...].
AptGet.update(opts)
Builds: apt-get update.
AptGet.upgrade(opts)
Builds: apt-get upgrade.
AptGet.dist_upgrade(opts)
Builds: apt-get dist-upgrade.
AptGet.install(pkgs, opts)
Builds: apt-get install <pkgs...> (plus modeled options).
AptGet.remove(pkgs, opts)
Builds: apt-get remove <pkgs...>.
If opts.purge = true, builds: apt-get purge <pkgs...>.
AptGet.autoremove(opts)
Builds: apt-get autoremove.
AptGet.clean(opts)
Builds: apt-get clean.
Options
AptGetCommonOpts
Modeled fields:
assume_yes(boolean):-yquiet(boolean|integer):-q/-qq(true or 1 =>-q, 2 =>-qq)extra(string[]): extra args appended after modeled options
AptGetInstallOpts
Extends AptGetCommonOpts.
Modeled fields:
no_install_recommends(boolean):--no-install-recommends
AptGetRemoveOpts
Extends AptGetCommonOpts.
Modeled fields:
purge(boolean): usespurgeinstead ofremove
Examples
local w = require("wardlib.tools.with")
local AptGet = require("wardlib.app.aptget").AptGet
-- sudo -n apt-get -qq -y upgrade
w.with(w.middleware.sudo(), AptGet.upgrade({ assume_yes = true, quiet = 2 })):run()
-- sudo -n apt-get -y install --no-install-recommends curl git
w.with(w.middleware.sudo(), AptGet.install({ "curl", "git" }, {
assume_yes = true,
no_install_recommends = true,
})):run()
archive
Thin wrapper around tar for creating, extracting, and listing archives.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Archive = require("wardlib.app.archive").Archive
Privilege escalation
This module does not implement sudo/doas options. If you need elevated
privileges (e.g. extracting into a root-owned directory), use wardlib.tools.with
middleware.
local w = require("wardlib.tools.with")
local Archive = require("wardlib.app.archive").Archive
w.with(w.middleware.sudo(), Archive.extract("/tmp/app.tar.gz", { to = "/srv/app" })):run()
API
Archive.bin
Executable name or path used for tar.
Archive.create(archive_path, inputs, opts)
Builds: tar -c <common opts...> -f <archive_path> <inputs...>
archive_path(string): output archive path.inputs(string[]): paths to include (must be non-empty).opts(ArchiveCommonOpts|nil): modeled options.
Archive.extract(archive_path, opts)
Builds: tar -x <common opts...> -f <archive_path> [--strip-components=N] [-C <to>]
archive_path(string): archive path.opts(ArchiveExtractOpts|nil): modeled options.
Archive.list(archive_path, opts)
Builds: tar -t <common opts...> -f <archive_path>
archive_path(string): archive path.opts(ArchiveCommonOpts|nil): modeled options.
Options
ArchiveCommonOpts
dir: string|nil— Forcreateonly:-C <dir>before input paths.verbose: boolean|nil—-v.compression: "gz"|"xz"|"zstd"|nil— Compression selector:"gz"→-z"xz"→-J"zstd"→--zstd
extra: string[]|nil— Pass-through flags appended after modeled options.
ArchiveExtractOpts
Extends ArchiveCommonOpts with:
to: string|nil— Destination directory:-C <to>.strip_components: integer|nil—--strip-components=<n>.
extra ordering
extra is appended after modeled options:
- for
create: before-f <archive_path>and before input paths - for
extract: before-f <archive_path> - for
list: before-f <archive_path>
Examples
Create tar.gz from a project directory
local Archive = require("wardlib.app.archive").Archive
-- tar -c -z -C /home/me/project -f /tmp/project.tar.gz .
local cmd = Archive.create("/tmp/project.tar.gz", { "." }, {
dir = "/home/me/project",
compression = "gz",
})
Create an xz archive of selected paths (verbose)
local Archive = require("wardlib.app.archive").Archive
-- tar -c -J -v -C /home/me -f /tmp/stuff.tar.xz docs photos
local cmd = Archive.create("/tmp/stuff.tar.xz", { "docs", "photos" }, {
dir = "/home/me",
compression = "xz",
verbose = true,
})
Extract into a destination directory
local Archive = require("wardlib.app.archive").Archive
-- tar -x -f /tmp/project.tar.gz -C /srv/app
local cmd = Archive.extract("/tmp/project.tar.gz", {
to = "/srv/app",
})
Extract while stripping top-level directory
local Archive = require("wardlib.app.archive").Archive
-- tar -x -f /tmp/project.tar.gz --strip-components=1 -C /srv/app
local cmd = Archive.extract("/tmp/project.tar.gz", {
strip_components = 1,
to = "/srv/app",
})
List archive contents and parse as lines
local Archive = require("wardlib.app.archive").Archive
local out = require("wardlib.tools.out")
-- tar -t -v -f /tmp/project.tar.gz
local res = Archive.list("/tmp/project.tar.gz", { verbose = true }):output()
local lines = out.res(res):ok():lines()
Pass-through extra tar flags
local Archive = require("wardlib.app.archive").Archive
-- tar -c -z -C /home/me/project --exclude=.git --exclude=target -f /tmp/src.tar.gz .
local cmd = Archive.create("/tmp/src.tar.gz", { "." }, {
dir = "/home/me/project",
compression = "gz",
extra = { "--exclude=.git", "--exclude=target" },
})
awk
Thin wrapper around awk (and gawk-style extensions) that constructs
ward.process.cmd(...) invocations.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Awk = require("wardlib.app.awk").Awk
Privilege escalation
This module does not implement sudo/doas options. Use wardlib.tools.with
middleware when needed.
API
Awk.bin
Executable name or path used for awk.
Awk.cmd(argv)
Low-level escape hatch.
Builds: awk <argv...>
argv: string[]|nil— raw argv (without theawkbinary).
Awk.eval(program, inputs, opts)
Inline program mode.
Builds: awk <opts...> <program> <assigns...> <inputs...>
program: string— inline awk program.inputs: string|string[]|nil— input file(s). Whennil, awk reads stdin.opts: AwkOpts|nil— modeled options.
Awk.source(programs, inputs, opts)
Multiple inline programs.
Builds: awk <opts...> -e <p1> -e <p2> ... <assigns...> <inputs...>
programs: string|string[]— one or more programs.
Awk.file(scripts, inputs, opts)
Script file mode.
Builds: awk <opts...> -f <file1> -f <file2> ... <assigns...> <inputs...>
scripts: string|string[]— one or more.awkscript paths.
Options (AwkOpts)
General:
field_sep: string|nil—-F <sep>.vars: table|nil—-v name=valuerepeated. Accepts:- array form:
{ "k=v", "x=1" } - map form:
{ k = "v", x = 1 }(sorted by key for deterministic argv)
- array form:
assigns: table|nil— post-program assignmentsname=value(array or map; map is sorted by key).includes: string[]|nil— gawk:-i <file>repeated.extra: string[]|nil— extra args appended before the program/scripts.
Boolean long flags (mostly gawk):
posix(--posix)traditional(--traditional)lint(--lint)interval(--interval)bignum(--bignum)sandbox(--sandbox)csv(--csv)optimize(--optimize)ignore_case(--ignore-case)characters_as_bytes(--characters-as-bytes)use_lc_numeric(--use-lc-numeric)
Optional-value long flags (true => flag only; string => --flag=<value>):
debug— gawk:--debug[=flags]profile— gawk:--profile[=file]pretty_print— gawk:--pretty-print[=file]dump_variables— gawk:--dump-variables[=file]
Examples
Inline program
local Awk = require("wardlib.app.awk").Awk
-- awk -F: '{print $1}' /etc/passwd
local cmd = Awk.eval("{print $1}", "/etc/passwd", {
field_sep = ":",
})
With variables (-v) and assignments
local Awk = require("wardlib.app.awk").Awk
-- awk -v a=x -v b=2 '{print a, b, $1}' y=t z=9 /etc/passwd
local cmd = Awk.eval("{print a, b, $1}", "/etc/passwd", {
vars = { a = "x", b = 2 },
assigns = { y = "t", z = 9 },
})
Notes:
vars(-v) are applied before the program.assigns(name=value) are appended after the program (and before inputs).
Multiple programs (-e)
local Awk = require("wardlib.app.awk").Awk
-- awk -e 'BEGIN{a=1}' -e '{print a}' input.txt
local cmd = Awk.source({ "BEGIN{a=1}", "{print a}" }, "input.txt")
Script files (-f)
local Awk = require("wardlib.app.awk").Awk
-- awk -f a.awk -f b.awk input.txt
local cmd = Awk.file({ "a.awk", "b.awk" }, "input.txt")
Parse stdout as lines
local Awk = require("wardlib.app.awk").Awk
local out = require("wardlib.tools.out")
local res = Awk.eval("{print $1}", "/etc/passwd"):output()
local lines = out.res(res):ok():lines()
bemenu
bemenu is a Wayland-native dynamic menu inspired by dmenu.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Bemenu = require("wardlib.app.bemenu").Bemenu
API
Bemenu.bin
Executable name or path (default: "bemenu").
Bemenu.bin_run
Executable name or path (default: "bemenu-run").
Bemenu.menu(opts)
Builds: bemenu <opts...>
Bemenu.run(opts)
Builds: bemenu-run <opts...>
Options
BemenuOpts
prompt: string?→-p <text>lines: number?→-l <n>ignorecase: boolean?→-icenter: boolean?→-cfork: boolean?→-fno_cursor: boolean?→-Cextra: string[]?→ extra argv appended after modeled options
Examples
local Bemenu = require("wardlib.app.bemenu").Bemenu
Bemenu.menu({ prompt = "Run", lines = 10 }):output()
blkid
blkid prints block device attributes (LABEL, UUID, TYPE, etc.).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Blkid = require("wardlib.app.blkid").Blkid
Privilege escalation
Depending on your system configuration, probing certain devices may require
elevated privileges. This module does not implement sudo/doas options;
use wardlib.tools.with middleware.
local w = require("wardlib.tools.with")
local Blkid = require("wardlib.app.blkid").Blkid
w.with(w.middleware.sudo(), Blkid.id("/dev/sda1")):run()
API
Blkid.bin
Executable name or path used for blkid.
Blkid.id(devices, opts)
Builds: blkid <opts...> [devices...]
devices: string|string[]|nil— one or more device paths. Whennil,blkidprobes available devices.opts: BlkidOpts|nil— modeled options.
Blkid.by_label(label)
Builds: blkid -L <label>
Blkid.by_uuid(uuid)
Builds: blkid -U <uuid>
Options (BlkidOpts)
output: "full"|"value"|"device"|"export"|"udev"|nil—-o <fmt>.tags: string[]|nil—-s <tag>repeated (e.g.{ "UUID", "TYPE" }).match: string|string[]|nil—-t <token>repeated (e.g."TYPE=ext4").cache_file: string|nil—-c <file>(use"/dev/null"to disable cache).probe: boolean|nil—-p(low-level probe).wipe_cache: boolean|nil—-w/--wipe-cache.garbage_collect: boolean|nil—-g/--garbage-collect.extra: string[]|nil— pass-through args appended after modeled options.
Examples
Probe a device and print selected fields
local Blkid = require("wardlib.app.blkid").Blkid
-- blkid -o export -s UUID -s TYPE /dev/sda1
local cmd = Blkid.id("/dev/sda1", {
output = "export",
tags = { "UUID", "TYPE" },
})
Find the device path by label or UUID
local Blkid = require("wardlib.app.blkid").Blkid
-- blkid -L root
local by_label = Blkid.by_label("root")
-- blkid -U 1111-2222
local by_uuid = Blkid.by_uuid("1111-2222")
Filter by token match
local Blkid = require("wardlib.app.blkid").Blkid
-- blkid -t TYPE=ext4 -t LABEL=myroot
local cmd = Blkid.id(nil, {
match = { "TYPE=ext4", "LABEL=myroot" },
})
Parse -o export output
local Blkid = require("wardlib.app.blkid").Blkid
local out = require("wardlib.tools.out")
local res = Blkid.id("/dev/sda1", { output = "export" }):output()
local kv = {}
for _, line in ipairs(out.res(res):ok():lines()) do
local k, v = line:match("^([^=]+)=(.*)$")
if k ~= nil then kv[k] = v end
end
-- kv.UUID, kv.TYPE, ...
cfdisk
Thin wrapper around cfdisk (util-linux) for interactive partition editing.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
cfdisk is interactive (curses UI). It is not suitable for non-interactive
automation; for scripted partitioning use wardlib.app.sfdisk.
Import
local Cfdisk = require("wardlib.app.cfdisk").Cfdisk
Privilege escalation
Partition editing typically requires elevated privileges. This module does not
implement sudo/doas options; use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Cfdisk = require("wardlib.app.cfdisk").Cfdisk
w.with(w.middleware.sudo(), Cfdisk.edit("/dev/sda")):run()
API
Cfdisk.bin
Executable name or path used for cfdisk.
Cfdisk.edit(device, opts)
Builds: cfdisk <opts...> <device>
device: string— block device path (must not start with-).opts: CfdiskOpts|nil— modeled options.
Options (CfdiskOpts)
color: "auto"|"never"|"always"|nil— adds--color[=<when>].sector_size: integer|nil— adds--sector-size <n>.zero: boolean|nil— adds--zero.read_only: boolean|nil— adds--read-only.extra: string[]|nil— pass-through args appended before the device.
Examples
Interactive partition editing
local Cfdisk = require("wardlib.app.cfdisk").Cfdisk
-- cfdisk /dev/sda
Cfdisk.edit("/dev/sda"):run()
With modeled options + pass-through flags
local Cfdisk = require("wardlib.app.cfdisk").Cfdisk
-- cfdisk --color=never --sector-size 4096 --read-only --zero --wipe always /dev/sda
Cfdisk.edit("/dev/sda", {
color = "never",
sector_size = 4096,
read_only = true,
zero = true,
extra = { "--wipe", "always" }, -- anything not modeled
}):run()
chattr
chattr changes file attributes on Linux filesystems.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Chattr = require("wardlib.app.chattr").Chattr
Privilege escalation
Changing attributes frequently requires elevated privileges. This module does
not implement sudo/doas options; use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Chattr = require("wardlib.app.chattr").Chattr
w.with(w.middleware.sudo(), Chattr.set("important.txt", "+i")):run()
API
Chattr.bin
Executable name or path used for chattr.
Chattr.set(paths, mode, opts)
Builds: chattr <opts...> -- <mode> <paths...>
paths: string|string[]— one or more paths (must be non-empty).mode: string— attribute mode string (examples:+i,-i,=ai).opts: ChattrOpts|nil— modeled options.
Chattr.raw(argv, opts)
Low-level escape hatch.
Builds: chattr <modeled-opts...> <argv...>
Options (ChattrOpts)
recursive: boolean|nil—-R.verbose: boolean|nil—-V.force: boolean|nil—-f.version: integer|nil—-v <version>.extra: string[]|nil— pass-through args appended after modeled options.
Examples
Make a file immutable
local Chattr = require("wardlib.app.chattr").Chattr
-- chattr -- +i important.txt
local cmd = Chattr.set("important.txt", "+i")
Clear immutable recursively
local Chattr = require("wardlib.app.chattr").Chattr
-- chattr -R -- -i /srv/app
local cmd = Chattr.set("/srv/app", "-i", { recursive = true })
chmod
chmod changes file mode bits.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Chmod = require("wardlib.app.chmod").Chmod
Privilege escalation
Changing mode on root-owned paths requires elevated privileges. This module
does not implement sudo/doas options; use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Chmod = require("wardlib.app.chmod").Chmod
w.with(w.middleware.sudo(), Chmod.set("/srv/app", "0755", { recursive = true })):run()
API
Chmod.bin
Executable name or path used for chmod.
Chmod.set(paths, mode, opts)
Builds: chmod <opts...> -- <mode> <paths...>
paths: string|string[]— one or more paths.mode: string— mode string (e.g.0755,u+rwx,a-w).opts: ChmodOpts|nil— modeled options.
Chmod.reference(paths, ref, opts)
GNU-style reference mode.
Builds: chmod <opts...> --reference=<ref> -- <paths...>
ref: string— reference file path.
Chmod.raw(argv, opts)
Low-level escape hatch.
Builds: chmod <modeled-opts...> <argv...>
Options (ChmodOpts)
recursive: boolean|nil—-R.verbose: boolean|nil—-v.changes: boolean|nil—-c.silent: boolean|nil—-f.reference: string|nil— GNU:--reference=<file>.preserve_root: boolean|nil— GNU:--preserve-root.no_preserve_root: boolean|nil— GNU:--no-preserve-root.extra: string[]|nil— pass-through args appended after modeled options.
Notes:
preserve_rootandno_preserve_rootare mutually exclusive.
Examples
Set recursive numeric mode
local Chmod = require("wardlib.app.chmod").Chmod
-- chmod -R -- 0755 bin
local cmd = Chmod.set("bin", "0755", { recursive = true })
Copy mode from a reference file
local Chmod = require("wardlib.app.chmod").Chmod
-- chmod --reference=ref -- target
local cmd = Chmod.reference("target", "ref")
chown
chown changes file owner and group.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Chown = require("wardlib.app.chown").Chown
Privilege escalation
Changing ownership typically requires elevated privileges. This module does
not implement sudo/doas options; use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Chown = require("wardlib.app.chown").Chown
w.with(w.middleware.sudo(), Chown.set("/srv/app", "root", "root", { recursive = true })):run()
API
Chown.bin
Executable name or path used for chown.
Chown.set(paths, owner, group, opts)
Builds: chown <opts...> -- <owner[:group]> <paths...>
paths: string|string[]— one or more paths.owner: string|nil— owner (user name or numeric id).group: string|nil— group (group name or numeric id).opts: ChownOpts|nil— modeled options.
Rules:
- Provide
owner,group, or both. - If
ownerisnilandgroupis notnil, the wrapper emits:<group>.
Chown.raw(argv, opts)
Low-level escape hatch.
Builds: chown <modeled-opts...> <argv...>
Options (ChownOpts)
recursive: boolean|nil—-R.verbose: boolean|nil—-v.changes: boolean|nil—-c.silent: boolean|nil—-f.dereference: boolean|nil—-h(affects symlinks).preserve_root: boolean|nil— GNU:--preserve-root.no_preserve_root: boolean|nil— GNU:--no-preserve-root.extra: string[]|nil— pass-through args appended after modeled options.
Notes:
preserve_rootandno_preserve_rootare mutually exclusive.
Examples
Set owner and group recursively
local Chown = require("wardlib.app.chown").Chown
-- chown -R -- root:root /srv/app
local cmd = Chown.set("/srv/app", "root", "root", { recursive = true })
Change group only
local Chown = require("wardlib.app.chown").Chown
-- chown -- :wheel somefile
local cmd = Chown.set("somefile", nil, "wheel")
chroot
Thin wrapper around chroot (GNU coreutils).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
The wrapper models the common GNU flags:
--userspec=USER:GROUP--groups=G1,G2,...--skip-chdir
Everything else can be passed via opts.extra.
Import
local Chroot = require("wardlib.app.chroot").Chroot
Privilege escalation
Entering a chroot typically requires elevated privileges. This module does not
implement sudo/doas options; use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Chroot = require("wardlib.app.chroot").Chroot
w.with(w.middleware.sudo(), Chroot.run("/mnt/root", { "/usr/bin/id" })):run()
API
Chroot.bin
Executable name or path used for chroot.
Chroot.run(root, argv, opts)
Builds: chroot [opts...] <root> [command [args...]]
root: string— new root directory.argv: string[]|nil— command and arguments inside the chroot. Whennil,chrootruns the default shell.opts: ChrootOpts|nil— modeled options.
Options (ChrootOpts)
userspec: string|nil—--userspec=<user>:<group>.groups: string|string[]|nil—--groups=<g1>,<g2>.skip_chdir: boolean|nil—--skip-chdir.extra: string[]|nil— pass-through args appended before positional args.
Examples
Run a command inside a root
local Chroot = require("wardlib.app.chroot").Chroot
-- chroot /mnt/root /bin/sh -lc 'echo ok'
local cmd = Chroot.run("/mnt/root", { "/bin/sh", "-lc", "echo ok" })
Run as a specific user/group
local Chroot = require("wardlib.app.chroot").Chroot
-- chroot --userspec=1000:1000 --groups=wheel,audio --skip-chdir /mnt/root /usr/bin/id
local cmd = Chroot.run("/mnt/root", { "/usr/bin/id" }, {
userspec = "1000:1000",
groups = { "wheel", "audio" },
skip_chdir = true,
})
compose
Unified wrapper for Compose commands.
This module targets one of two engines:
- Docker Compose v2 plugin:
docker compose ...(default) - Podman Compose plugin:
podman compose ...(whenengine = "podman")
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Compose = require("wardlib.app.compose").Compose
Privilege escalation
Compose typically runs as the current user. If your engine requires elevated
privileges on your system, wrap the returned command with wardlib.tools.with.
local w = require("wardlib.tools.with")
local Compose = require("wardlib.app.compose").Compose
w.with(w.middleware.sudo(), Compose.version()):run()
Engine selection
The engine option selects which binary is used:
engine = nilorengine = "docker"→docker compose ...engine = "podman"→podman compose ...
API
All functions return a ward.process.cmd(...) object.
Low-level helpers
Compose.cmd(subcmd, argv, opts)
Builds: (<engine>) compose <subcmd> <global opts...> <argv...>
subcmd: stringargv: string|string[]|nil— appended after modeled options.opts: ComposeOpts|nil
Compose.raw(argv, opts)
Builds: (<engine>) compose <global opts...> <argv...>
Lifecycle
Compose.up(services, opts)
Builds: (<engine>) compose up <global opts...> <up opts...> [services...]
Compose.down(opts)
Builds: (<engine>) compose down <global opts...> <down opts...>
Compose.ps(services, opts)
Builds: (<engine>) compose ps <global opts...> <ps opts...> [services...]
Compose.logs(services, opts)
Builds: (<engine>) compose logs <global opts...> <logs opts...> [services...]
Compose.start(services, opts) / Compose.stop(services, opts) / Compose.restart(services, opts)
Builds: (<engine>) compose <start|stop|restart> <global opts...> <start/stop opts...> [services...]
Images
Compose.build(services, opts)
Builds: (<engine>) compose build <global opts...> <build opts...> [services...]
Compose.pull(services, opts)
Builds: (<engine>) compose pull <global opts...> <pull opts...> [services...]
Run and exec
Compose.exec(service, cmdline, opts)
Builds: (<engine>) compose exec <global opts...> <exec opts...> <service> <cmdline...>
service: stringcmdline: string|string[]|nil— command inside the container; whennil, exec opens the service default command.
Compose.run(service, cmdline, opts)
Builds: (<engine>) compose run <global opts...> <run opts...> <service> <cmdline...>
Introspection
Compose.config(opts)
Builds: (<engine>) compose config <global opts...>
Compose.version(opts)
Builds: (<engine>) compose version <global opts...>
Options
ComposeOpts (global options)
Applied to all subcommands:
engine: "docker"|"podman"|nil— select binary.project_name: string|nil—-p <name>.file: string|string[]|nil—-f <file>repeated.env_file: string|string[]|nil—--env-file <file>repeated.profile: string|string[]|nil—--profile <name>repeated.ansi: string|nil—--ansi <never|always|auto>.progress: string|nil—--progress <auto|tty|plain|quiet>.extra: string[]|nil— pass-through args appended after modeled global options.
ComposeUpOpts
Extends ComposeOpts with:
detach(-d)build(--build)force_recreate(--force-recreate)no_recreate(--no-recreate)remove_orphans(--remove-orphans)no_start(--no-start)wait(--wait)
ComposeDownOpts
Extends ComposeOpts with:
remove_orphans(--remove-orphans)volumes(-v)rmi: string|nil(--rmi <all|local>)timeout: integer|nil(-t <seconds>)
ComposePsOpts
Extends ComposeOpts with:
all(-a)quiet(-q)status: string|nil(--status <running|paused|exited|created|restarting|removing|dead>)format: string|nil(--format <fmt>)
ComposeLogsOpts
Extends ComposeOpts with:
follow(-f)timestamps(-t)tail: string|integer|nil(--tail <n|all>)no_color(--no-color)since: string|nil(--since <time>)until: string|nil(--until <time>)
ComposeBuildOpts
Extends ComposeOpts with:
pull(--pull)no_cache(--no-cache)parallel(--parallel)push(--push)
ComposePullOpts
Extends ComposeOpts with:
include_deps(--include-deps)ignore_failures(--ignore-pull-failures)
ComposeStartStopOpts
Extends ComposeOpts with:
timeout: integer|nil(-t <seconds>)
ComposeExecOpts
Extends ComposeOpts with:
detach(-d)interactive(-i)tty(-t)user: string|nil(-u <user>)workdir: string|nil(-w <dir>)env: string|string[]|nil(-e <k=v>repeatable)
ComposeRunOpts
Extends ComposeOpts with:
detach(-d)rm(--rm)name: string|nil(--name <name>)entrypoint: string|nil(--entrypoint <entrypoint>)user: string|nil(-u <user>)workdir: string|nil(-w <dir>)env: string|string[]|nil(-e <k=v>repeatable)publish: string|string[]|nil(-p <host:container>repeatable)volume: string|string[]|nil(-v <host:container>repeatable)no_deps(--no-deps)service_ports(--service-ports)
Examples
Bring up a project in detached mode
local Compose = require("wardlib.app.compose").Compose
-- docker compose -f compose.yml up -d
local cmd = Compose.up(nil, { file = "compose.yml", detach = true })
Use podman engine
local Compose = require("wardlib.app.compose").Compose
-- podman compose down --remove-orphans
local cmd = Compose.down({ engine = "podman", remove_orphans = true })
Exec with environment and workdir
local Compose = require("wardlib.app.compose").Compose
-- docker compose exec -w /w -e A=1 web sh -lc 'id'
local cmd = Compose.exec("web", { "sh", "-lc", "id" }, { workdir = "/w", env = "A=1" })
Parse compose config output as text
local Compose = require("wardlib.app.compose").Compose
local out = require("wardlib.tools.out")
local res = Compose.config({ file = "compose.yml" }):output()
local yaml_text = out.res(res):ok():text()
Parse compose ps --format when supported
Some Compose versions support JSON formatting via --format json.
local Compose = require("wardlib.app.compose").Compose
local out = require("wardlib.tools.out")
local res = Compose.ps(nil, { format = "json" }):output()
local data = out.res(res):ok():json()
cp
cp copies files and directories.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Cp = require("wardlib.app.cp").Cp
Privilege escalation
This module does not implement sudo/doas options. When copying into
protected locations, use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Cp = require("wardlib.app.cp").Cp
w.with(w.middleware.sudo(), Cp.copy("./app", "/srv/app", { recursive = true })):run()
API
All functions return a ward.process.cmd(...) object.
Cp.bin
Executable name or path used for cp.
Cp.copy(src, dest, opts)
Builds: cp <opts...> -- <src...> <dest>
src: string|string[]— one or more source paths.dest: string— destination path.opts: CpOpts|nil— modeled options.
Cp.into(src, dir, opts)
Builds: cp <opts...> -t <dir> -- <src...>
This uses GNU-style -t. If your platform does not support -t,
prefer Cp.copy(src, dir, ...).
Cp.raw(argv, opts)
Low-level escape hatch.
Builds: cp <modeled-opts...> <argv...>
Options (CpOpts)
recursive: boolean|nil—-r.force: boolean|nil—-f.interactive: boolean|nil—-i.update: boolean|nil—-u.verbose: boolean|nil—-v.preserve: boolean|nil—-p.archive: boolean|nil—-a.parents: boolean|nil— GNU:--parents.target_directory: string|nil— GNU:-t <dir>.no_target_directory: boolean|nil— GNU:-T.extra: string[]|nil— pass-through args appended after modeled options.
Notes:
forceandinteractiveare mutually exclusive.
Examples
Copy multiple sources recursively
local Cp = require("wardlib.app.cp").Cp
-- cp -r -- a b dst
local cmd = Cp.copy({ "a", "b" }, "dst", { recursive = true })
Copy into a directory (GNU -t)
local Cp = require("wardlib.app.cp").Cp
-- cp --parents -t out -- src/file
local cmd = Cp.into("src/file", "out", { parents = true })
Raw escape hatch
local Cp = require("wardlib.app.cp").Cp
-- cp -a --reflink=auto -- a b
local cmd = Cp.raw({ "--reflink=auto", "--", "a", "b" }, { archive = true })
curl
curl is a command-line tool for transferring data (HTTP, HTTPS, etc.).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Curl = require("wardlib.app.curl").Curl
API
Curl.request(urls, opts)
Builds: curl <opts...> [urls...]
urls:string|string[]|nil- If
nil,curlruns with only the configured options.
- If
Curl.get(url, opts)
Convenience for Curl.request(url, opts).
Curl.head(url, opts)
Convenience for Curl.request(url, { head = true, ... }).
Curl.download(url, out, opts)
Convenience for downloading a URL.
- If
outis provided, uses-o <out>. - Otherwise uses
-O(remote name).
Curl.post(url, data, opts)
Convenience for POSTing a request body.
- If
opts.requestis not set, defaults toPOST. - If
datais provided, sets-d <data>.
All functions return a ward.process.cmd(...) object.
Options (CurlOpts)
Common fields:
- Verbosity / error handling:
silent(-s),show_error(-S),verbose(-v),fail(--fail) - Redirects / method:
location(-L),head(-I),request(-X <method>) - Request body:
data(-d <data>),data_raw(--data-raw <data>),form(-F <name=content>, repeatable) - Headers / identity:
user_agent(-A <ua>),header(-H <header>, repeatable) - Cookies:
cookie(-b <cookie>),cookie_jar(-c <file>) - Output:
output(-o <file>),remote_name(-O),remote_header_name(-J) - TLS:
insecure(-k),cacert(--cacert <file>),cert(--cert <cert[:passwd]>),key(--key <key>) - Timeouts / retries:
connect_timeout(--connect-timeout <sec>),max_time(--max-time <sec>),retry(--retry <n>) - Misc:
compressed(--compressed),ipv4(-4),ipv6(-6),http1_1(--http1.1),http2(--http2) - Formatting:
write_out(-w <format>) - Escape hatch:
extra(appended after modeled options)
Examples
GET with redirects and headers
local Curl = require("wardlib.app.curl").Curl
-- curl -s -S -L -H 'Accept: */*' https://example.com
local cmd = Curl.get("https://example.com", {
silent = true,
show_error = true,
location = true,
header = "Accept: */*",
})
Download to a file
local Curl = require("wardlib.app.curl").Curl
-- curl -L -o out.bin https://example.com/file
local cmd = Curl.download("https://example.com/file", "out.bin")
POST form-encoded data
local Curl = require("wardlib.app.curl").Curl
-- curl -X POST -d 'a=1&b=2' https://example.com/api
local cmd = Curl.post("https://example.com/api", "a=1&b=2")
Capture output and parse JSON
local Curl = require("wardlib.app.curl").Curl
local out = require("wardlib.tools.out")
local res = Curl.get("https://api.github.com/repos/neovim/neovim", {
silent = true,
show_error = true,
fail = true,
header = "Accept: application/vnd.github+json",
}):output()
local obj = out.res(res):label("curl github api"):json()
-- obj.full_name, obj.stargazers_count, ...
dconf
dconf is a low-level configuration system used by many desktop components
(notably GNOME).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Dconf = require("wardlib.app.dconf").Dconf
Notes
- Keys must start with
/and must not end with/. - Directories must start with
/and must end with/. Dconf.write(...)encodes common Lua values into GVariant literals; for complex values, useDconf.raw("<gvariant>").
API
Dconf.raw(value)
Marks a raw GVariant literal that will be passed through without encoding.
Dconf.encode(value)
Encodes common Lua primitives into GVariant literals:
string-> quoted string (single quotes with minimal escaping)boolean->true/falsenumber-> number literalDconf.raw(...)-> passed through as-is
Dconf.read(key)
Builds: dconf read <key>
Dconf.write(key, value)
Builds: dconf write <key> <gvariant>
Dconf.reset(key_or_dir, opts)
Builds: dconf reset [-f] <path>
If opts.force=true, the path must be a directory (end with /).
Dconf.list(dir)
Builds: dconf list <dir>
Dconf.dump(dir)
Builds: dconf dump <dir>
Dconf.load(dir, data)
Builds: dconf load <dir>
If data is provided, it is attached as stdin when possible.
All functions return a ward.process.cmd(...) object.
Examples
Read and parse a value
local Dconf = require("wardlib.app.dconf").Dconf
local out = require("wardlib.tools.out")
local res = Dconf.read("/org/gnome/desktop/interface/gtk-theme"):output()
local value = out.res(res):label("dconf read gtk-theme"):trim():line()
-- value is a GVariant literal, e.g. 'Adwaita-dark'
Write a string (auto-encoding)
local Dconf = require("wardlib.app.dconf").Dconf
-- dconf write /org/gnome/desktop/interface/gtk-theme 'Adwaita-dark'
Dconf.write("/org/gnome/desktop/interface/gtk-theme", "Adwaita-dark"):run()
Write a raw GVariant literal
local Dconf = require("wardlib.app.dconf").Dconf
-- dconf write /org/example/raw [1, 2, 3]
Dconf.write("/org/example/raw", Dconf.raw("[1, 2, 3]")):run()
Reset a subtree
local Dconf = require("wardlib.app.dconf").Dconf
-- dconf reset -f /org/example/
Dconf.reset("/org/example/", { force = true }):run()
Dump and restore
local Dconf = require("wardlib.app.dconf").Dconf
local out = require("wardlib.tools.out")
local dump = out.cmd(Dconf.dump("/org/example/")):label("dconf dump"):text()
-- Load back (stdin)
Dconf.load("/org/example/", dump):run()
dmenu
dmenu is a simple X11 menu / launcher that reads newline-separated entries
from stdin and prints the selected entry to stdout.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Dmenu = require("wardlib.app.dmenu").Dmenu
API
Dmenu.bin
Executable name or path (default: "dmenu").
Dmenu.menu(opts)
Builds: dmenu <opts...>
Returns a ward.Cmd.
Options
DmenuOpts
Flags:
bottom: boolean?→-bfast: boolean?→-finsensitive: boolean?→-i
Values:
lines: number?→-l <n>monitor: number?→-m <n>prompt: string?→-p <text>font: string?→-fn <font>normal_bg: string?→-nb <color>normal_fg: string?→-nf <color>selected_bg: string?→-sb <color>selected_fg: string?→-sf <color>windowid: string?→-w <id>
Other:
extra: string[]?→ extra argv appended after modeled options
Examples
Run dmenu with a prompt
local Dmenu = require("wardlib.app.dmenu").Dmenu
-- Feed items via stdin using ward.process APIs.
local cmd = Dmenu.menu({ prompt = "Run:" })
-- Example only: how you provide stdin depends on your ward version.
-- Typically you'd do: cmd:input("..."):output() or similar.
dnf
dnf is the package manager for Fedora and many RHEL-family distributions.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Dnf = require("wardlib.app.dnf").Dnf
Running with elevated privileges
Most dnf operations require root. Instead of passing { sudo = true } to this
module (not supported), use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Dnf = require("wardlib.app.dnf").Dnf
-- sudo -n dnf -y upgrade
w.with(w.middleware.sudo(), Dnf.upgrade(nil, { assume_yes = true })):run()
API
Dnf.install(pkgs, opts)
Builds: dnf <opts...> install <pkgs...>
Dnf.remove(pkgs, opts)
Builds: dnf <opts...> remove <pkgs...>
Dnf.update(pkgs, opts)
Builds: dnf <opts...> update [pkgs...]
If pkgs is nil, updates all packages.
Dnf.upgrade(pkgs, opts)
Builds: dnf <opts...> upgrade [pkgs...]
Dnf.autoremove(opts)
Builds: dnf <opts...> autoremove
Dnf.makecache(opts)
Builds: dnf <opts...> makecache
Dnf.search(term, opts)
Builds: dnf <opts...> search <term>
Dnf.info(pkgs, opts)
Builds: dnf <opts...> info <pkgs...>
Dnf.cmd(subcmd, argv, opts)
Generic helper for dnf <opts...> <subcmd> [argv...].
Dnf.raw(argv, opts)
Builds: dnf <opts...> <argv...>
Options (DnfCommonOpts)
Modeled fields:
- Non-interactive:
assume_yes (-y),assume_no (-n)(mutually exclusive) - Logging:
quiet (-q),verbose (-v) - Metadata/control:
refresh (--refresh),cacheonly (-C) - Solver:
best (--best),allowerasing (--allowerasing),skip_broken (--skip-broken) - Verification:
nogpgcheck (--nogpgcheck) - Targeting:
releasever (--releasever=...),installroot (--installroot=...) - Repos:
enable_repo (--enablerepo=...),disable_repo (--disablerepo=...) - Escape hatch:
extra
Examples
local Dnf = require("wardlib.app.dnf").Dnf
-- dnf -y --refresh install git ripgrep
local cmd1 = Dnf.install({ "git", "ripgrep" }, { assume_yes = true, refresh = true })
-- sudo -n dnf -y upgrade
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), Dnf.upgrade(nil, { assume_yes = true })):run()
-- dnf --enablerepo=updates search kernel
local cmd3 = Dnf.search("kernel", { enable_repo = "updates" })
docker
docker is a container runtime and image management CLI.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Docker = require("wardlib.app.docker").Docker
Running with elevated privileges
Many Linux distributions allow non-root access to Docker via the docker group.
If you need to run docker under privilege escalation,
use wardlib.tools.with middleware.
local with = require("wardlib.tools.with")
local Docker = require("wardlib.app.docker").Docker
with.with(with.middleware.sudo(), function()
Docker.ps({ all = true }):run()
end)
API
Docker.cmd(subcmd, argv)
Builds: docker <subcmd> [argv...]
Docker.run(image, cmdline, opts)
Builds: docker run <opts...> <image> [cmd...]
Docker.exec(container, cmdline, opts)
Builds: docker exec <opts...> <container> [cmd...]
Docker.build(context, opts)
Builds: docker build <opts...> <context>
Docker.pull(image, opts) / Docker.push(image, opts)
Builds: docker pull <image> and docker push <image>.
Docker.ps(opts) / Docker.images(opts)
Builds: docker ps <opts...> and docker images <opts...>.
Docker.logs(container, opts)
Builds: docker logs <opts...> <container>.
Lifecycle helpers
Docker.rm(containers, opts)→docker rm ...Docker.rmi(images, opts)→docker rmi ...Docker.start(containers, opts)→docker start ...Docker.stop(containers, opts)→docker stop ...Docker.restart(containers, opts)→docker restart ...Docker.inspect(targets, opts)→docker inspect ...Docker.tag(source, target, opts)→docker tag ...
Auth helpers
Docker.login(registry, opts)→docker login ...- For security, this wrapper does not accept a password string.
Prefer
password_stdin=trueand supply stdin via your own pipeline/middleware.
- For security, this wrapper does not accept a password string.
Prefer
Docker.logout(registry, opts)→docker logout ...
Docker.raw(argv, opts)
Builds: docker <argv...>
Use this when you need a docker feature not modeled in the structured option types.
All functions return a ward.process.cmd(...) object.
Options
Repeatable fields (like env, publish, volume, filter, etc.) accept string|string[].
DockerRunOpts
- Session:
detach(-d),interactive(-i),tty(-t),rm(--rm) - Identity:
name(--name),hostname(--hostname),workdir(-w),user(-u),entrypoint(--entrypoint) - Environment:
env(-e, repeatable),env_file(--env-file, repeatable) - Networking/storage:
publish(-p, repeatable),volume(-v, repeatable),network(--network),add_host(--add-host, repeatable) - Labels/caps:
label(--label, repeatable),privileged(--privileged),cap_add(--cap-add, repeatable),cap_drop(--cap-drop, repeatable) - Platform/pull:
platform(--platform),pull(--pull <policy>) - Escape hatch:
extra
DockerExecOpts
detach(-d),interactive(-i),tty(-t)user(-u),workdir(-w)env(-e, repeatable)- Escape hatch:
extra
DockerBuildOpts
tag(-t, repeatable),file(-f)build_arg(--build-arg, repeatable)target(--target),platform(--platform)pull(--pull),no_cache(--no-cache),progress(--progress)- Escape hatch:
extra
DockerPsOpts
all(-a),quiet(-q),no_trunc(--no-trunc),latest(-l),size(-s)last(-n <n>),format(--format <fmt>)filter(--filter, repeatable)- Escape hatch:
extra
DockerImagesOpts
all(-a),quiet(-q),no_trunc(--no-trunc),digests(--digests)format(--format <fmt>)filter(--filter, repeatable)- Escape hatch:
extra
DockerLogsOpts
follow(-f),timestamps(-t),details(--details)since(--since),until(--until),tail(--tail <n|all>)- Escape hatch:
extra
Other option types
DockerRmOpts:force(-f),volumes(-v),link(-l),extraDockerRmiOpts:force(-f),no_prune(--no-prune),extraDockerStopOpts:time(-t <seconds>),extraDockerInspectOpts:format(-f <format>),size(-s),type(--type),extraDockerLoginOpts:username(-u <user>),password_stdin(--password-stdin),extra
Examples
local Docker = require("wardlib.app.docker").Docker
-- docker run --rm -e A=1 -p 8080:80 alpine:3 sh -lc 'echo ok'
local cmd1 = Docker.run("alpine:3", { "sh", "-lc", "echo ok" }, {
rm = true,
env = "A=1",
publish = "8080:80",
})
-- docker build -t myimg:dev -f Dockerfile --build-arg A=1 .
local cmd2 = Docker.build(".", {
tag = "myimg:dev",
file = "Dockerfile",
build_arg = "A=1",
})
-- docker ps -a --filter status=running
local cmd3 = Docker.ps({ all = true, filter = "status=running" })
-- docker logs -f --tail 100 myctr
local cmd4 = Docker.logs("myctr", { follow = true, tail = 100 })
local out = require("wardlib.tools.out")
-- docker inspect returns JSON; parse it
local inspect_res = Docker.inspect("myctr"):output()
local info = out.res(inspect_res):label("docker inspect myctr"):json()
-- `info` is typically an array of objects
-- Run docker under sudo (if needed)
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
Docker.images({ all = true }):run()
end)
dunst
This module wraps two CLI tools used with the Dunst notification daemon:
dunstify– send notifications (client)dunstctl– control a runningdunstinstance
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Dunst = require("wardlib.app.dunst").Dunst
local DunstCtl = require("wardlib.app.dunst").DunstCtl
API
Dunst.notify(summary, opts)
Builds: dunstify <opts...> <summary> [body]
summary: notification title/summaryopts.body: optional body text (appended as a positional argument)
Dunst.close(id)
Builds: dunstify -C <id>
Dunst.capabilities()
Builds: dunstify --capabilities
Dunst.serverInfo()
Builds: dunstify --serverinfo
All functions return a ward.process.cmd(...) object.
DunstCtl.* (dunstctl)
These functions build
dunstctl <command> ....
DunstCtl.close()→dunstctl closeDunstCtl.closeAll()→dunstctl close-allDunstCtl.context()→dunstctl contextDunstCtl.historyPop([id])→dunstctl history-pop [id]DunstCtl.historyRm(id)→dunstctl history-rm <id>DunstCtl.historyClear()→dunstctl history-clearDunstCtl.isPaused()→dunstctl is-pausedDunstCtl.setPaused(v)→dunstctl set-paused <true|false|toggle>vmay be boolean (true/false) or a string ("true"|"false"|"toggle")
DunstCtl.getPauseLevel()→dunstctl get-pause-levelDunstCtl.setPauseLevel(level)→dunstctl set-pause-level <0..100>DunstCtl.count([scope])→dunstctl count [displayed|history|waiting]DunstCtl.action(notification_position)→dunstctl action <pos>DunstCtl.rule(rule_name, action)→dunstctl rule <name> <enable|disable|toggle>DunstCtl.rules([opts])→dunstctl rules [--json]opts.json = trueadds--json
DunstCtl.reload([files])→dunstctl reload [dunstrc ...]filesmay be a string orstring[]
DunstCtl.debug()→dunstctl debugDunstCtl.help()→dunstctl help
Options (DunstifyOptions)
- Content:
body - App identity:
app_name(-a <name>) - Urgency:
urgency(-u low|normal|critical) - Timeout:
timeout(-t <ms>) - Replace:
replaceId(-r <id>) - Hints:
hints(-h <hint>) - Actions:
action(-A <action>) - Icons:
icon(-i <icon>),raw_icon(-I <path>) - Category:
category(-c <category>) - Interaction:
block(-b) waits for a user action - Output:
printId(-p) prints notification id to stdout
Examples
local Dunst = require("wardlib.app.dunst").Dunst
local DunstCtl = require("wardlib.app.dunst").DunstCtl
local out = require("wardlib.tools.out")
-- Simple notification
Dunst.notify("Hello", { body = "World" }):run()
-- Notification that prints its id
local res = Dunst.notify("Build", {
body = "Finished",
urgency = "normal",
timeout = 2000,
printId = true,
}):output()
local id = tonumber(out.res(res):label("dunstify -p"):trim():line())
-- Close it later
Dunst.close(id):run()
-- Inspect server info / capabilities
local caps = out.cmd(Dunst.capabilities()):label("dunstify --capabilities"):lines()
local info = out.cmd(Dunst.serverInfo()):label("dunstify --serverinfo"):text()
-- Pause / resume notifications (dunstctl)
DunstCtl.setPaused(true):run()
-- ... later
DunstCtl.setPaused(false):run()
-- Query paused status
local paused = out.cmd(DunstCtl.isPaused()):label("dunstctl is-paused"):trim():text()
-- Typical output is "true" or "false" (depends on dunstctl version)
-- Pop last closed notification from history
DunstCtl.historyPop():run()
-- Reload config
DunstCtl.reload({ os.getenv("HOME") .. "/.config/dunst/dunstrc" }):run()
efibootmgr
Thin wrapper around efibootmgr (UEFI Boot Manager configuration).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
This module intentionally models only the most common flags;
use opts.extra for everything else.
Import
local E = require("wardlib.app.efibootmgr").Efibootmgr
Running with elevated privileges
Most efibootmgr operations require root. Use wardlib.tools.with middleware:
local with = require("wardlib.tools.with")
local E = require("wardlib.app.efibootmgr").Efibootmgr
with.with(with.middleware.sudo(), function()
E.list({ verbose = true }):run()
end)
API
E.cmd(opts)
Builds: efibootmgr <opts...>
E.list(opts)
Alias for E.cmd(opts).
E.set_bootnext(bootnum, opts) / E.delete_bootnext(opts)
Builds: efibootmgr -n XXXX and efibootmgr -N.
E.set_bootorder(order, opts) / E.delete_bootorder(opts)
Builds: efibootmgr -o 0001,0002,... and efibootmgr -O.
E.set_timeout(seconds, opts) / E.delete_timeout(opts)
Builds: efibootmgr -t <seconds> and efibootmgr -T.
E.delete(bootnum, opts)
Builds: efibootmgr -b XXXX -B.
E.create_entry(opts)
Convenience: sets opts.create=true and builds:
efibootmgr -c -d <disk> -p <part> -l <loader> -L <label> ...
All functions return a ward.process.cmd(...) object.
Options (EfibootmgrOpts)
Common fields:
- Binary:
bin(override executable name/path) - Output:
verbose(-v),quiet(-q) - Entry selection:
bootnum(-b XXXX) whereXXXXis a 4-hex-digit boot number - Entry state:
active(-a),inactive(-A) - Entry deletion:
delete_bootnum(-B) - Entry creation:
create(-c),create_only(-C) - Entry parameters:
disk(-d <disk>),part(-p <part>),loader(-l <loader>),label(-L <label>) - One-time next boot:
bootnext(-n XXXX),delete_bootnext(-N) - Boot order:
bootorder(-o ...),delete_bootorder(-O)bootorderaccepts a comma-separated string or an array of boot numbers.
- Timeout:
timeout(-t <sec>),delete_timeout(-T) - Other flags:
unicode(-u),write_signature(-w),remove_dups(-D),driver(-r),sysprep(-y) - Device path flags:
full_dev_path(--full-dev-path),file_dev_path(--file-dev-path) - Append extra loader args:
append_binary_args(-@ <file>; use-to read from stdin) - Escape hatch:
extra
Examples
List current configuration
local with = require("wardlib.tools.with")
local E = require("wardlib.app.efibootmgr").Efibootmgr
with.with(with.middleware.sudo(), function()
-- efibootmgr -v
E.list({ verbose = true }):run()
end)
Set BootNext (one-time next boot)
local with = require("wardlib.tools.with")
local E = require("wardlib.app.efibootmgr").Efibootmgr
with.with(with.middleware.sudo(), function()
-- efibootmgr -n 0004
E.set_bootnext(4):run()
-- efibootmgr -N
E.delete_bootnext():run()
end)
Create a boot entry
local with = require("wardlib.tools.with")
local E = require("wardlib.app.efibootmgr").Efibootmgr
with.with(with.middleware.sudo(), function()
E.create_entry({
disk = "/dev/sda",
part = 1,
loader = "\\\\EFI\\\\Linux\\\\grubx64.efi",
label = "Linux",
}):run()
end)
fd
fd is a simple, fast and user-friendly alternative to find.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Fd = require("wardlib.app.fd").Fd
API
Fd.search(pattern, paths, opts)
Builds: fd <opts...> [pattern] [paths...]
pattern:string|nil- If
nil, defaults to".". - If you want to match all entries explicitly, you may also pass
"".
- If
paths:string|string[]|nil- If
nil, fd searches the current directory.
- If
Fd.all(paths, opts)
Convenience for Fd.search(".", paths, opts).
Fd.raw(argv, opts)
Builds: fd <opts...> <argv...>
Use this when you need an fd feature not modeled in FdOpts.
Options (FdOpts)
Common fields:
- Ignore/visibility:
hidden (-H),no_ignore (-I),unrestricted (-u),no_ignore_vcs - Traversal:
follow (-L),max_depth (-d),min_depth (--min-depth),exact_depth (--exact-depth) - Match mode:
glob (-g),fixed_strings (-F) - Case:
case_sensitive (-s),ignore_case (-i) - Filtering:
type (-t),extension (-e),exclude (-E),size (-S),changed_within,changed_before - Output:
absolute_path (-a),full_path (-p),print0 (-0),quiet (-q),show_errors - Actions:
exec (-x),exec_batch (-X)(mutually exclusive) - Extra:
extra
Examples
local Fd = require("wardlib.app.fd").Fd
-- fd -e lua -e md --hidden -- "" .
local cmd1 = Fd.search("", ".", {
hidden = true,
extension = { "lua", "md" },
})
-- fd -t f -E node_modules -d 3 needle .
local cmd2 = Fd.search("needle", ".", {
type = "f",
exclude = "node_modules",
max_depth = 3,
})
-- fd -x echo {} (pass command tokens as array)
local cmd3 = Fd.search(".", nil, {
exec = { "echo", "{}" },
})
Parse results with wardlib.tools.out
local Fd = require("wardlib.app.fd").Fd
local out = require("wardlib.tools.out")
-- fd -t f -e lua --hidden --print0 needle .
local res = Fd.search("needle", ".", {
type = "f",
extension = "lua",
hidden = true,
print0 = true,
}):output()
-- Note: `print0` produces NUL-separated output. If you use `print0`, you must split on `\0`.
local raw = out.res(res):label("fd print0"):text()
local files = {}
for s in raw:gmatch("([^\0]+)\0") do files[#files + 1] = s end
feh
feh is an image viewer that is often used for setting wallpapers in X11 environments.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Feh = require("wardlib.app.feh").Feh
API
Feh.view(inputs, opts)
Builds: feh <opts...> [inputs...]
inputs:string|string[]|nil- If
nil,fehruns with only options.
- If
Feh.bg(image, opts)
Builds: feh <bg-opts...> <image>
Feh.bg_multi(images, opts)
Builds: feh <bg-opts...> <images...>
All functions return a ward.process.cmd(...) object.
Options
FehOpts
- View mode:
fullscreen(-F),borderless(-x) - Zoom/rotation:
keep_zoom_vp(--keep-zoom-vp),zoom(-Z),zoom_percent(--zoom <percent>),auto_rotate(--auto-rotate) - UI:
draw_filename(-d),caption_path(--caption-path),title(--title) - Geometry/timing:
geometry(-g),reload(--reload <sec>),slideshow_delay(-D <sec>) - Traversal:
recursive(-r) - Ordering:
sort(--sort <mode>),reverse(--reverse),randomize(--randomize) - Performance:
preload(--preload),cache_size(--cache-size <MB>) - Escape hatch:
extra
FehBgOpts
mode:"center"|"fill"|"max"|"scale"|"tile"(maps to--bg-*)no_fehbg:--no-fehbg- Escape hatch:
extra
Examples
View images fullscreen
local Feh = require("wardlib.app.feh").Feh
-- feh -F --randomize a.png b.png
local cmd = Feh.view({ "a.png", "b.png" }, {
fullscreen = true,
randomize = true,
})
Set wallpaper
local Feh = require("wardlib.app.feh").Feh
-- feh --bg-fill wall.png
Feh.bg("wall.png", { mode = "fill" }):run()
Set wallpaper without writing ~/.fehbg
local Feh = require("wardlib.app.feh").Feh
-- feh --bg-center --no-fehbg wall.png
Feh.bg("wall.png", { mode = "center", no_fehbg = true }):run()
Set multiple wallpapers (e.g. multi-monitor)
local Feh = require("wardlib.app.feh").Feh
-- feh --bg-scale left.png right.png
Feh.bg_multi({ "left.png", "right.png" }, { mode = "scale" }):run()
find
find walks directory trees and evaluates an expression against each entry.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
findvariants (GNU/BSD/BusyBox) differ. Useexpr/extra_exprto access unmodeled features.- Traversal mode flags
-P,-L,-Hmust be placed before the paths; this wrapper enforces that.
Import
local Find = require("wardlib.app.find").Find
API
Find.run(paths, expr, opts)
Builds: find [(-P|-L|-H)] <extra...> -- [paths...] <modeled-expr...> <expr...>
paths:string|string[]|nil- If
nil, defaults to{"."}.
- If
expr:string|string[]|nil- Additional expression tokens appended after the modeled expression.
opts:FindOpts|nil
Find.search(paths, opts)
Convenience for Find.run(paths, nil, opts).
Find.raw(argv, opts)
Builds: find <modeled-start-opts...> <argv...>
Use this when you need full control over parsing.
Options (FindOpts)
Modeled fields:
- Traversal mode:
follow_mode('P'|'L'|'H') - Common traversal controls:
maxdepth,mindepth,xdev,depth - Common tests:
type,name,iname,path,ipath,regex,iregex,size,user,group,perm,mtime,atime,ctime,newer,empty,readable,writable,executable - Action:
print0(otherwise find defaults to-print) - Escape hatches:
extra(before--) andextra_expr(after modeled expression)
Examples
local Find = require("wardlib.app.find").Find
-- find -- . -maxdepth 1 -type f -name '*.lua' -print0
local cmd1 = Find.search(".", {
maxdepth = 1,
type = "f",
name = "*.lua",
print0 = true,
})
-- find -L -- /var/log -xdev -name '*.log' -print
local cmd2 = Find.run("/var/log", { "-name", "*.log", "-print" }, {
follow_mode = "L",
xdev = true,
})
Parse output
local Find = require("wardlib.app.find").Find
local out = require("wardlib.tools.out")
-- find -- . -maxdepth 1 -type f -name '*.lua'
local files = out.cmd(Find.search(".", { maxdepth = 1, type = "f", name = "*.lua" }))
:label("find lua files")
:lines()
fuzzel
fuzzel is a Wayland-native application launcher. It also supports a --dmenu
mode compatible with dmenu-style pipelines.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Fuzzel = require("wardlib.app.fuzzel").Fuzzel
API
Fuzzel.bin
Executable name or path (default: "fuzzel").
Fuzzel.launcher(opts)
Builds: fuzzel <opts...>
Fuzzel.dmenu(opts)
Builds: fuzzel --dmenu <opts...>
Options
FuzzelOpts
config: string?→--config <file>output: string?→-o <output>font: string?→-f <font>prompt: string?→-p <prompt>prompt_only: string?→--prompt-only <prompt>hide_prompt: boolean?→--hide-promptplaceholder: string?→--placeholder <text>search: string?→--search <text>no_icons: boolean?→-Ianchor: string?→-a <anchor>lines: number?→-l <n>width: number?→-w <n>no_sort: boolean?→--no-sortextra: string[]?→ extra argv appended after modeled options
Examples
Simple launcher
local Fuzzel = require("wardlib.app.fuzzel").Fuzzel
Fuzzel.launcher({ prompt = "Run" }):output()
--dmenu mode
local Fuzzel = require("wardlib.app.fuzzel").Fuzzel
Fuzzel.dmenu({ placeholder = "Type..." }):output()
Git
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Status in a specific repo directory
local Git = require("wardlib.app.git").Git
local out = require("wardlib.tools.out")
-- Equivalent to: git -C /home/me/project status -s -b
local cmd = Git.status({
dir = "/home/me/project",
short = true,
branch = true,
})
local lines = out.cmd(cmd):label("git status"):lines()
-- lines is a string[] with each status line
Root of repository
local Git = require("wardlib.app.git").Git
local out = require("wardlib.tools.out")
-- Equivalent to: git -C /home/me/project rev-parse --show-toplevel
local root = out.cmd(Git.root({ dir = "/home/me/project" }))
:label("git rev-parse --show-toplevel")
:trim()
:line()
Check “is this a git work tree?”
Git.is_repo() is intentionally modeled as a command because many scripts want
exit-code semantics.
local Git = require("wardlib.app.git").Git
-- Equivalent to: git -C /home/me/project rev-parse --is-inside-work-tree
local res = Git.is_repo({ dir = "/home/me/project" }):output()
-- Conventionally, exit code 0 means "yes".
local is_repo = res.ok == true
Current branch name
Use git rev-parse --abbrev-ref HEAD and parse a single line.
local Git = require("wardlib.app.git").Git
local out = require("wardlib.tools.out")
local branch = out.cmd(Git.cmd("rev-parse", { "--abbrev-ref", "HEAD" }, { dir = "/home/me/project" }))
:label("git rev-parse --abbrev-ref HEAD")
:trim()
:line()
Clone (shallow + branch)
local Git = require("wardlib.app.git").Git
-- Equivalent to: git clone --depth 1 --branch main https://example.com/repo.git repo
local cmd = Git.clone("https://example.com/repo.git", "repo", {
depth = 1,
branch = "main",
})
Clone (recursive)
local Git = require("wardlib.app.git").Git
-- Equivalent to: git clone --recursive https://example.com/repo.git
local cmd = Git.clone("https://example.com/repo.git", nil, {
recursive = true,
})
Push with upstream
local Git = require("wardlib.app.git").Git
-- Equivalent to: git -C /home/me/project push -u origin main
local cmd = Git.push("origin", "main", {
dir = "/home/me/project",
upstream = true,
})
Pass-through extra arguments
extra is a direct append to the subcommand argv. Use it for flags you
don’t want to model yet.
local Git = require("wardlib.app.git").Git
-- Equivalent to: git -C /home/me/project status -s --ignored=matching
local cmd = Git.status({
dir = "/home/me/project",
short = true,
extra = { "--ignored=matching" },
})
-- Equivalent to: git push --force-with-lease origin main
local cmd2 = Git.push("origin", "main", {
dir = "/home/me/project",
extra = { "--force-with-lease" },
})
Use an explicit git binary
local Git = require("wardlib.app.git").Git
Git.bin = "/usr/bin/git" -- or another path
local cmd = Git.status({ dir = "/home/me/project", short = true })
grep
grep searches text for regular expressions (or fixed strings).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Grep = require("wardlib.app.grep").Grep
API
Grep.search(pattern, inputs, opts)
Builds: grep <opts...> -e <pattern>... [inputs...]
pattern:string|string[]- Always emitted using
-e <pattern>(possibly repeated), which avoids ambiguity when patterns start with-.
- Always emitted using
inputs:string|string[]|nil- If
nil, grep reads stdin.
- If
Grep.count_matches(pattern, inputs, opts)
Builds: grep <opts...> -c -e <pattern>... [inputs...]
Grep.list_files(pattern, inputs, opts)
Builds: grep <opts...> -l -e <pattern>... [inputs...]
Grep.raw(argv, opts)
Builds: grep <opts...> <argv...>
Use this when you need a flag or submode not modeled in GrepOpts.
Options (GrepOpts)
Core matching:
extended(-E),fixed(-F),perl(-P) — mutually exclusive.ignore_case(-i),word(-w),line(-x),invert(-v)
Output / selection:
count(-c),quiet(-q)line_number(-n)files_with_matches(-l),files_without_matches(-L)with_filename(-H),no_filename(-h) — mutually exclusive.
Recursion:
recursive(-r),recursive_follow(-R) — mutually exclusive.
Context and limits:
max_count(-m <n>)after_context(-A <n>),before_context(-B <n>)context(-C <n>) — mutually exclusive withafter_context/before_context.
Binary / NUL:
null(-Z),null_data(-z, GNU)text(-a),binary_without_match(-I)
GNU-specific convenience:
color(--color[=WHEN]):truemaps to--color=auto.include,exclude,exclude_dir: add--include=...,--exclude=...,--exclude-dir=....
Other:
extra: appended verbatim after modeled options.
Examples
local Grep = require("wardlib.app.grep").Grep
-- grep -E -i -n -A 2 --color=auto --include=*.txt -e foo -e bar a.txt b.txt
local cmd = Grep.search({ "foo", "bar" }, { "a.txt", "b.txt" }, {
extended = true,
ignore_case = true,
line_number = true,
after_context = 2,
color = true,
include = "*.txt",
})
-- grep -F -c -e needle file
local count = Grep.count_matches("needle", "file", { fixed = true })
Handling “no matches” (exit code 1)
Many grep modes exit with status 1 when no matches are found (which may be expected).
You can handle this by allowing failure and interpreting the output.
local Grep = require("wardlib.app.grep").Grep
local out = require("wardlib.tools.out")
local res = Grep.search("needle", "file", { fixed = true }):output()
local o = out.res(res):label("grep needle")
-- If you expect "no matches" to be ok:
local ok = o:allow_fail():ok()
local lines = o:allow_fail():lines()
– ok is true for exit code 0; if exit code is 1, lines will be empty. – If you want strict success for any non-zero exit, omit :allow_fail().
gsettings
gsettings is a CLI for reading and writing GSettings keys
(commonly used by GNOME and related components).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Gsettings = require("wardlib.app.gsettings").Gsettings
API
Gsettings.get(schema, key)
Builds: gsettings get <schema> <key>
Gsettings.set(schema, key, value)
Builds: gsettings set <schema> <key> <value>
The value is passed verbatim. Provide a valid GVariant string for the target type.
Gsettings.reset(schema, key)
Builds: gsettings reset <schema> <key>
Gsettings.list_keys(schema)
Builds: gsettings list-keys <schema>
Gsettings.list_schemas()
Builds: gsettings list-schemas
Gsettings.list_recursively(schema_or_path)
Builds: gsettings list-recursively [schema_or_path]
All functions return a ward.process.cmd(...) object.
Examples
Read a key
local Gsettings = require("wardlib.app.gsettings").Gsettings
local out = require("wardlib.tools.out")
-- gsettings get org.gnome.desktop.interface clock-show-date
local res = Gsettings.get("org.gnome.desktop.interface", "clock-show-date"):output()
local v = out.res(res):label("gsettings get"):trim():line()
-- Example output: true
Set a key
local Gsettings = require("wardlib.app.gsettings").Gsettings
-- gsettings set org.gnome.desktop.interface clock-show-date true
Gsettings.set("org.gnome.desktop.interface", "clock-show-date", "true"):run()
Reset a key
local Gsettings = require("wardlib.app.gsettings").Gsettings
-- gsettings reset org.gnome.desktop.interface clock-show-date
Gsettings.reset("org.gnome.desktop.interface", "clock-show-date"):run()
List keys for a schema
local Gsettings = require("wardlib.app.gsettings").Gsettings
local out = require("wardlib.tools.out")
-- gsettings list-keys org.gnome.desktop.interface
local keys = out.cmd(Gsettings.list_keys("org.gnome.desktop.interface"))
:label("gsettings list-keys")
:lines()
List schemas / recursively dump values
local Gsettings = require("wardlib.app.gsettings").Gsettings
-- gsettings list-schemas
local cmd1 = Gsettings.list_schemas()
-- gsettings list-recursively
local cmd2 = Gsettings.list_recursively()
-- gsettings list-recursively org.gnome.desktop.interface
local cmd3 = Gsettings.list_recursively("org.gnome.desktop.interface")
app.gzip
app.gzip is a thin command-construction wrapper around the gzip binary.
It returns ward.process.cmd(...) objects.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Gzip = require("wardlib.app.gzip").Gzip
Privilege escalation
gzip is typically unprivileged, but you may need elevated privileges when
working in root-owned directories. Use wardlib.tools.with middleware for
predictable privilege handling.
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
-- Example: compress a file in a root-owned directory
Gzip.compress("/var/log/app.log", { keep = true }):run()
end)
Options: GzipOpts
decompress: boolean?—-dkeep: boolean?—-kforce: boolean?—-fstdout: boolean?—-crecursive: boolean?—-rtest: boolean?—-tlist: boolean?—-lverbose: boolean?—-vquiet: boolean?—-qsuffix: string?—-S <suffix>level: integer?— compression level-1..-9extra: string[]?— appended after modeled options
Notes:
pathsmust be non-empty.- When
levelis set, it is validated as an integer in[1..9].
API
Gzip.run(paths, opts)
Builds: gzip <opts...> -- <paths...>
Gzip.run(paths: string|string[], opts: GzipOpts|nil) -> ward.Cmd
Gzip.compress(paths, opts)
Convenience for compression.
- Forces
decompress = false. - Builds the same shape as
Gzip.run.
Gzip.compress(paths: string|string[], opts: GzipOpts|nil) -> ward.Cmd
Gzip.decompress(paths, opts)
Convenience for decompression.
- Forces
decompress = true.
Gzip.decompress(paths: string|string[], opts: GzipOpts|nil) -> ward.Cmd
Gzip.raw(argv, opts)
Low-level escape hatch.
Builds: gzip <modeled-opts...> <argv...>
Gzip.raw(argv: string|string[], opts: GzipOpts|nil) -> ward.Cmd
Examples
Compress a file and keep the original
-- gzip -k -9 -- data.json
local cmd = Gzip.compress("data.json", { keep = true, level = 9 })
cmd:run()
Decompress a file (force overwrite)
-- gzip -d -f -- data.json.gz
Gzip.decompress("data.json.gz", { force = true }):run()
Stream compressed data to stdout
-- gzip -c -- data.json
local res = Gzip.run("data.json", { stdout = true }):output()
-- res.stdout contains compressed bytes
List gzip members and parse output
gzip -l emits a small table. Use wardlib.tools.out to consume it.
local out = require("wardlib.tools.out")
local lines = out.cmd(Gzip.run("data.json.gz", { list = true }))
:label("gzip -l data.json.gz")
:lines()
-- `lines` is an array of text rows; you can parse it further if needed.
app.ip
app.ip is a thin, command-construction wrapper around the iproute2 ip
binary. It returns ward.process.cmd(...) objects so you can execute them
using your preferred ward.process execution stratgy.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Privilege escalation
Many ip subcommands require elevated privileges (for example, link set,
addr add/del, route add/del, and most netns operations). Prefer
wardlib.tools.with so privilege escalation is explicit and scoped.
local Ip = require("wardlib.app.ip").Ip
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
-- ip link set dev eth0 up
Ip.link_set("eth0", { up = true }):run()
end)
Parsing JSON output
The wrapper does not parse output, but ip can emit JSON (-j).
Combine it with wardlib.tools.out for a predictable workflow.
local Ip = require("wardlib.app.ip").Ip
local out = require("wardlib.tools.out")
local addrs = out.cmd(Ip.raw({ "addr", "show" }, { json = true }))
:label("ip -j addr show")
:json()
-- `addrs` is an array of interfaces. Each entry contains `ifname` and `addr_info`.
-- You can filter it in Lua to find, for example, all IPv4 addresses.
Global options: IpOpts
These options are accepted anywhere an opts: IpOpts|nil argument is present.
Address-family selection
inet4: boolean?- Adds
-4(IPv4 only). - Mutually exclusive with
inet6.
- Adds
inet6: boolean?- Adds
-6(IPv6 only). - Mutually exclusive with
inet4.
- Adds
family: string?- Adds
-f <family>. - Typical values:
"inet","inet6","link","bridge","mpls".
- Adds
Namespace and batch execution
netns: string?- Adds
-n <netns>. - Executes the
ipcommand in the context of the specified network namespace.
- Adds
batch: string?- Adds
-b <file>. - Executes commands from a batch file.
- Adds
Output formatting
json: boolean?- Adds
-j. - Requests JSON output.
- Adds
pretty: boolean?- Adds
-p. - Pretty-prints JSON output (typically meaningful only with
json = true).
- Adds
oneline: boolean?- Adds
-o. - One-line output formatting.
- Adds
brief: boolean?- Adds
-br. - Brief output.
- Adds
details: boolean?- Adds
-d. - Show details.
- Adds
human: boolean?- Adds
-h. - Human-readable output.
- Adds
resolve: boolean?- Adds
-r. - Resolve names (where applicable).
- Adds
color: boolean|string?- If
true, adds-c. - If string, adds
-c <mode>. - Common modes:
"auto","always","never".
- If
Timestamps and statistics
timestamp: boolean?- Adds
-t.
- Adds
timestamp_short: boolean?- Adds
-ts.
- Adds
stats: boolean|number?- Adds
-s. - If
true, adds-sonce. - If a number
n, adds-sntimes (iproute2 uses repeated-sto increase verbosity).
- Adds
Extra arguments
extra: string[]?- Appended after all modeled global options.
- Use for global flags not explicitly modeled.
Methods
Ip.raw(argv, opts)
Build an ip command from an arbitrary argument vector.
Signature
Ip.raw(argv: string|string[], opts: IpOpts|nil) -> ward.Cmd
Semantics
- Builds:
ip <global-opts...> <argv...> - Use this for unmodeled
ipobjects/subcommands.
Example
-- ip -d link show dev eth0
local cmd = Ip.raw({ "link", "show", "dev", "eth0" }, { details = true })
Link operations
Ip.link_show(dev, opts)
Show links (interfaces).
Signature
Ip.link_show(dev: string|nil, opts: IpLinkShowOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> link show ...
Parameters
dev: string|nil- If provided, adds
dev <dev>.
- If provided, adds
Options: IpLinkShowOpts
Extends IpOpts and adds:
up: boolean?- Adds selector
up.
- Adds selector
master: string?- Adds
master <ifname>selector.
- Adds
vrf: string?- Adds
vrf <name>selector.
- Adds
type: string?- Adds
type <kind>selector.
- Adds
group: string|number?- Adds
group <group>selector.
- Adds
Examples
-- ip -br link show
local cmd1 = Ip.link_show(nil, { brief = true })
-- ip link show dev eth0 up
local cmd2 = Ip.link_show("eth0", { up = true })
Ip.link_set(dev, opts)
Modify link properties.
Signature
Ip.link_set(dev: string, opts: IpLinkSetOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> link set dev <dev> ...
Options: IpLinkSetOpts
Extends IpOpts and adds:
up: boolean?- Adds
up. - Mutually exclusive with
down.
- Adds
down: boolean?- Adds
down. - Mutually exclusive with
up.
- Adds
mtu: number?- Adds
mtu <n>.
- Adds
qlen: number?- Adds
txqueuelen <n>.
- Adds
name: string?- Adds
name <newname>.
- Adds
alias: string?- Adds
alias <text>.
- Adds
address: string?- Adds
address <lladdr>.
- Adds
broadcast: string?- Adds
broadcast <lladdr>.
- Adds
master: string?- Adds
master <ifname>.
- Adds
nomaster: boolean?- Adds
nomaster.
- Adds
set_netns: string?- Adds
netns <name>(moves interface to namespace).
- Adds
extra_after: string[]?- Appended after modeled
link setarguments.
- Appended after modeled
Examples
-- ip link set dev eth0 up mtu 1500
local cmd = Ip.link_set("eth0", { up = true, mtu = 1500 })
-- ip link set dev eth0 netns ns1
local cmd2 = Ip.link_set("eth0", { set_netns = "ns1" })
Address operations
Ip.addr_show(dev, opts)
Show interface addresses.
Signature
Ip.addr_show(dev: string|nil, opts: IpAddrShowOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> addr show ...
Parameters
dev: string|nil- If provided, adds
dev <dev>.
- If provided, adds
Options: IpAddrShowOpts
Extends IpOpts and adds:
up: boolean?- Adds selector
up.
- Adds selector
scope: string?- Adds
scope <scope>selector (e.g."global","link").
- Adds
label: string?- Adds
label <pattern>selector.
- Adds
to: string?- Adds
to <prefix>selector.
- Adds
Examples
-- ip -j -p addr show dev eth0
local cmd = Ip.addr_show("eth0", { json = true, pretty = true })
Ip.addr_add(addr, dev, opts)
Add an address to an interface.
Signature
Ip.addr_add(addr: string, dev: string, opts: IpAddrChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> addr add <addr> dev <dev> ...
Options: IpAddrChangeOpts
Extends IpOpts and adds:
label: string?- Adds
label <label>.
- Adds
broadcast: string?- Adds
broadcast <addr>.
- Adds
anycast: string?- Adds
anycast <addr>.
- Adds
scope: string?- Adds
scope <scope>.
- Adds
valid_lft: string|number?- Adds
valid_lft <time>(e.g."forever").
- Adds
preferred_lft: string|number?- Adds
preferred_lft <time>.
- Adds
noprefixroute: boolean?- Adds
noprefixroute.
- Adds
extra_after: string[]?- Appended after modeled addr arguments.
Example
-- ip addr add 192.0.2.10/24 dev eth0
local cmd = Ip.addr_add("192.0.2.10/24", "eth0")
Ip.addr_del(addr, dev, opts)
Remove an address from an interface.
Signature
Ip.addr_del(addr: string, dev: string, opts: IpAddrChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> addr del <addr> dev <dev> ...
Options are identical to Ip.addr_add (IpAddrChangeOpts).
Ip.addr_flush(dev, opts)
Flush addresses (optionally scoped/selective).
Signature
Ip.addr_flush(dev: string|nil, opts: IpAddrFlushOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> addr flush ...
Parameters
dev: string|nil- If provided, adds
dev <dev>.
- If provided, adds
Options: IpAddrFlushOpts
Extends IpOpts and adds:
scope: string?- Adds
scope <scope>selector.
- Adds
label: string?- Adds
label <pattern>selector.
- Adds
to: string?- Adds
to <prefix>selector.
- Adds
extra_after: string[]?- Appended after modeled flush selectors.
Route operations
Ip.route_show(opts)
Show routes.
Signature
Ip.route_show(opts: IpRouteShowOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> route show ...
Options: IpRouteShowOpts
Extends IpOpts and adds:
table: string|number?- Adds
table <id>selector.
- Adds
vrf: string?- Adds
vrf <name>selector.
- Adds
dev: string?- Adds
dev <ifname>selector.
- Adds
proto: string?- Adds
proto <proto>selector.
- Adds
scope: string?- Adds
scope <scope>selector.
- Adds
type: string?- Adds
type <type>selector.
- Adds
extra_after: string[]?- Appended after modeled selectors.
Ip.route_get(dst, opts)
Query kernel route to destination.
Signature
Ip.route_get(dst: string, opts: IpRouteGetOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> route get <dst> ...
Options: IpRouteGetOpts
Extends IpOpts and adds:
from: string?- Adds
from <addr>.
- Adds
iif: string?- Adds
iif <ifname>.
- Adds
oif: string?- Adds
oif <ifname>.
- Adds
vrf: string?- Adds
vrf <name>.
- Adds
mark: string|number?- Adds
mark <fwmark>.
- Adds
uid: string|number?- Adds
uid <uid>.
- Adds
extra_after: string[]?- Appended after modeled args.
Ip.route_add(dst, opts)
Add a route.
Signature
Ip.route_add(dst: string, opts: IpRouteChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> route add <dst> ...
Ip.route_replace(dst, opts)
Replace (or add) a route.
Signature
Ip.route_replace(dst: string, opts: IpRouteChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> route replace <dst> ...
Ip.route_del(dst, opts)
Delete a route.
Signature
Ip.route_del(dst: string, opts: IpRouteChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> route del <dst> ...
Options: IpRouteChangeOpts
Used by route_add, route_replace, and route_del.
Extends IpOpts and adds:
via: string?- Adds
via <addr>.
- Adds
dev: string?- Adds
dev <ifname>.
- Adds
src: string?- Adds
src <addr>.
- Adds
metric: number?- Adds
metric <n>.
- Adds
table: string|number?- Adds
table <id>.
- Adds
proto: string?- Adds
proto <proto>.
- Adds
scope: string?- Adds
scope <scope>.
- Adds
type: string?- Adds
type <type>.
- Adds
onlink: boolean?- Adds
onlink.
- Adds
mtu: number?- Adds
mtu <n>.
- Adds
advmss: number?- Adds
advmss <n>.
- Adds
initcwnd: number?- Adds
initcwnd <n>.
- Adds
initrwnd: number?- Adds
initrwnd <n>.
- Adds
realm: string?- Adds
realm <realm>.
- Adds
preference: string?- Adds
pref <pref>(common for IPv6:"low","medium","high").
- Adds
extra_after: string[]?- Appended after modeled route arguments.
Example
-- ip -4 route add default via 192.0.2.1 dev eth0 metric 100 table 100
local cmd = Ip.route_add("default", {
inet4 = true,
via = "192.0.2.1",
dev = "eth0",
metric = 100,
table = 100,
})
Neighbor operations
Ip.neigh_show(dev, opts)
Show neighbor table entries.
Signature
Ip.neigh_show(dev: string|nil, opts: IpNeighShowOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> neigh show ...
Options: IpNeighShowOpts
Extends IpOpts and adds:
nud: string?- Adds
nud <state>selector.
- Adds
proxy: boolean?- Adds
proxyselector.
- Adds
router: boolean?- Adds
routerselector.
- Adds
extra_after: string[]?- Appended after modeled selectors.
Ip.neigh_add(dst, lladdr, dev, opts)
Add a neighbor entry.
Signature
Ip.neigh_add(dst: string, lladdr: string, dev: string, opts: IpNeighChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> neigh add <dst> dev <dev> lladdr <lladdr> ...
Ip.neigh_del(dst, lladdr, dev, opts)
Delete a neighbor entry.
Signature
Ip.neigh_del(dst: string, lladdr: string|nil, dev: string, opts: IpNeighChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> neigh del <dst> dev <dev> [lladdr <lladdr>] ...
Ip.neigh_flush(dev, opts)
Flush neighbor entries.
Signature
Ip.neigh_flush(dev: string|nil, opts: IpNeighFlushOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> neigh flush ...
Options: IpNeighChangeOpts
Used by neigh_add and neigh_del.
Extends IpOpts and adds:
nud: string?- Adds
nud <state>.
- Adds
router: boolean?- Adds
router.
- Adds
proxy: boolean?- Adds
proxy.
- Adds
extra_after: string[]?- Appended after modeled args.
Options: IpNeighFlushOpts
Extends IpOpts and adds:
nud: string?- Adds
nud <state>selector.
- Adds
proxy: boolean?- Adds
proxyselector.
- Adds
extra_after: string[]?- Appended after modeled selectors.
Rule operations
Ip.rule_show(opts)
Show policy routing rules.
Signature
Ip.rule_show(opts: IpRuleShowOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> rule show ...
Options: IpRuleShowOpts
Extends IpOpts and adds:
table: string|number?- Adds
table <id>selector.
- Adds
extra_after: string[]?- Appended after modeled selectors.
Ip.rule_add(opts)
Add a policy routing rule.
Signature
Ip.rule_add(opts: IpRuleChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> rule add ...
Ip.rule_del(opts)
Delete a policy routing rule.
Signature
Ip.rule_del(opts: IpRuleChangeOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> rule del ...
Options: IpRuleChangeOpts
Used by rule_add and rule_del.
Extends IpOpts and adds:
priority: number?- Adds
priority <n>.
- Adds
from: string?- Adds
from <prefix>.
- Adds
to: string?- Adds
to <prefix>.
- Adds
iif: string?- Adds
iif <ifname>.
- Adds
oif: string?- Adds
oif <ifname>.
- Adds
fwmark: string|number?- Adds
fwmark <mark>.
- Adds
table: string|number?- Adds
table <id>.
- Adds
lookup: string|number?- Alias for
table. Iftableis not set,lookupis used.
- Alias for
suppress_prefixlength: number?- Adds
suppress_prefixlength <n>.
- Adds
uidrange: string?- Adds
uidrange <start>-<end>.
- Adds
extra_after: string[]?- Appended after modeled args.
Namespace operations
Ip.netns_list(opts)
List network namespaces.
Signature
Ip.netns_list(opts: IpOpts|nil) -> ward.Cmd
Generated command
ip <global-opts...> netns list
Ip.netns_add(name, opts)
Create a network namespace.
Signature
Ip.netns_add(name: string, opts: IpOpts|nil) -> ward.Cmd
Generated command
ip <global-opts...> netns add <name>
Ip.netns_del(name, opts)
Delete a network namespace.
Signature
Ip.netns_del(name: string, opts: IpOpts|nil) -> ward.Cmd
Generated command
ip <global-opts...> netns del <name>
Ip.netns_exec(name, argv, opts)
Execute a command inside a network namespace.
Signature
Ip.netns_exec(name: string, argv: string|string[], opts: IpOpts|nil) -> ward.Cmd
Generated command
ip <global-opts...> netns exec <name> <argv...>
Example
-- ip netns exec ns1 ip addr
local cmd = Ip.netns_exec("ns1", { "ip", "addr" })
Monitor operations
Ip.monitor(objects, opts)
Monitor changes in kernel networking objects.
Signature
Ip.monitor(objects: string|string[]|nil, opts: IpOpts|nil) -> ward.Cmd
Generated command
- Base:
ip <global-opts...> monitor [objects...]
Parameters
objects: string|string[]|nil- If
nil, usesip monitordefault set. - If set, each object is appended (e.g.
"link","addr","route").
- If
Example
-- ip -o monitor link addr
local cmd = Ip.monitor({ "link", "addr" }, { oneline = true })
app.jq
app.jq is a thin command-construction wrapper around the jq binary.
It returns ward.process.cmd(...) objects.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Jq = require("wardlib.app.jq").Jq
Options: JqOpts
Input
null_input: boolean?—-n(use null input)raw_input: boolean?—-R(read raw strings, not JSON)slurp: boolean?—-s(read all inputs into a single array)
Output formatting
compact_output: boolean?—-craw_output: boolean?—-rjoin_output: boolean?—-jsort_keys: boolean?—-Smonochrome_output: boolean?—-Mcolor_output: boolean?—-Cexit_status: boolean?—-e(set exit status based on output)ascii_output: boolean?—-atab: boolean?—--tabindent: integer?—--indent <n>(validated as integer>= 0)
Notes:
color_outputandmonochrome_outputare mutually exclusive.
Variables
All variable maps are repeatable and are emitted in stable-sorted order for predictable argv.
arg: table<string,string>?—--arg <name> <value>argjson: table<string,string>?—--argjson <name> <json>slurpfile: table<string,string>?—--slurpfile <name> <file>rawfile: table<string,string>?—--rawfile <name> <file>
Variable names are validated as identifiers matching: ^[A-Za-z_][A-Za-z0-9_]*$.
Extra
extra: string[]?— appended after modeled options (before the filter)
API
Jq.eval(filter, inputs, opts)
Builds: jq <opts...> -- <filter> [inputs...]
Jq.eval(filter: string|nil, inputs: string|string[]|nil, opts: JqOpts|nil) -> ward.Cmd
Semantics:
- If
filterisnil, it defaults to".". - The wrapper always emits
--before the filter to avoid ambiguity when a filter starts with-. - If
inputsisnil, jq reads stdin.
Jq.eval_file(file, inputs, opts)
Builds: jq <opts...> -f <file> [inputs...]
Jq.eval_file(file: string, inputs: string|string[]|nil, opts: JqOpts|nil) -> ward.Cmd
Jq.eval_stdin(filter, data, opts)
Convenience over Jq.eval(filter, nil, opts) that attaches data to stdin.
Jq.eval_stdin(filter: string|nil, data: string, opts: JqOpts|nil) -> ward.Cmd
Jq.raw(argv, opts)
Low-level escape hatch.
Builds: jq <opts...> <argv...>
Jq.raw(argv: string|string[], opts: JqOpts|nil) -> ward.Cmd
Examples
Extract a field as a string
local out = require("wardlib.tools.out")
-- jq -r -- '.name' data.json
local name = out.cmd(Jq.eval(".name", "data.json", { raw_output = true }))
:label("jq -r .name data.json")
:trim()
:line()
Filter and print multiple values
-- jq -c -- '.items[] | select(.enabled) | .id' data.json
local res = Jq.eval(".items[] | select(.enabled) | .id", "data.json", { compact_output = true }):output()
-- res.stdout contains newline-delimited JSON scalars (strings/numbers)
Use variables
-- jq --arg key value -- '.[$key]' data.json
local cmd = Jq.eval(".[$key]", "data.json", { arg = { key = "value" } })
Feed JSON via stdin
local out = require("wardlib.tools.out")
-- jq -c -- '.'
local json = out.cmd(Jq.eval_stdin(".", "{\"a\": 1}", { compact_output = true }))
:label("jq stdin")
:text()
app.kill
app.kill provides thin command-construction wrappers around common process
termination utilities:
kill— signal processes by PIDkillall— signal processes by namepkill— signal processes by pattern
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Kill = require("wardlib.app.kill").Kill
Privilege escalation
Signaling processes owned by other users (or PID namespaces) may require
elevated privileges. Prefer wardlib.tools.with to make privilege escalation
explicit and scoped.
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
Kill.pid(1234, "TERM"):run()
end)
Types
Signal—string|number- Examples:
"TERM","SIGKILL",9.
- Examples:
Options
KillOpts
signal: Signal?— adds-s <signal>list: boolean?—-l(list signals; ignores pids)table: boolean?—-L(list signals in a table; not available on all implementations)extra: string[]?— appended after modeled options
KillallOpts
signal: Signal?—-s <sig>exact: boolean?—-eexact matchignore_case: boolean?—-Iignore caseinteractive: boolean?—-iask before killingwait: boolean?—-wwait for processes to dieregexp: boolean?—-rinterpret names as regexuser: string?—-u <user>verbose: boolean?—-vquiet: boolean?—-qextra: string[]?— appended after modeled options
PkillOpts
signal: Signal?— added as-<sig>(compact pkill form)full: boolean?—-fmatch full command lineexact: boolean?—-xmatch whole namenewest: boolean?—-nselect newestoldest: boolean?—-oselect oldestparent: number?—-P <ppid>group: number?—-g <pgrp>session: number?—-s <sid>terminal: string?—-t <tty>user: string?—-u <user>uid: number?—-U <uid>euid: number?—-e <euid>(procps)invert: boolean?—-vinvert matchcount: boolean?—-ccount matcheslist_name: boolean?—-llist pid and namelist_full: boolean?—-alist full command line (procps)delimiter: string?—-d <delim>(procps)extra: string[]?— appended after modeled options
API
Kill.kill(pids, opts)
Construct a kill command.
Builds: kill <opts...> [pids...]
Kill.kill(pids: number|number[]|string|string[]|nil, opts: KillOpts|nil) -> ward.Cmd
Notes:
- If
pidsisnil, the command is built with only options (useful for-l).
Kill.killall(names, opts)
Builds: killall <opts...> [names...]
Kill.killall(names: string|string[]|nil, opts: KillallOpts|nil) -> ward.Cmd
Kill.pkill(pattern, opts)
Builds: pkill <opts...> [pattern]
Kill.pkill(pattern: string|nil, opts: PkillOpts|nil) -> ward.Cmd
Convenience helpers
Kill.pid(pid, sig)—kill -s <sig> <pid>Kill.by_name(name, sig)—killall -s <sig> <name>Kill.by_pattern(pattern, sig, full)—pkill [-f] -<sig> <pattern>
Examples
Kill a PID with SIGTERM
-- kill -s TERM 123
Kill.pid(123, "TERM"):run()
Kill multiple PIDs
-- kill -s KILL 100 101 102
Kill.kill({ 100, 101, 102 }, { signal = "KILL" }):run()
Kill by process name
-- killall -s 9 firefox
Kill.by_name("firefox", 9):run()
Kill by pattern (full command line)
-- pkill -KILL -f "ssh .* -N"
Kill.by_pattern("ssh .* -N", "KILL", true):run()
List available signals and parse output
local out = require("wardlib.tools.out")
local lines = out.cmd(Kill.kill(nil, { list = true }))
:label("kill -l")
:lines()
-- `lines` contains the signal list; format depends on kill implementation.
Count matching processes with pkill -c
local out = require("wardlib.tools.out")
-- pkill -c -f 'ssh .* -N'
local n = out.cmd(Kill.pkill("ssh .* -N", { count = true, full = true }))
:label("pkill -c -f ...")
:trim()
:line()
-- `n` is a string; convert to number if needed.
app.ls
app.ls is a thin command-construction wrapper around the ls binary.
It returns ward.process.cmd(...) objects.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Ls = require("wardlib.app.ls").Ls
Options: LsOpts
all: boolean?—-aalmost_all: boolean?—-A(mutually exclusive withall)long: boolean?—-lhuman: boolean?—-h(GNU; typically used with-l)classify: boolean?—-Fone_per_line: boolean?—-1recursive: boolean?—-Rdirectory: boolean?—-d(list directories themselves, not contents)reverse: boolean?—-r
Sorting (mutually exclusive):
sort_time: boolean?—-tsort_size: boolean?—-S(GNU)no_sort: boolean?—-U(BSD); GNU uses-f(useextrafor portability)
GNU-only formatting:
color: 'auto'|'always'|'never'?—--color=<mode>time_style: string?—--time-style=<style>
Extra:
extra: string[]?— appended after modeled options
API
Ls.list(paths, opts)
List directory contents.
Builds: ls <opts...> -- [paths...]
Ls.list(paths: string|string[]|nil, opts: LsOpts|nil) -> ward.Cmd
Notes:
- If
pathsisnil, defaults to{ "." }.
Ls.raw(argv, opts)
Low-level escape hatch.
Builds: ls <modeled-opts...> <argv...>
Ls.raw(argv: string|string[], opts: LsOpts|nil) -> ward.Cmd
Examples
Long listing with human-readable sizes
-- ls -lh -- /var/log
Ls.list("/var/log", { long = true, human = true }):run()
List one entry per line and parse output
local out = require("wardlib.tools.out")
local files = out.cmd(Ls.list(".", { one_per_line = true }))
:label("ls -1")
:lines()
Recursive listing
-- ls -R -- ./src
Ls.list("./src", { recursive = true }):run()
Use extra flags for platform-specific behavior
-- GNU: ls -f (do not sort)
Ls.list(".", { extra = { "-f" } }):run()
-- BSD/macOS color flags differ; use extra for portability as needed.
app.lsblk
app.lsblk is a thin command-construction wrapper around the util-linux
lsblk binary. It returns ward.process.cmd(...) objects.
lsblk supports JSON output (-J), and this wrapper models that flag.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Lsblk = require("wardlib.app.lsblk").Lsblk
Privilege escalation
Most lsblk listing operations are unprivileged, but some environments
restrict access to block-device metadata. If you need elevation, use
wardlib.tools.with.
local with = require("wardlib.tools.with")
local cmd = Lsblk.list(nil, { json = true })
local data = with.with(with.middleware.sudo(), cmd):output()
Options: LsblkOpts
json: boolean?—-J/--jsonoutput: string|string[]?—-o <cols>(string or array joined by commas)bytes: boolean?—-bpaths: boolean?—-pfs: boolean?—-fall: boolean?—-anodeps: boolean?—-dlist: boolean?—-lraw: boolean?—-rnoheadings: boolean?—-nsort: string?—--sort <col>tree: boolean?—--treeextra: string[]?— appended after modeled options
API
Lsblk.list(devices, opts)
Construct an lsblk command.
Builds: lsblk <opts...> [devices...]
Lsblk.list(devices: string|string[]|nil, opts: LsblkOpts|nil) -> ward.Cmd
Notes:
- If
devicesisnil,lsblkenumerates all block devices.
Examples
Parse JSON output
local out = require("wardlib.tools.out")
local data = out.cmd(Lsblk.list(nil, { json = true }))
:label("lsblk -J")
:json()
-- util-linux typically returns: { blockdevices = [...] }
local devs = data.blockdevices or {}
Select specific columns
-- lsblk -o NAME,SIZE,TYPE,MOUNTPOINT -J
local out = require("wardlib.tools.out")
local data = out.cmd(Lsblk.list(nil, {
json = true,
output = { "NAME", "SIZE", "TYPE", "MOUNTPOINT" },
}))
:label("lsblk -o ... -J")
:json()
Raw, no headings
-- lsblk -nrp -o NAME,SIZE
Lsblk.list(nil, { noheadings = true, raw = true, paths = true, output = { "NAME", "SIZE" } }):run()
app.mkdir
app.mkdir is a thin command-construction wrapper around the mkdir binary.
It returns ward.process.cmd(...) objects.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Mkdir = require("wardlib.app.mkdir").Mkdir
Options: MkdirOpts
parents: boolean?—-pverbose: boolean?—-vmode: string?—-m <mode>dry_run: boolean?—--dry-run(GNU)extra: string[]?— appended after modeled options
API
Mkdir.make(paths, opts)
Create one or more directories.
Builds: mkdir <opts...> -- <paths...>
Mkdir.make(paths: string|string[], opts: MkdirOpts|nil) -> ward.Cmd
Mkdir.raw(argv, opts)
Low-level escape hatch.
Builds: mkdir <modeled-opts...> <argv...>
Mkdir.raw(argv: string|string[], opts: MkdirOpts|nil) -> ward.Cmd
Examples
Create nested directories
-- mkdir -p -- /var/lib/myapp/data
Mkdir.make("/var/lib/myapp/data", { parents = true }):run()
Set directory mode
-- mkdir -m 0750 -- /var/lib/myapp
Mkdir.make("/var/lib/myapp", { mode = "0750" }):run()
Dry-run (GNU)
-- mkdir --dry-run -p -- ./a/b
Mkdir.make("./a/b", { dry_run = true, parents = true }):run()
app.mkfs
app.mkfs is a thin command-construction wrapper around mkfs and
mkfs.<fstype> frontends.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
The wrapper prefers mkfs.<fstype> when present in PATH; otherwise it falls
back to mkfs -t <fstype>.
Filesystem-specific flags are intentionally not modeled. Use
opts.extra.
Import
local Mkfs = require("wardlib.app.mkfs").Mkfs
Privilege escalation and safety
Formatting filesystems is destructive and almost always requires elevated privileges.
Use wardlib.tools.with middleware so privilege escalation is explicit and
scoped:
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
-- mkfs.ext4 /dev/sdb1
Mkfs.ext4("/dev/sdb1"):run()
end)
Options: MkfsOpts
bin: string?- Override the binary (name or absolute path).
- When set, it is used directly and no
-t <fstype>is added.
extra: string[]?- Extra args appended before the device.
- Use this for filesystem-specific flags (for example, labels, force flags, etc.).
API
Mkfs.format(fstype, device, opts)
Format a device with an explicit filesystem type.
If mkfs.<fstype> exists in PATH, it is used directly. Otherwise:
- Builds:
mkfs -t <fstype> <extra...> <device>
Mkfs.format(fstype: string, device: string, opts: MkfsOpts|nil) -> ward.Cmd
Convenience helpers
These call Mkfs.format(...) with the corresponding fstype:
Mkfs.ext4(device, opts)Mkfs.xfs(device, opts)Mkfs.btrfs(device, opts)Mkfs.vfat(device, opts)Mkfs.f2fs(device, opts)
Examples
Create an ext4 filesystem with extra flags
local with = require("wardlib.tools.with")
-- mkfs.ext4 -F -L data /dev/sdb1
with.with(with.middleware.sudo(), function()
Mkfs.ext4("/dev/sdb1", { extra = { "-F", "-L", "data" } }):run()
end)
Use the generic formatter
-- mkfs -t xfs -f /dev/sdb2
Mkfs.format("xfs", "/dev/sdb2", { extra = { "-f" } })
Force a specific binary
This bypasses the mkfs.<fstype> / mkfs -t selection logic.
-- /usr/sbin/mkfs.ext4 -F /dev/sdb1
Mkfs.format("ext4", "/dev/sdb1", { bin = "/usr/sbin/mkfs.ext4", extra = { "-F" } })
app.mount
app.mount provides thin command-construction wrappers around util-linux
mount and umount.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
This module models a small set of commonly used options. Everything unmodeled
can be passed through via opts.extra.
Import
local Mount = require("wardlib.app.mount").Mount
Privilege escalation
Mounting and unmounting typically require elevated privileges. Prefer
wardlib.tools.with so escalation is explicit and scoped.
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
Mount.mount("/dev/sdb1", "/mnt/data", { fstype = "ext4" }):run()
end)
Options
MountOpts
fstype: string?— adds-t <fstype>options: string|string[]?— adds-o <opts>(string or list joined by commas)readonly: boolean?— addsroto-obind: boolean?— adds--bindrbind: boolean?— adds--rbindmove: boolean?— adds--moveverbose: boolean?— adds-vfake: boolean?— adds-fextra: string[]?— appended before positional args
UmountOpts
lazy: boolean?— adds-lforce: boolean?— adds-frecursive: boolean?— adds-Rverbose: boolean?— adds-vextra: string[]?— appended before positional args
API
Mount.mount(source, target, opts)
Builds: mount [opts] [source] [target]
Mount.mount(source: string|nil, target: string|nil, opts: MountOpts|nil) -> ward.Cmd
Notes:
- If both
sourceandtargetarenil, this corresponds to plainmount(printing the current mount table).
Mount.umount(target, opts)
Builds: umount [opts] <target>
Mount.umount(target: string, opts: UmountOpts|nil) -> ward.Cmd
Examples
Show current mounts
local out = require("wardlib.tools.out")
local mounts = out.cmd(Mount.mount(nil, nil))
:label("mount")
:lines()
Mount a device read-only
-- mount -t ext4 -o ro /dev/sdb1 /mnt/data
Mount.mount("/dev/sdb1", "/mnt/data", { fstype = "ext4", readonly = true }):run()
Bind mount
-- mount --bind /src /dst
Mount.mount("/src", "/dst", { bind = true }):run()
Unmount recursively
-- umount -R /mnt/data
Mount.umount("/mnt/data", { recursive = true }):run()
app.mv
app.mv is a thin command-construction wrapper around the mv binary.
It returns ward.process.cmd(...) objects.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Mv = require("wardlib.app.mv").Mv
Privilege escalation
Moving files into protected locations may require elevated privileges. Prefer
wardlib.tools.with so escalation is explicit and scoped.
local with = require("wardlib.tools.with")
with.with(with.middleware.sudo(), function()
Mv.move("./app.conf", "/etc/myapp/app.conf", { force = true }):run()
end)
Options: MvOpts
Overwrite / interaction:
force: boolean?—-f(mutually exclusive withinteractive)interactive: boolean?—-ino_clobber: boolean?—-n(GNU; mutually exclusive withforce/interactive)
Other behavior:
update: boolean?—-uverbose: boolean?—-v
GNU-only:
backup: boolean?—--backupsuffix: string?—--suffix=<s>target_directory: string?—-t <dir>no_target_directory: boolean?—-T
Extra:
extra: string[]?— appended after modeled options
API
Mv.move(src, dest, opts)
Move one or more sources to a destination.
Builds: mv <opts...> -- <src...> <dest>
Mv.move(src: string|string[], dest: string, opts: MvOpts|nil) -> ward.Cmd
Mv.into(src, dir, opts)
Move one or more sources into a directory using GNU-style -t.
Builds: mv <opts...> -t <dir> -- <src...>
Mv.into(src: string|string[], dir: string, opts: MvOpts|nil) -> ward.Cmd
Notes:
-tis GNU-style. If your platform does not support it, preferMv.move(src, dir, ...).
Mv.raw(argv, opts)
Low-level escape hatch.
Builds: mv <modeled-opts...> <argv...>
Mv.raw(argv: string|string[], opts: MvOpts|nil) -> ward.Cmd
Examples
Move a file
-- mv -- a.txt b.txt
Mv.move("a.txt", "b.txt"):run()
Move multiple files into a directory
-- mv -- a.txt b.txt ./dst
Mv.move({ "a.txt", "b.txt" }, "./dst"):run()
Use GNU -t (move into)
-- mv -t ./dst -- a.txt b.txt
Mv.into({ "a.txt", "b.txt" }, "./dst"):run()
Prevent overwrite (GNU)
-- mv -n -- a.txt ./dst/a.txt
Mv.move("a.txt", "./dst/a.txt", { no_clobber = true }):run()
pacman
app.pacman is a thin wrapper around Arch’s pacman that constructs
ward.process.cmd(...) invocations.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Pacman = require("wardlib.app.pacman").Pacman
Running with elevated privileges
Most pacman operations require root. Instead of passing { sudo = true } to
this module (not supported), use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Pacman = require("wardlib.app.pacman").Pacman
-- sudo -n pacman -Sy
w.with(w.middleware.sudo(), Pacman.sync()):run()
API
All functions return a ward.Cmd.
Pacman.sync(opts)
Builds: pacman -Sy (or -Syy if opts.refresh = true).
Pacman.upgrade(opts)
Builds: pacman -Syu (or -Syyu if opts.refresh = true).
Pacman.install(pkgs, opts)
Builds: pacman -S <pkgs...> (plus modeled options).
Pacman.remove(pkgs, opts)
Builds: pacman -R[flags] <pkgs...>.
Flags are derived from:
opts.nosave=>nopts.recursive=>sopts.cascade=>c
Pacman.search(pattern, opts)
Builds: pacman -Ss <pattern>.
Pacman.info(pkg, opts)
Builds: pacman -Qi <pkg>.
Pacman.list_installed(opts)
Builds: pacman -Q.
Options
PacmanCommonOpts
Modeled fields:
noconfirm(boolean):--noconfirmextra(string[]): extra args appended after modeled options
PacmanSyncOpts
Extends PacmanCommonOpts.
Modeled fields:
refresh(boolean): uses-Syy/-Syyuinstead of-Sy/-Syu
PacmanInstallOpts
Extends PacmanCommonOpts.
Modeled fields:
needed(boolean):--needed
PacmanRemoveOpts
Extends PacmanCommonOpts.
Modeled fields:
recursive(boolean): includesflag (-Rs)nosave(boolean): includenflag (-Rn)cascade(boolean): includecflag (-Rc)
Examples
local w = require("wardlib.tools.with")
local Pacman = require("wardlib.app.pacman").Pacman
-- sudo -n pacman -Syu --noconfirm
w.with(w.middleware.sudo(), Pacman.upgrade({ noconfirm = true })):run()
-- sudo -n pacman -S --needed --noconfirm curl git
w.with(w.middleware.sudo(), Pacman.install({ "curl", "git" }, {
needed = true,
noconfirm = true,
})):run()
-- pacman -Ss lua
local r = Pacman.search("lua"):output()
ping
ping (commonly from iputils) sends ICMP ECHO requests to test
reachability and latency.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Ping = require("wardlib.app.ping").Ping
Privilege model
On Linux, ICMP raw sockets typically require CAP_NET_RAW. Depending on your
system, ping may be:
- setuid root (traditional),
- file-capability enabled (
cap_net_raw+ep), or - restricted (requiring
sudo).
If your environment requires elevation, prefer scoping it explicitly:
local w = require("wardlib.tools.with")
local Ping = require("wardlib.app.ping").Ping
w.with(w.middleware.sudo(), function()
Ping.once("1.1.1.1"):run()
end)
API
Ping.bin
Executable name or path (default: "ping").
Ping.ping(dest, opts)
Builds: ping <opts...> <dest>
Ping.once(dest, opts)
Builds: ping <opts...> -c 1 <dest>
Ping.flood(dest, opts)
Builds: ping <opts...> -f <dest>
Options
PingOpts
inet4: boolean?→-4inet6: boolean?→-6(mutually exclusive withinet4)count: number?→-c <n>interval: number?→-i <sec>timeout: number?→-W <sec>(per-packet timeout)deadline: number?→-w <sec>(overall deadline)size: number?→-s <bytes>ttl: number?→-t <ttl>tos: number?→-Q <tos>(TOS/DSCP)mark: number?→-m <mark>(fwmark; availability depends on ping implementation)interface: string?→-I <ifname|addr>source: string?→ alias forinterface(same-Iflag)preload: number?→-l <n>flood: boolean?→-fadaptive: boolean?→-Aquiet: boolean?→-qverbose: boolean?→-vaudible: boolean?→-anumeric: boolean?→-ntimestamp: boolean?→-Drecord_route: boolean?→-Rpmtudisc: string?→-M <do|dont|want>pattern: string?→-p <pattern>(hex pattern)extra: string[]?→ appended after modeled options
Examples
Build a command
local Ping = require("wardlib.app.ping").Ping
-- ping -4 -c 3 -i 0.2 -s 56 -I eth0 1.1.1.1
local cmd = Ping.ping("1.1.1.1", {
inet4 = true,
count = 3,
interval = 0.2,
size = 56,
interface = "eth0",
})
cmd:run()
Parse summary output
local Ping = require("wardlib.app.ping").Ping
local out = require("wardlib.tools.out")
local res = Ping.once("example.com", { numeric = true }):output()
-- You choose what to parse; this gets the last line (summary) reliably.
local summary = out.res(res)
:label("ping")
:lines()
local last = summary[#summary]
-- e.g. "1 packets transmitted, 1 received, 0% packet loss, time 0ms"
playerctl
playerctl controls MPRIS-compatible media players (play/pause/next/status/metadata).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Playerctl = require("wardlib.app.playerctl").Playerctl
Privilege model
playerctl is a user-session tool. It typically runs without elevation. If your
Ward script runs as root (or inside a non-user environment), make sure the relevant
session bus is available.
API
Playerctl.bin
Executable name or path (default: "playerctl").
Playerctl.cmd(subcmd, argv, opts)
Builds: playerctl <global-opts...> <subcmd> [argv...]
Use this as an escape hatch for subcommands not covered by convenience helpers.
Convenience commands
All of the following return a ward.Cmd:
Playerctl.play(opts)→playerctl ... playPlayerctl.pause(opts)→playerctl ... pausePlayerctl.play_pause(opts)→playerctl ... play-pausePlayerctl.next(opts)→playerctl ... nextPlayerctl.previous(opts)→playerctl ... previousPlayerctl.stop(opts)→playerctl ... stopPlayerctl.status(opts)→playerctl ... status
Playerctl.metadata(opts)
Builds: playerctl <global-opts...> metadata [--format <fmt>]
Returns track metadata. If opts.format is provided, output is formatted by
playerctl and typically becomes a single line.
Options
PlayerctlOpts
player: string?→--player <name>all_players: boolean?→--all-playersignore: string[]?→--ignore-player <name>(repeatable)extra: string[]?→ extra argv appended before the subcommand
PlayerctlMetadataOpts
Extends PlayerctlOpts:
format: string?→--format <fmt>
Examples
Control a specific player
local Playerctl = require("wardlib.app.playerctl").Playerctl
-- playerctl --player spotify play-pause
local cmd = Playerctl.play_pause({ player = "spotify" })
cmd:run()
Next / previous across all players
local Playerctl = require("wardlib.app.playerctl").Playerctl
Playerctl.next({ all_players = true }):run()
Playerctl.previous({ all_players = true }):run()
Ignore some players
local Playerctl = require("wardlib.app.playerctl").Playerctl
-- playerctl --all-players --ignore-player firefox --ignore-player chromium status
local cmd = Playerctl.status({
all_players = true,
ignore = { "firefox", "chromium" },
})
cmd:run()
Parse status (stdout)
local Playerctl = require("wardlib.app.playerctl").Playerctl
local out = require("wardlib.tools.out")
local status = out.cmd(Playerctl.status())
:label("playerctl status")
:trim()
:line()
-- status is typically: "Playing", "Paused", or "Stopped"
Read metadata with a format string
local Playerctl = require("wardlib.app.playerctl").Playerctl
local out = require("wardlib.tools.out")
local line = out.cmd(Playerctl.metadata({
player = "spotify",
format = "{{artist}} - {{title}}",
}))
:label("playerctl metadata")
:trim()
:text()
Advanced flags via extra
local Playerctl = require("wardlib.app.playerctl").Playerctl
-- playerctl --player spotify --follow status
local cmd = Playerctl.status({
player = "spotify",
extra = { "--follow" },
})
-- cmd:run() will stream until interrupted
podman
podman is a daemonless container engine. It can operate rootless (recommended)
or with elevated privileges depending on your host configuration.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Podman = require("wardlib.app.podman").Podman
Privilege model
- Rootless Podman: run normally as your user.
- System/root Podman: if your workflow requires root (for certain storage drivers,
networking setups, or host administration), scope privilege escalation explicitly
via
wardlib.tools.with.
local with = require("wardlib.tools.with")
local Podman = require("wardlib.app.podman").Podman
with.with(with.middleware.sudo(), function()
Podman.ps({ all = true }):run()
end)
API
Podman.bin
Executable name or path (default: "podman").
Podman.cmd(subcmd, argv)
Generic helper: builds podman <subcmd> [argv...].
Podman.run(image, cmdline, opts)
Builds: podman run <opts...> <image> [cmd...].
Podman.exec(container, cmdline, opts)
Builds: podman exec <opts...> <container> [cmd...].
Podman.build(context, opts)
Builds: podman build <opts...> <context>.
If context is nil, the wrapper uses ".".
Podman.pull(image, opts) / Podman.push(image, opts)
Builds: podman pull <image> and podman push <image>.
Podman.ps(opts) / Podman.images(opts)
Builds: podman ps <opts...> and podman images <opts...>.
Podman.logs(container, opts)
Builds: podman logs <opts...> <container>.
Lifecycle helpers
Podman.rm(containers, opts)→podman rm ...Podman.rmi(images, opts)→podman rmi ...Podman.start(containers, opts)→podman start ...Podman.stop(containers, opts)→podman stop ...Podman.restart(containers, opts)→podman restart ...Podman.inspect(targets, opts)→podman inspect ...Podman.tag(source, target, opts)→podman tag ...
Auth helpers
Podman.login(registry, opts)→podman login [opts...] [registry]- For security, the wrapper does not accept a password string; prefer
password_stdin=true.
- For security, the wrapper does not accept a password string; prefer
Podman.logout(registry, opts)→podman logout [registry]
Podman.raw(argv, opts)
Low-level escape hatch. Builds: podman <extra...> <argv...>.
Use this when you need a Podman feature not modeled by the structured helpers.
Options
Repeatable fields accept string|string[].
PodmanRunOpts
detach: boolean?→-dinteractive: boolean?→-itty: boolean?→-trm: boolean?→--rmname: string?→--name <name>hostname: string?→--hostname <hostname>workdir: string?→-w <dir>user: string?→-u <user>entrypoint: string?→--entrypoint <entrypoint>env: string|string[]?→-e <k=v>(repeatable)env_file: string|string[]?→--env-file <file>(repeatable)publish: string|string[]?→-p <host:container>(repeatable)volume: string|string[]?→-v <host:container>(repeatable)network: string?→--network <net>add_host: string|string[]?→--add-host <host:ip>(repeatable)label: string|string[]?→--label <k=v>(repeatable)privileged: boolean?→--privilegedcap_add: string|string[]?→--cap-add <cap>(repeatable)cap_drop: string|string[]?→--cap-drop <cap>(repeatable)platform: string?→--platform <platform>pull: string?→--pull <policy>extra: string[]?→ extra argv appended after modeled options
PodmanExecOpts
detach: boolean?→-dinteractive: boolean?→-itty: boolean?→-tuser: string?→-u <user>workdir: string?→-w <dir>env: string|string[]?→-e <k=v>(repeatable)extra: string[]?
PodmanBuildOpts
tag: string|string[]?→-t <tag>(repeatable)file: string?→-f <containerfile>build_arg: string|string[]?→--build-arg <k=v>(repeatable)target: string?→--target <stage>platform: string?→--platform <platform>pull: boolean?→--pullno_cache: boolean?→--no-cachelayers: boolean?→--layersformat: string?→--format <format>extra: string[]?
PodmanPsOpts
all: boolean?→-aquiet: boolean?→-qno_trunc: boolean?→--no-trunclatest: boolean?→-llast: integer?→-n <n>size: boolean?→-sformat: string?→--format <fmt>filter: string|string[]?→--filter <filter>(repeatable)extra: string[]?
PodmanImagesOpts
all: boolean?→-aquiet: boolean?→-qno_trunc: boolean?→--no-truncdigests: boolean?→--digestsformat: string?→--format <fmt>filter: string|string[]?→--filter <filter>(repeatable)extra: string[]?
PodmanLogsOpts
follow: boolean?→-ftimestamps: boolean?→-tsince: string?→--since <time>until: string?→--until <time>tail: string|integer?→--tail <n|all>extra: string[]?
PodmanRmOpts
force: boolean?→-fvolumes: boolean?→-vextra: string[]?
PodmanRmiOpts
force: boolean?→-fextra: string[]?
PodmanStopOpts
time: integer?→-t <seconds>extra: string[]?
PodmanInspectOpts
format: string?→-f <format>size: boolean?→-stype: string?→--type <type>extra: string[]?
PodmanLoginOpts
username: string?→-u <user>password_stdin: boolean?→--password-stdinextra: string[]?
Examples
Run and remove a container
local Podman = require("wardlib.app.podman").Podman
-- podman run --rm -e A=1 -p 8080:80 alpine:3 sh -lc 'echo ok'
Podman.run("alpine:3", { "sh", "-lc", "echo ok" }, {
rm = true,
env = "A=1",
publish = "8080:80",
}):run()
Build an image
local Podman = require("wardlib.app.podman").Podman
-- podman build -t myimg:dev -f Containerfile --layers .
Podman.build(".", {
tag = "myimg:dev",
file = "Containerfile",
layers = true,
}):run()
Inspect and parse JSON output
podman inspect prints JSON by default (an array). Parse it using wardlib.tools.out:
local Podman = require("wardlib.app.podman").Podman
local out = require("wardlib.tools.out")
local data = out.cmd(Podman.inspect("myctr"))
:label("podman inspect myctr")
:json()
-- data is usually an array; take first element
local obj = data[1]
local image = obj.ImageName
Tail logs
local Podman = require("wardlib.app.podman").Podman
Podman.logs("myctr", { follow = true, tail = 100 }):run()
Login with --password-stdin
local proc = require("ward.process")
local Podman = require("wardlib.app.podman").Podman
-- printf '%s' "$TOKEN" | podman login --password-stdin -u myuser registry.example.com
local feeder = proc.cmd("printf", "%s", "mytoken")
local cmd = Podman.login("registry.example.com", {
username = "myuser",
password_stdin = true,
})
(feeder | cmd):run()
app.qemu
app.qemu is a set of command-construction wrappers around QEMU tools:
qemu-system-<arch>(system emulation)qemu-img(disk image utility)qemu-nbd(Network Block Device server / /dev/nbd binding / list mode)qemu-storage-daemon(QSD)
This module constructs
ward.process.cmd(...)invocations; it does not parse output. Consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local qemu = require("wardlib.app.qemu")
local Qemu = qemu.Qemu
Philosophy
QEMU has a very large CLI surface. app.qemu covers the common patterns with structured
option tables and helper builders (for QEMU’s k=v,k2=v2 comma-list syntax),
while still leaving an escape hatch:
opts.extra: string[]is appended verbatim at the end for advanced flags.
Helpers (comma-list builders)
Many QEMU flags accept comma-separated specs. These helpers accept either:
- a raw
string(passed as-is), or - a
tablethat is encoded into a stablehead,k=v,...string (keys sorted).
Boolean values in tables are encoded as on/off.
qemu.hostfwd(spec) -> string
Builds a hostfwd= value for user-mode networking (-netdev user,...).
qemu.hostfwd({ hostport = 2222, guestport = 22 })
-- => "tcp::2222-:22"
qemu.hostfwd({
proto = "udp",
hostaddr = "127.0.0.1",
hostport = 5353,
guestaddr = "10.0.2.15",
guestport = 5353,
})
-- => "udp:127.0.0.1:5353-10.0.2.15:5353"
Fields:
proto(default"tcp")hostaddr(optional; empty means all interfaces)hostport(required)guestaddr(optional; empty means default guest)guestport(required)
qemu.drive(spec) -> string
Builds a -drive <spec> string.
- Required key when using a table:
file - Alias:
if_becomesif(becauseifis a Lua keyword)
local drive = qemu.drive({
file = "disk.qcow2",
format = "qcow2",
if_ = "virtio",
cache = "writeback",
})
-- => "file=disk.qcow2,cache=writeback,format=qcow2,if=virtio"
qemu.netdev(spec) -> string
Builds a -netdev <spec> string.
- Required key when using a table:
type
local net0 = qemu.netdev({
type = "user",
id = "net0",
hostfwd = qemu.hostfwd({ hostport = 2222, guestport = 22 }),
})
qemu.device(spec) -> string
Builds a -device <spec> string.
- Required key when using a table:
driver
local nic = qemu.device({ driver = "virtio-net-pci", netdev = "net0" })
qemu.chardev(spec), qemu.fsdev(spec), qemu.object(spec), qemu.kv(spec)
These mirror the same encoding approach:
chardev: required keybackendfsdev: required keydriverobject: required keytypekv: generick=v,k2=v2encoder
System emulation
Qemu.system(arch, opts) -> ward.Cmd
Runs qemu-system-<arch> with a structured opts table.
archexamples:"x86_64","aarch64","riscv64", …opts.bincan override the binary (name or absolute path)
Common options:
memory:-m <val>(string or number)smp:-smp <val>(string or number)machine:-machine <val>accel:-accel <val>(string or string[])cpu:-cpu <val>nographic:-nographicdisplay:-display <val>serial,monitor,qmp: repeatabledrive,device,netdev,chardev,fsdev,object,global: repeatable (strings or tables)extra: appended verbatim
Additional supported system options
In addition to the common options above, the wrapper also supports:
name:-name <val>uuid:-uuid <val>bios:-bios <file>kernel:-kernel <file>initrd:-initrd <file>append:-append <cmdline>boot:-boot <spec>cdrom:-cdrom <file>snapshot:-snapshotdaemonize:-daemonizepidfile:-pidfile <path>gdb:-gdb <dev>start_paused:-Srtc:-rtc <spec>clock:-clock <spec>global:-global <spec>(repeatable)images: positional images appended last (string|string[])
Repeatable spec lists (each element may be a raw string or a table encoded into a stable comma-list):
object:-object <spec>chardev:-chardev <spec>fsdev:-fsdev <spec>netdev:-netdev <spec>device:-device <spec>drive:-drive <spec>
Example: kernel boot
Qemu.system_x86_64({
nographic = true,
machine = "q35",
accel = { "kvm", "tcg" },
memory = "1G",
kernel = "bzImage",
initrd = "initramfs.cpio.gz",
append = "console=ttyS0 root=/dev/ram0",
}):run()
Example: QMP via -chardev
Qemu.system_x86_64({
nographic = true,
chardev = { { backend = "socket", id = "qmp0", path = "/tmp/qmp.sock", server = true, wait = false } },
qmp = { "chardev:qmp0,server=on,wait=off" },
}):run()
Convenience helpers:
Qemu.system_x86_64(opts)Qemu.system_i386(opts)Qemu.system_aarch64(opts)Qemu.system_arm(opts)Qemu.system_riscv64(opts)Qemu.system_ppc64(opts)
Example: boot a qcow2 image with user-mode networking
local qemu = require("wardlib.app.qemu")
local Qemu = qemu.Qemu
Qemu.system_x86_64({
memory = "2G",
smp = 4,
nographic = true,
netdev = {
{ type = "user", id = "net0", hostfwd = qemu.hostfwd({ hostport = 2222, guestport = 22 }) },
},
device = {
{ driver = "virtio-net-pci", netdev = "net0" },
},
drive = {
{ file = "disk.qcow2", format = "qcow2", if_ = "virtio" },
},
}):run()
Disk images (qemu-img)
Qemu.img_create(path, size, opts)
Builds:
qemu-img create [-f <fmt>] [-o <k=v,...>] <path> <size>
Qemu.img_create("disk.qcow2", "20G", {
format = "qcow2",
options = { cluster_size = "2M", preallocation = "metadata" },
}):run()
Qemu.img_info(path, opts)
Builds:
qemu-img info [-f <fmt>] [--output=<fmt>] <path>
Qemu.img_convert(src, dst, opts)
Builds:
qemu-img convert ... <src> <dst>
Supported options include: input_format (-f), output_format (-O), output_options (-o),
backing_file (-B), compress (-c), progress (-p), quiet (-q),
target_image_opts, image_opts, and extra.
QemuImgConvertOpts fields
input_format:-f <fmt>output_format:-O <fmt>output_options:-o <opts>(string|string[]|table; encoded as comma-list)backing_file:-B <file>snapshot:-s <name>compress:-cprogress:-pquiet:-qunsafe:-Uimage_opts:--image-optstarget_image_opts:--target-image-optsextra: appended verbatim
Qemu.img_resize(path, size, opts)
Builds:
qemu-img resize [-f <fmt>] <path> <size>
Snapshots
Qemu.img_snapshot_list(path)Qemu.img_snapshot_create(path, name)Qemu.img_snapshot_apply(path, name)Qemu.img_snapshot_delete(path, name)
NBD (qemu-nbd)
The wrapper supports both:
- local
/dev/nbdXbinding (Linux) via-c/--connectand-d/--disconnect, and - server/list mode with
--port,--bind,--socket,--export-name, TLS objects, etc.
Qemu.nbd_connect(dev, image, opts)
-- qemu-nbd -c /dev/nbd0 -f qcow2 disk.qcow2
Qemu.nbd_connect("/dev/nbd0", "disk.qcow2", { format = "qcow2" }):run()
Qemu.nbd_disconnect(dev, opts)
-- qemu-nbd -d /dev/nbd0
Qemu.nbd_disconnect("/dev/nbd0"):run()
Qemu.nbd_serve(image, opts)
Runs qemu-nbd in server mode (TCP or Unix socket), with options like:
port,bind,socketread_only,shared,persistent,fork,pid_fileexport_name,descriptionobject,tls_creds,tls_authz,tls_hostnameimage_opts+filenameas a block-driver options string
Qemu.nbd_list(opts)
Connects as a client and lists exports (-L).
NBD options (QemuNbdOpts)
The wrapper maps a large portion of qemu-nbd flags into opts (plus opts.extra). Highlights:
- Listening / endpoints:
port (-p),bind (-b),socket (-k) - Image selection:
format (-f),offset (-o),image_opts (--image-opts) - Access & behavior:
read_only (-r),shared (-e),persistent (-t),fork (--fork),pid_file (--pid-file) - Snapshots:
snapshot (-s),load_snapshot (-l) - IO tuning:
cache,nocache (-n),aio,discard,detect_zeroes,allocation_depth (-A),bitmap (-B) - TLS:
object (--object ...),tls_creds,tls_hostname,tls_authz - Metadata/debug:
export_name (-x),description (-D),handshake_limit,verbose (-v),trace (-T ...)
Example: serve via TCP
Qemu.nbd_serve("disk.qcow2", {
format = "qcow2",
bind = "127.0.0.1",
port = 10809,
export_name = "disk0",
persistent = true,
}):run()
Example: list exports
Qemu.nbd_list({ bind = "127.0.0.1", port = 10809 }):run()
QEMU Storage Daemon (qemu-storage-daemon)
Qemu.storage_daemon(opts)
Constructs a qemu-storage-daemon invocation.
Common options:
chardev,monitorobjectblockdevnbd_serverexportpidfile,daemonizeextra
Storage daemon options (QemuStorageDaemonOpts)
chardev:--chardev <spec>(repeatable; string or table viaqemu.chardev)monitor:--monitor <spec>(repeatable)object:--object <spec>(repeatable; string or table viaqemu.object)blockdev:--blockdev <spec>(repeatable; each item encoded as a comma-list)nbd_server:--nbd-server <spec>(repeatable; comma-list)export:--export <spec>(repeatable; comma-list)pidfile:--pidfile=<path>daemonize:--daemonizeextra: appended verbatim
Example: NBD export via QSD
Qemu.storage_daemon({
blockdev = {
{ driver = "file", filename = "disk.qcow2", node_name = "file0" },
{ driver = "qcow2", file = "file0", node_name = "img0" },
},
nbd_server = {
{ addr = { type = "inet", host = "127.0.0.1", port = "10809" } },
},
export = {
{ type = "nbd", id = "exp0", node_name = "img0", name = "disk0", writable = true },
},
}):run()
rg
rg (ripgrep) is a fast recursive search tool that respects ignore files
by default.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Rg = require("wardlib.app.rg").Rg
Exit codes
rg uses exit codes that often surprise users:
- 0: at least one match was found
- 1: no matches were found
- 2: an error occurred
When you want “no matches” to be a non-failure, use wardlib.tools.out with
:allow_fail() and interpret res.code.
API
Rg.bin
Executable name or path (default: "rg").
Rg.search(pattern, paths, opts)
Builds: rg <opts...> -e <pattern>... [paths...]
pattern:string|string[]- Always emitted using
-e <pattern>(possibly repeated) to avoid ambiguity when patterns start with-.
- Always emitted using
paths:string|string[]|nil- If
nil, ripgrep searches the current directory.
- If
Rg.files(paths, opts)
Builds: rg <opts...> --files [paths...]
Rg.raw(argv, opts)
Builds: rg <opts...> <argv...>
Use this when you need an rg feature not modeled in RgOpts.
Options
RgOpts
Matching behavior:
fixed: boolean?→-F(fixed strings)ignore_case: boolean?→-ismart_case: boolean?→-Scase_sensitive: boolean?→-sword: boolean?→-wline: boolean?→-xinvert: boolean?→-v
Output / formatting:
count: boolean?→-c(count matching lines)count_matches: boolean?→--count-matchesquiet: boolean?→-qline_number: boolean?→-ncolumn: boolean?→--columnheading: boolean?→--headingno_filename: boolean?→--no-filename(mutually exclusive withwith_filename)with_filename: boolean?→--with-filename(mutually exclusive withno_filename)vimgrep: boolean?→--vimgrepjson: boolean?→--json(JSON Lines / NDJSON)
Context / limits:
after_context: number?→-A <n>before_context: number?→-B <n>context: number?→-C <n>(mutually exclusive withafter_context/before_context)max_count: number?→-m <n>threads: number?→-j <n>
Filesystem behavior:
follow: boolean?→-L(follow symlinks)hidden: boolean?→--hiddenno_ignore: boolean?→--no-ignoreno_ignore_vcs: boolean?→--no-ignore-vcs
Filtering:
glob: string|string[]?→-g <glob>(repeatable)type: string|string[]?→--type <type>(repeatable)type_not: string|string[]?→--type-not <type>(repeatable)files_with_matches: boolean?→--files-with-matchesfiles_without_match: boolean?→--files-without-match
Replacement:
replace: string?→-r <replacement>
Other:
color: boolean|string?→--color[=WHEN]- If
true, emits--color=auto. - If a string, emits
--color=<value>.
- If
extra: string[]?→ extra argv appended after modeled options
Examples
Basic search
local Rg = require("wardlib.app.rg").Rg
-- rg -F -S --hidden -g '*.lua' -g '!vendor/**' --type lua -C 2 --color=never -e TODO . src
local cmd = Rg.search("TODO", { ".", "src" }, {
fixed = true,
smart_case = true,
hidden = true,
glob = { "*.lua", "!vendor/**" },
type = "lua",
context = 2,
color = "never",
})
cmd:run()
Treat “no matches” as non-failure
local Rg = require("wardlib.app.rg").Rg
local out = require("wardlib.tools.out")
local o = out.cmd(Rg.search("needle", ".", { fixed = true }))
:label("rg search")
:allow_fail()
local res = o:res() -- access the underlying CmdResult
if res.code == 0 then
local lines = out.res(res):lines()
-- matches present
elseif res.code == 1 then
-- no matches (not an error)
else
-- res.code == 2 (or other) – treat as error
error("rg failed: " .. tostring(res.code))
end
Parse --json output (NDJSON)
local Rg = require("wardlib.app.rg").Rg
local out = require("wardlib.tools.out")
local events = out.cmd(Rg.search("TODO", ".", { json = true }))
:label("rg --json")
:json_lines()
-- events is an array of decoded JSON objects (one per line)
-- You can filter for { type = "match" } events, etc.
List files ripgrep would search
local Rg = require("wardlib.app.rg").Rg
-- rg --files .
local cmd = Rg.files(".")
cmd:run()
rm
rm removes directory entries.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Rm = require("wardlib.app.rm").Rm
API
Rm.bin
Executable name or path (default: "rm").
Rm.remove(paths, opts)
Builds: rm <opts...> -- <paths...>
paths accepts string|string[].
Rm.raw(argv, opts)
Builds: rm <modeled-opts...> <argv...>
Use this when you need rm flags not modeled in RmOpts.
Options (RmOpts)
force: boolean?→-f(mutually exclusive withinteractive)interactive: boolean?→-i(mutually exclusive withforce)recursive: boolean?→-r/-Rdir: boolean?→-d(remove empty directories)verbose: boolean?→-v(GNU)extra: string[]?→ extra argv appended after modeled options
Examples
Remove multiple paths
local Rm = require("wardlib.app.rm").Rm
-- rm -f -r -- build dist
Rm.remove({ "build", "dist" }, { force = true, recursive = true }):run()
Run with elevation (when required)
Some removals require privileges (e.g. deleting root-owned files). Prefer explicit, scoped elevation:
local Rm = require("wardlib.app.rm").Rm
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), function()
Rm.remove("/var/tmp/some-root-owned-file", { force = true }):run()
end)
rofi
rofi is a popular application launcher / window switcher.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Rofi = require("wardlib.app.rofi").Rofi
API
Rofi.bin
Executable name or path (default: "rofi").
Rofi.dmenu(opts)
Builds: rofi <common...> -dmenu <dmenu...>
Rofi.show(mode, opts)
Builds: rofi <common...> -show <mode> <extra...>
Options
RofiCommonOpts
config: string?→-config <file>theme: string?→-theme <theme>theme_str: string?→-theme-str <string>modi: string?→-modi <modes>show_icons: boolean?→-show-iconsterminal: string?→-terminal <terminal>extra: string[]?→ extra argv appended after modeled options
RofiDmenuOpts
Extends RofiCommonOpts and adds:
sep: string?→-sep <sep>prompt: string?→-p <prompt>lines: number?→-l <n>insensitive: boolean?→-ionly_match: boolean?→-only-matchno_custom: boolean?→-no-customformat: string?→-format <fmt>select: string?→-select <string>mesg: string?→-mesg <msg>password: boolean?→-passwordmarkup_rows: boolean?→-markup-rowsmulti_select: boolean?→-multi-selectsync: boolean?→-syncinput: string?→-input <file>window_title: string?→-window-title <title>windowid: string?→-w <windowid>
Examples
run launcher
local Rofi = require("wardlib.app.rofi").Rofi
Rofi.show("run", {
modi = "run,drun",
show_icons = true,
}):output()
-dmenu mode
local Rofi = require("wardlib.app.rofi").Rofi
Rofi.dmenu({ prompt = "Run" }):output()
rsync
rsync synchronizes files and directories efficiently (local or remote). It is commonly
used for deployment, backup, and mirroring.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Rsync = require("wardlib.app.rsync").Rsync
API
Rsync.bin
Executable name or path (default: "rsync").
Rsync.sync(src, dest, opts)
Builds: rsync <opts...> <src...> <dest>
src:string|string[](one or many sources)dest:string(destination directory or remote target)
Options
RsyncOpts
archive: boolean?→-acompress: boolean?→-zverbose: boolean?→-vprogress: boolean?→--progressdelete: boolean?→--deletedry_run: boolean?→--dry-runchecksum: boolean?→--checksumpartial: boolean?→--partialexcludes: string[]?→--exclude <pattern>(repeatable)include: string[]?→--include <pattern>(repeatable)rsh: string?→-e <rsh>(e.g."ssh -p 2222 -i ~/.ssh/id_ed25519")extra: string[]?→ extra argv appended before src/dest
Examples
Local directory sync (archive + delete)
local Rsync = require("wardlib.app.rsync").Rsync
-- rsync -a --delete ./src/ ./dst/
local cmd = Rsync.sync("./src/", "./dst/", {
archive = true,
delete = true,
})
cmd:run()
Remote sync over SSH with excludes
local Rsync = require("wardlib.app.rsync").Rsync
-- rsync -a -z --delete -e "ssh -p 2222 -i ~/.ssh/id_ed25519" \
-- --exclude .git --exclude target \
-- ./project/ me@host:/srv/project/
local cmd = Rsync.sync("./project/", "me@host:/srv/project/", {
archive = true,
compress = true,
delete = true,
rsh = "ssh -p 2222 -i ~/.ssh/id_ed25519",
excludes = { ".git", "target" },
})
cmd:run()
Multiple sources into one destination
local Rsync = require("wardlib.app.rsync").Rsync
-- rsync -a a/ b/ ./dst/
Rsync.sync({ "a/", "b/" }, "./dst/", { archive = true }):run()
Dry run with progress
local Rsync = require("wardlib.app.rsync").Rsync
-- rsync -a --dry-run --progress ./src/ host:/dst/
Rsync.sync("./src/", "host:/dst/", {
archive = true,
dry_run = true,
progress = true,
}):run()
Include / exclude patterns
local Rsync = require("wardlib.app.rsync").Rsync
-- rsync -a --include "*.lua" --exclude "*" ./src/ ./dst/
Rsync.sync("./src/", "./dst/", {
archive = true,
include = { "*.lua" },
excludes = { "*" },
}):run()
sed
sed is a stream editor for filtering and transforming text.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Sed = require("wardlib.app.sed").Sed
Portability notes
-E(extended regex) is supported on both GNU and BSD sed.- In-place editing (
-i) differs between implementations. This wrapper modelsin_placeso it works on GNU sed and is typically accepted on BSD sed as well. If you need strict BSD behavior, passextra = { "-i", ".bak" }.
API
Sed.bin
Executable name or path (default: "sed").
Sed.run(inputs, opts)
Builds: sed <opts...> [inputs...]
- If
inputsisnil, sed reads stdin. inputsacceptsstring|string[]|nil.
Sed.script(script, inputs, opts)
Convenience wrapper around Sed.run that adds -e <script>.
Sed.replace(pattern, repl, inputs, opts)
Convenience wrapper that adds -e s/pattern/repl/g.
This does not escape
pattern/repl; escape them yourself if needed.
Sed.inplace_replace(pattern, repl, inputs, backup_suffix, opts)
Convenience wrapper that enables in-place editing (via -i) and adds the substitution.
inputsmust bestring|string[](a real file path list).backup_suffixmay benil(GNU-style-i) or a string like.bak.
Options
SedOpts
extended: boolean?→-Equiet: boolean?→-nin_place: boolean|string?→-ior-i<suffix>backup_suffix: string?→ alias forin_place = "<suffix>"expression: string|string[]?→-e <script>(repeatable)file: string|string[]?→-f <file>(repeatable)null_data: boolean?→-z(GNU)follow_symlinks: boolean?→--follow-symlinks(GNU)posix: boolean?→--posix(GNU)sandbox: boolean?→--sandbox(GNU)extra: string[]?→ extra argv appended after modeled options
Examples
Apply a script to stdin
local Sed = require("wardlib.app.sed").Sed
-- sed -e 's/foo/bar/g'
local cmd = Sed.script("s/foo/bar/g")
Apply a script to files
local Sed = require("wardlib.app.sed").Sed
-- sed -E -e 's/foo/bar/g' a.txt b.txt
local cmd = Sed.replace("foo", "bar", { "a.txt", "b.txt" }, { extended = true })
In-place edit
local Sed = require("wardlib.app.sed").Sed
-- sed -i.bak -e 's/hello/hi/g' file.txt
local cmd = Sed.inplace_replace("hello", "hi", "file.txt", ".bak")
cmd:run()
Multiple expressions
local Sed = require("wardlib.app.sed").Sed
-- sed -e 's/a/b/g' -e 's/c/d/g' file.txt
local cmd = Sed.run("file.txt", {
expression = { "s/a/b/g", "s/c/d/g" },
})
Read stdout using wardlib.tools.out
local Sed = require("wardlib.app.sed").Sed
local out = require("wardlib.tools.out")
local txt = out.cmd(Sed.script("s/foo/bar/g"))
:label("sed")
:text()
sfdisk
sfdisk (from util-linux) is a script-oriented tool for partitioning block devices.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result. It provides a small helper to build sfdisk scripts (Sfdisk.script) and a convenience pipeline builder (Sfdisk.apply).
Import
local Sfdisk = require("wardlib.app.sfdisk").Sfdisk
Privilege model
Partitioning requires elevated privileges on most systems. Prefer a scoped middleware
approach (instead of embedding sudo flags in wrappers):
local with = require("wardlib.tools.with")
local Sfdisk = require("wardlib.app.sfdisk").Sfdisk
with.with(with.middleware.sudo(), function()
Sfdisk.list("/dev/sda"):run()
end)
API
Sfdisk.bin
Executable name or path (default: "sfdisk").
Sfdisk.cmd(argv, opts)
Builds: sfdisk <opts...> [argv...]
Generic escape hatch for constructing sfdisk invocations.
Sfdisk.dump(device, opts)
Builds: sfdisk --dump <device>
Sfdisk.json(device, opts)
Builds: sfdisk --json <device>
Sfdisk.list(device, opts)
Builds: sfdisk --list <device>
Sfdisk.write(device, opts)
Builds: sfdisk <opts...> <device>
Returns a command expecting the sfdisk script on stdin.
Sfdisk.script(spec)
Encodes a structured SfdiskTable into an sfdisk script string.
Sfdisk.apply(device, spec_or_script, opts)
Builds a pipeline:
printf "%s" <script> | sfdisk <opts...> <device>
spec_or_script may be a raw string script or a structured SfdiskTable.
Options
SfdiskOpts
force: boolean?→--forceno_reread: boolean?→--no-rereadno_act: boolean?→--no-actquiet: boolean?→--quietlock: boolean|"yes"|"no"|"nonblock"|nil→--lock[=mode]true/"yes"→--lockfalse/"no"→--lock=no"nonblock"→--lock=nonblock
wipe: "auto"|"never"|"always"|string|nil→--wipe <mode>label: string?→--label <type>(e.g."gpt","dos")sector_size: integer?→--sector-size <n>extra: string[]?→ extra argv inserted before positional args
Script structures
SfdiskPartition
Each partition line is rendered as comma-separated key=value fields.
start: string|integer|nilsize: string|integer|niltype: string|niluuid: string|nilname: string|nilattrs: string|nilbootable: boolean|nil→ emitsbootableextra: table<string, string|number|boolean>|nil→ appended at end (sorted by key)
SfdiskTable
Header fields are emitted as key: value lines.
label: string|nillabel_id: string|nil→label-id:unit: string|nil→unit:(e.g."sectors")first_lba: integer|nil→first-lba:last_lba: integer|nil→last-lba:extra_header: table<string, string|number|boolean>|nil→ extra header lines (sorted)partitions: SfdiskPartition[](required)
Examples
Dump and parse JSON output
local Sfdisk = require("wardlib.app.sfdisk").Sfdisk
local out = require("wardlib.tools.out")
local data = out.cmd(Sfdisk.json("/dev/sda"))
:label("sfdisk --json /dev/sda")
:json()
-- data.sfdisk is typically a table with disklabel/partitions information.
Apply partitions via stdin in one call (raw script)
This matches: printf "..." | sfdisk ....
local with = require("wardlib.tools.with")
local Sfdisk = require("wardlib.app.sfdisk").Sfdisk
local script = [[
label: gpt
unit: sectors
start=2048, size=1048576, type=U
start=1050624, size=20971520, type=8300
]]
with.with(with.middleware.sudo(), function()
Sfdisk.apply("/dev/sda", script, { force = true }):run()
end)
Apply partitions via a structured spec
local with = require("wardlib.tools.with")
local Sfdisk = require("wardlib.app.sfdisk").Sfdisk
local spec = {
label = "gpt",
unit = "sectors",
partitions = {
{ start = 2048, size = "512M", type = "U" },
{ start = 1050624, size = "20G", type = "8300", bootable = true },
},
}
with.with(with.middleware.sudo(), function()
Sfdisk.apply("/dev/sda", spec, { force = true, wipe = "always" }):run()
end)
Advanced flags and passthrough args
local with = require("wardlib.tools.with")
local Sfdisk = require("wardlib.app.sfdisk").Sfdisk
local spec = {
label = "gpt",
partitions = {
{ start = 2048, size = "1G", type = "U" },
{ start = 0, size = 0, type = "8300" },
},
}
with.with(with.middleware.sudo(), function()
Sfdisk.apply("/dev/sda", spec, {
force = true,
no_reread = true,
lock = "nonblock",
wipe = "always",
sector_size = 4096,
extra = { "--wipe-partitions", "always" },
}):run()
end)
sha256sum
sha256sum computes and checks SHA-256 message digests.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Sha256sum = require("wardlib.app.sha256sum").Sha256sum
API
Sha256sum.bin
Executable name or path (default: "sha256sum").
Sha256sum.sum(files, opts)
Builds: sha256sum <opts...> [-- <files...>]
- If
filesisnil,sha256sumreads stdin. - If
filesis provided, the wrapper inserts--before the file list.
Sha256sum.check(check_files, opts)
Builds: sha256sum -c <opts...> [-- <check_files...>]
- If
check_filesisnil,sha256sumreads the checksum list from stdin. - If
check_filesis provided, the wrapper inserts--before the file list.
Sha256sum.raw(argv, opts)
Builds: sha256sum <opts...> <argv...>
Use this when you need sha256sum options not modeled in Sha256sumOpts.
Options (Sha256sumOpts)
binary: boolean?→-b(binary mode)text: boolean?→-t(text mode)- Mutually exclusive:
binaryandtext.
- Mutually exclusive:
tag: boolean?→--tagzero: boolean?→-z(NUL line terminator)
Check-mode options (when using Sha256sum.check / -c):
quiet: boolean?→--quietstatus: boolean?→--statuswarn: boolean?→--warnstrict: boolean?→--strictignore_missing: boolean?→--ignore-missing
Escape hatch:
extra: string[]?→ appended after modeled options
Examples
Compute hashes for files
local Sha256sum = require("wardlib.app.sha256sum").Sha256sum
-- sha256sum -- file1 file2
local cmd = Sha256sum.sum({ "file1", "file2" })
cmd:run()
Parse the hash from stdout
local Sha256sum = require("wardlib.app.sha256sum").Sha256sum
local out = require("wardlib.tools.out")
local text = out.cmd(Sha256sum.sum("file1"))
:label("sha256sum file1")
:trim()
:line()
-- Default format: "<hash> <filename>"
local hash = text:match("^(%x+)%s")
assert(hash ~= nil, "failed to parse sha256")
Verify checksums
local Sha256sum = require("wardlib.app.sha256sum").Sha256sum
-- sha256sum -c --strict -- checksums.txt
local cmd = Sha256sum.check("checksums.txt", { strict = true })
cmd:run()
Read checksum list from stdin
local proc = require("ward.process")
local Sha256sum = require("wardlib.app.sha256sum").Sha256sum
-- printf '%s' "<hash> file" | sha256sum -c
local feeder = proc.cmd("printf", "%s", "hello\n")
(feeder | Sha256sum.check(nil)):run()
ss
ss (socket statistics) is part of iproute2 and is commonly used to
inspect TCP/UDP/UNIX sockets.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Ss = require("wardlib.app.ss").Ss
Privilege model
Many ss queries work unprivileged, but showing process info (-p) may require
additional permissions depending on your system. If you need elevation, scope
it explicitly using wardlib.tools.with middleware.
API
Ss.bin
Executable name or path (default: "ss").
Ss.show(filter, opts)
Builds: ss <opts...> [filter...]
filter:string|string[]|nil- If
string, appended as a single argv element. - If
string[], each element is appended as a single token (useful for complex filters).
- If
Ss.summary(opts)
Builds: ss <opts...> -s
Ss.listen(filter, opts)
Builds: ss <opts...> -l [filter...]
Ss.all_sockets(filter, opts)
Builds: ss <opts...> -a [filter...]
Options
SsOpts
Address family:
inet4: boolean?→-4inet6: boolean?→-6(mutually exclusive withinet4)family: string?→-f <family>(e.g."inet","inet6","unix","link")
Socket types:
tcp: boolean?→-tudp: boolean?→-uraw: boolean?→-wunix: boolean?→-x
Selection:
all: boolean?→-alistening: boolean?→-l
Output formatting/details:
numeric: boolean?→-nresolve: boolean?→-rno_header: boolean?→-Hextended: boolean?→-einfo: boolean?→-imemory: boolean?→-mtimers: boolean?→-osummary: boolean?→-s
Process / packet (-p):
process: boolean?→-p(show process using socket)packet: boolean?→-p(packet sockets; mutually exclusive withprocess)
SELinux context:
context: string?→-Z <context>show_context: boolean?→-Z
Escape hatch:
extra: string[]?→ additional argv appended after modeled options
Examples
Show listening SSH sockets with process info
local Ss = require("wardlib.app.ss").Ss
-- ss -t -l -n -p state listening dport = :ssh
local cmd = Ss.show({ "state", "listening", "dport", "=", ":ssh" }, {
tcp = true,
listening = true,
numeric = true,
process = true,
})
cmd:run()
Summary
local Ss = require("wardlib.app.ss").Ss
local out = require("wardlib.tools.out")
local txt = out.cmd(Ss.summary({ numeric = true }))
:label("ss -s")
:text()
-- txt contains the full summary table
ssh / scp
wardlib.app.ssh is a thin wrapper around OpenSSH ssh and scp.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
Ssh.exec()accepts a remote command either as a single string (passed as-is) or as a string array (appended as individual argv items).- Use
wardlib.tools.outto parse stdout/stderr.
Import
local Ssh = require("wardlib.app.ssh").Ssh
API
Ssh.target(host, opts)
Builds a target string:
host(default)user@hostwhenopts.useris provided
Ssh.remote(host, path, opts)
Builds a remote scp path string:
host:/pathuser@host:/pathwhenopts.useris provided
Ssh.exec(host, remote, opts)
Builds: ssh <common-opts...> <target> [remote...]
- If
remoteisnil, builds an interactive session. - If
remoteis a string, it is passed as a single argument. - If
remoteis a string array, items are appended as argv.
Ssh.scp(src, dst, opts)
Builds: scp <common-opts...> <scp-opts...> <src> <dst>
Ssh.copy_to(host, local_path, remote_path, opts)
Convenience for local → remote:
Builds: scp ... <local_path> <user@host:remote_path>
Ssh.copy_from(host, remote_path, local_path, opts)
Convenience for remote → local:
Builds: scp ... <user@host:remote_path> <local_path>
Options
SshCommonOpts
user: string?— username (prepended asuser@host)port: integer?— port (ssh -p,scp -P)identity_file: string?— identity file (-i <path>)batch: boolean?—-o BatchMode=yesstrict_host_key_checking: boolean|string?—-o StrictHostKeyChecking=<...>true→yes,false→no, or pass a string such as"accept-new"
known_hosts_file: string?—-o UserKnownHostsFile=<path>connect_timeout: integer?—-o ConnectTimeout=<seconds>extra: string[]?— extra argv items appended before host/paths
ScpOpts (extends SshCommonOpts)
recursive: boolean?—-rpreserve_times: boolean?—-pcompress: boolean?—-Cquiet: boolean?—-q
Examples
Interactive SSH session (with identity + port)
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to: ssh -p 2222 -i ~/.ssh/id_ed25519 me@example.com
local cmd = Ssh.exec("example.com", nil, {
user = "me",
port = 2222,
identity_file = "~/.ssh/id_ed25519",
})
-- cmd:run()
Run a remote command (array form)
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to: ssh me@example.com systemctl --user status pipewire.service
local cmd = Ssh.exec("example.com", { "systemctl", "--user", "status", "pipewire.service" }, {
user = "me",
})
Capture stdout from a remote command
local Ssh = require("wardlib.app.ssh").Ssh
local out = require("wardlib.tools.out")
local hostname = out.cmd(Ssh.exec("example.com", { "hostname" }, { user = "me", batch = true }))
:label("ssh hostname")
:trim()
:line()
-- hostname is a single line string
Hardened non-interactive connection (BatchMode + host key policy)
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to:
-- ssh \
-- -o BatchMode=yes \
-- -o StrictHostKeyChecking=accept-new \
-- -o UserKnownHostsFile=/tmp/known_hosts \
-- -o ConnectTimeout=5 \
-- me@example.com 'uname -a'
local cmd = Ssh.exec("example.com", "uname -a", {
user = "me",
batch = true,
strict_host_key_checking = "accept-new",
known_hosts_file = "/tmp/known_hosts",
connect_timeout = 5,
})
Copy local → remote (scp)
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to: scp -P 2200 -C ./build.tar.gz me@example.com:/tmp/build.tar.gz
local cmd = Ssh.copy_to("example.com", "./build.tar.gz", "/tmp/build.tar.gz", {
user = "me",
port = 2200,
compress = true,
})
Copy remote → local (scp)
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to: scp -P 2200 me@example.com:/var/log/syslog ./syslog
local cmd = Ssh.copy_from("example.com", "/var/log/syslog", "./syslog", {
user = "me",
port = 2200,
})
Use Ssh.scp directly (recursive directory copy)
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to: scp -r ./dist me@example.com:/srv/app/dist
local cmd = Ssh.scp("./dist", "me@example.com:/srv/app/dist", {
recursive = true,
})
Advanced flags via extra
local Ssh = require("wardlib.app.ssh").Ssh
-- Equivalent to: ssh -vv -o LogLevel=ERROR me@example.com 'true'
local cmd = Ssh.exec("example.com", "true", {
user = "me",
extra = { "-vv", "-o", "LogLevel=ERROR" },
})
swap
Wrappers around Linux swap tooling:
mkswap— initialize a swap areaswapon— enable swapswapoff— disable swap
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Most swap operations require elevated privileges. Prefer using
wardlib.tools.with with the sudo/doas middleware.
Import
local Swap = require("wardlib.app.swap").Swap
API
Swap.mkswap(target, opts)
Builds: mkswap <opts...> <target>
Swap.swapon(targets, opts)
Builds: swapon <opts...> [targets...]
- If
targetsisnil, runsswaponwith only modeled options (useful for--show). - If
targetsis a string array, all targets are appended.
Swap.swapoff(targets, opts)
Builds: swapoff <opts...> [targets...]
- If
targetsisnil, runsswapoffwith only modeled options (useful for-a).
Swap.status(opts)
Convenience: swapon --show.
Swap.disable_all(opts)
Convenience: swapoff -a.
Options
MkswapOpts
label: string?—-L <label>uuid: string?—-U <uuid>pagesize: number?—--pagesize <size>force: boolean?—-fcheck: boolean?—-c(check bad blocks)extra: string[]?— extra argv appended after modeled options
SwaponOpts
all: boolean?—-adiscard: string?—--discard[=<policy>]- pass
""(empty string) to emit bare--discard
- pass
fixpgsz: boolean?—--fixpgszpriority: number?—-p <prio>
Formatting for --show:
show: boolean?—--shownoheadings: boolean?—--noheadingsraw: boolean?—--rawbytes: boolean?—--bytesoutput: string|string[]?—--output <cols>(array is joined by,)
Escape hatch:
extra: string[]?— extra argv appended after modeled options
SwapoffOpts
all: boolean?—-averbose: boolean?—-vextra: string[]?— extra argv appended after modeled options
Examples
Create swap on a file (mkswap + swapon)
This example shows the command construction only. File allocation (fallocate/dd) is handled elsewhere.
local Swap = require("wardlib.app.swap").Swap
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), function()
-- mkswap -f -L swap0 /swapfile
Swap.mkswap("/swapfile", { force = true, label = "swap0" }):run()
-- swapon /swapfile
Swap.swapon("/swapfile"):run()
end)
Disable a specific swap and remove it from service
local Swap = require("wardlib.app.swap").Swap
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), function()
-- swapoff /swapfile
Swap.swapoff("/swapfile"):run()
end)
Show active swaps
local Swap = require("wardlib.app.swap").Swap
-- swapon --show --noheadings --output NAME,SIZE,USED,PRIO
local cmd = Swap.status({
noheadings = true,
output = { "NAME", "SIZE", "USED", "PRIO" },
})
-- cmd:output().stdout contains the table. Use wardlib.tools.out if you need parsing.
Enable all swaps from /etc/fstab
local Swap = require("wardlib.app.swap").Swap
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), Swap.swapon(nil, { all = true }))
:run()
Disable all swaps
local Swap = require("wardlib.app.swap").Swap
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), Swap.disable_all())
:run()
swaybg
swaybg sets wallpapers/backgrounds for Wayland compositors based on wlroots
(e.g. Sway).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
swaybgis typically a long-running process (it stays alive to keep the background set).- The wrapper only builds the command. Starting it in the background or managing its lifecycle is the caller’s responsibility.
Import
local Swaybg = require("wardlib.app.swaybg").Swaybg
API
Swaybg.set(image, mode, color)
Convenience for a single output.
Builds: swaybg -i <image> [-m <mode>] [-c <color>]
Swaybg.run(opts)
Full control. Builds one or more output blocks.
Builds: swaybg (-o <name> -i <image> [-m <mode>] [-c <color>])+ <extra...>
Options
SwaybgMode
One of:
"stretch" | "fill" | "fit" | "center" | "tile"
SwaybgOutput
name: string?— output name (e.g."DP-1")image: string— path to imagemode: SwaybgMode?— scaling modecolor: string?— background color (hex or name)
SwaybgOpts
outputs: SwaybgOutput|SwaybgOutput[]— one or more output specs (required)extra: string[]?— extra argv appended after modeled options
Examples
Set wallpaper for the default output
local Swaybg = require("wardlib.app.swaybg").Swaybg
-- Equivalent to: swaybg -i wall.png -m fill -c #000000
local cmd = Swaybg.set("wall.png", "fill", "#000000")
Set wallpapers per-output
local Swaybg = require("wardlib.app.swaybg").Swaybg
-- Equivalent to:
-- swaybg -o DP-1 -i a.png -m fit \
-- -o HDMI-A-1 -i b.png -m fill -c #111111
local cmd = Swaybg.run({
outputs = {
{ name = "DP-1", image = "a.png", mode = "fit" },
{ name = "HDMI-A-1", image = "b.png", mode = "fill", color = "#111111" },
},
})
Pass-through flags via extra
local Swaybg = require("wardlib.app.swaybg").Swaybg
-- Example: swaybg -i wall.png -m fill --some-flag
local cmd = Swaybg.run({
outputs = { image = "wall.png", mode = "fill" },
extra = { "--some-flag" },
})
systemd
wardlib.app.systemd is a thin wrapper around:
systemctljournalctl
It returns ward.process.cmd(...) objects.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
For predictable parsing of stdout/stderr, use wardlib.tools.out.
For scoped privilege escalation, use wardlib.tools.with.
Import
local Systemd = require("wardlib.app.systemd").Systemd
API
Systemd.start(unit, opts)
Builds: systemctl [--user] start <unit>
Systemd.stop(unit, opts)
Builds: systemctl [--user] stop <unit>
Systemd.restart(unit, opts)
Builds: systemctl [--user] restart <unit>
Systemd.reload(unit, opts)
Builds: systemctl [--user] reload <unit>
Systemd.enable(unit, opts)
Builds: systemctl [--user] enable [--now] <unit>
Systemd.disable(unit, opts)
Builds: systemctl [--user] disable [--now] <unit>
Systemd.is_active(unit, opts)
Builds: systemctl [--user] is-active <unit>
Systemd.is_enabled(unit, opts)
Builds: systemctl [--user] is-enabled <unit>
Systemd.status(unit, opts)
Builds: systemctl [--user] status [--no-pager] [--full] <unit>
Note: opts.no_pager defaults to true.
Systemd.daemon_reload(opts)
Builds: systemctl [--user] daemon-reload
Systemd.journal(unit, opts)
Builds: journalctl [--user] [--no-pager] [-u <unit>] [-f] [-n <lines>] [--since <time>] [--until <time>] [-p <prio>] [-o <format>]
Note: opts.no_pager defaults to true.
Options
SystemdCommonOpts
user: boolean?— use per-user manager (--user)
SystemdEnableDisableOpts (extends SystemdCommonOpts)
now: boolean?— start/stop unit immediately (--now)
SystemdStatusOpts (extends SystemdCommonOpts)
no_pager: boolean?—--no-pager(defaults totrue)full: boolean?—--full
SystemdJournalOpts (extends SystemdCommonOpts)
follow: boolean?—-flines: integer?—-n <lines>since: string?—--since <time>until: string?—--until <time>priority: string?—-p <prio>(e.g."err","warning","info","3")no_pager: boolean?—--no-pager(defaults totrue)output: string?—-o <format>(e.g."short-iso","cat","json")
Examples
Basic operations
local Systemd = require("wardlib.app.systemd").Systemd
-- systemctl restart nginx.service
Systemd.restart("nginx.service")
-- systemctl --user enable --now syncthing.service
Systemd.enable("syncthing.service", { user = true, now = true })
-- journalctl -u nginx.service -f -n 200
Systemd.journal("nginx.service", { follow = true, lines = 200, since = "yesterday" })
Running system service operations under sudo
local Systemd = require("wardlib.app.systemd").Systemd
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), function()
Systemd.daemon_reload():run()
Systemd.restart("nginx.service"):run()
end)
Query unit state (stdout parsing)
systemctl is-active and is-enabled encode state in both stdout and exit code.
A common pattern is to allow non-zero exit codes and parse the single-line output.
local Systemd = require("wardlib.app.systemd").Systemd
local out = require("wardlib.tools.out")
local active_state = out.cmd(Systemd.is_active("nginx.service"))
:label("systemctl is-active nginx.service")
:allow_fail()
:trim()
:line()
-- Examples: "active", "inactive", "failed", ...
local enabled_state = out.cmd(Systemd.is_enabled("nginx.service"))
:label("systemctl is-enabled nginx.service")
:allow_fail()
:trim()
:line()
-- Examples: "enabled", "disabled", "static", ...
Journal as JSON Lines
journalctl -o json emits one JSON object per line (NDJSON). Use out:json_lines().
local Systemd = require("wardlib.app.systemd").Systemd
local out = require("wardlib.tools.out")
local entries = out.cmd(Systemd.journal("nginx.service", { output = "json", lines = 50 }))
:label("journalctl -u nginx.service -o json -n 50")
:json_lines()
-- Each entry is a decoded Lua table. For example:
-- for _, e in ipairs(entries) do print(e.MESSAGE) end
Wait until a unit becomes active
Combine wardlib.tools.retry with systemctl is-active parsing.
local Systemd = require("wardlib.app.systemd").Systemd
local out = require("wardlib.tools.out")
local retry = require("wardlib.tools.retry")
retry.try(function()
local state = out.cmd(Systemd.is_active("nginx.service"))
:allow_fail()
:trim()
:line()
if state ~= "active" then
error("nginx.service is not active yet: " .. state)
end
end, { retries = 30, delay = 0.5 })
tofi
tofi is a Wayland-native menu / launcher. Some distributions ship helper
binaries such as tofi-run and tofi-drun.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Tofi = require("wardlib.app.tofi").Tofi
API
Tofi.bin
Executable name or path (default: "tofi").
Tofi.bin_run
Executable name or path (default: "tofi-run").
Tofi.bin_drun
Executable name or path (default: "tofi-drun").
Tofi.menu(opts)
Builds: tofi <opts...>
Tofi.run(opts)
Builds: tofi-run <opts...>
Tofi.drun(opts)
Builds: tofi-drun <opts...>
Options
TofiOpts
config: string?→-c <file>prompt_text: string?→--prompt-text <text>num_results: number?→--num-results <n>require_match: boolean?→--require-matchfuzzy_match: boolean?→--fuzzy-matchwidth: string?→--width <w>height: string?→--height <h>font: string?→--font <font>defines: table<string, any>?→ additional--key <value>pairs- stable key order
trueemits--keyfalse/nilare skipped
extra: string[]?→ extra argv appended after modeled options
Examples
tofi-run with a prompt
local Tofi = require("wardlib.app.tofi").Tofi
Tofi.run({
config = "tofi.conf",
prompt_text = "Run",
require_match = true,
}):output()
traceroute
traceroute discovers the network path to a destination by sending probes
with increasing TTL/hop-limit.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
- Address family flags
-4and-6are mutually exclusive. - Probe type flags are mutually exclusive:
-I(ICMP),-T(TCP),-U(UDP). - Use
extrato access unmodeled options; traceroute implementations vary across distributions.
Import
local Traceroute = require("wardlib.app.traceroute").Traceroute
API
Traceroute.trace(host, opts)
Builds: traceroute <opts...> <host> [packetlen]
If opts.packetlen is provided, it is appended after <host>.
Options (TracerouteOpts)
inet4: boolean?—-4inet6: boolean?—-6numeric: boolean?—-nas_lookup: boolean?—-A(AS number lookups)
Probe type (mutually exclusive):
icmp: boolean?—-I(ICMP ECHO)tcp: boolean?—-T(TCP SYN)udp: boolean?—-U(UDP)
Other controls:
method: string?—-M <method>(implementation-specific)interface: string?—-i <ifname>source: string?—-s <addr>first_ttl: number?—-f <n>max_ttl: number?—-m <n>queries: number?—-q <n>wait: number?—-w <sec>(wait for response)pause: number?—-z <sec>(pause between probes)port: number?—-p <port>do_not_fragment: boolean?—-F
Positional:
packetlen: number?— final packet length argument
Escape hatch:
extra: string[]?— extra argv appended after modeled options
Examples
local Traceroute = require("wardlib.app.traceroute").Traceroute
-- traceroute -n -I -m 16 -q 1 -w 2 example.com
local icmp = Traceroute.trace("example.com", {
numeric = true,
icmp = true,
max_ttl = 16,
queries = 1,
wait = 2,
})
-- traceroute -4 -T -p 443 1.1.1.1 60
local tcp = Traceroute.trace("1.1.1.1", {
inet4 = true,
tcp = true,
port = 443,
packetlen = 60,
})
-- traceroute -6 -n -U ipv6.google.com
local ipv6 = Traceroute.trace("ipv6.google.com", {
inet6 = true,
numeric = true,
udp = true,
})
unzip
unzip extracts and inspects zip archives (Info-ZIP).
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Safety note:
- Info-ZIP
unzipdoes not support--as end-of-options. For safety, this wrapper rejects zip paths (and optional file lists) that start with-. opts.passwordusesunzip -P <password>, which may be visible to other users on multi-user systems.
Import
local Unzip = require("wardlib.app.unzip").Unzip
API
Unzip.extract(zip_path, opts)
Builds: unzip <opts...> <zip_path> [files...] [-x <exclude...>] [-d <to>]
Unzip.list(zip_path, opts)
Builds: unzip -l <opts...> <zip_path> ...
Unzip.test(zip_path, opts)
Builds: unzip -t <opts...> <zip_path> ...
Unzip.raw(argv, opts)
Builds: unzip <opts...> <argv...>
Options (UnzipOpts)
to: string?— destination directory (-d <dir>)files: string|string[]?— optional file list to extractexclude: string|string[]?— patterns appended after-xoverwrite: boolean?—-onever_overwrite: boolean?—-n(mutually exclusive withoverwrite)quiet: boolean?—-qjunk_paths: boolean?—-jlist: boolean?—-l(low-level; preferUnzip.list())test: boolean?—-t(low-level; preferUnzip.test())password: string?—-P <password>extra: string[]?— extra argv appended after modeled options
Examples
Extract into a destination directory
local Unzip = require("wardlib.app.unzip").Unzip
-- unzip -o a.zip -d out
local cmd = Unzip.extract("a.zip", { overwrite = true, to = "out" })
Extract only specific files and exclude patterns
local Unzip = require("wardlib.app.unzip").Unzip
-- unzip a.zip x y -x "*.tmp" "*.bak" -d out
local cmd = Unzip.extract("a.zip", {
files = { "x", "y" },
exclude = { "*.tmp", "*.bak" },
to = "out",
})
List contents
local Unzip = require("wardlib.app.unzip").Unzip
-- unzip -q -l a.zip
local cmd = Unzip.list("a.zip", { quiet = true })
Test zip integrity
local Unzip = require("wardlib.app.unzip").Unzip
-- unzip -t a.zip
local cmd = Unzip.test("a.zip")
wget
wget is a command-line file downloader.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
Wget.fetch(nil, { input_file = "urls.txt" })is the idiomatic way to build a command that reads URLs from an input file.- GNU wget option surface is large; this wrapper models a practical subset and
provides
extrafor the rest.
Import
local Wget = require("wardlib.app.wget").Wget
API
Wget.fetch(urls, opts)
Builds: wget <opts...> [urls...]
- If
urlsisnil, runs wget with only the configured options (useful with-i). - If
urlsis a string array, all URLs are appended.
Wget.download(url, out, opts)
Convenience for a single URL.
- If
outis provided, setsopts.output_document(-O <out>).
Wget.mirror_site(url, dir, opts)
Convenience for mirroring.
- Always sets
opts.mirror = true(-m). - If
diris provided, setsopts.directory_prefix(-P <dir>).
Options (WgetOpts)
Verbosity:
quiet: boolean?—-qverbose: boolean?—-vno_verbose: boolean?—-nv
Download behavior:
continue: boolean?—-ctimestamping: boolean?—-Nno_clobber: boolean?—-ncspider: boolean?—--spider
Output / input:
output_document: string?—-O <file>directory_prefix: string?—-P <dir>input_file: string?—-i <file>
HTTP:
user_agent: string?—-U <ua>header: string|string[]?—--header=<h>(repeatable)method: string?—--method=<method>post_data: string?—--post-data=<data>post_file: string?—--post-file=<file>body_data: string?—--body-data=<data>body_file: string?—--body-file=<file>no_check_certificate: boolean?—--no-check-certificate
Networking:
inet4_only: boolean?—-4inet6_only: boolean?—-6timeout: number?—--timeout=<sec>wait: number?—--wait=<sec>tries: number?—--tries=<n>
Recursion / mirroring:
recursive: boolean?—-rlevel: number?—-l <n>no_parent: boolean?—-npmirror: boolean?—-mpage_requisites: boolean?—-pconvert_links: boolean?—-kadjust_extension: boolean?—-E
Escape hatch:
extra: string[]?— extra argv appended after modeled options
Examples
Download a file
local Wget = require("wardlib.app.wget").Wget
-- wget -O out.bin https://example.com/file
local cmd = Wget.download("https://example.com/file", "out.bin")
Download multiple URLs into a directory
local Wget = require("wardlib.app.wget").Wget
-- wget -P /tmp/downloads https://a https://b
local cmd = Wget.fetch({
"https://example.com/a",
"https://example.com/b",
}, { directory_prefix = "/tmp/downloads" })
Add custom headers and timeouts
local Wget = require("wardlib.app.wget").Wget
-- wget -q --timeout=10 --header="Accept: */*" https://example.com
local cmd = Wget.fetch("https://example.com", {
quiet = true,
timeout = 10,
header = "Accept: */*",
})
Read URLs from a file
local Wget = require("wardlib.app.wget").Wget
-- wget -i urls.txt -P out
local cmd = Wget.fetch(nil, { input_file = "urls.txt", directory_prefix = "out" })
Mirror a website into a directory
local Wget = require("wardlib.app.wget").Wget
-- wget -m -P /tmp/site https://example.com
local cmd = Wget.mirror_site("https://example.com", "/tmp/site")
wofi
wofi is a Wayland-native menu / launcher.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Wofi = require("wardlib.app.wofi").Wofi
API
Wofi.bin
Executable name or path (default: "wofi").
Wofi.dmenu(opts)
Builds: wofi <opts...> --dmenu
Wofi.show(mode, opts)
Builds: wofi <opts...> --show <mode>
Options
WofiOpts
conf: string?→--conf <file>style: string?→--style <file>prompt: string?→--prompt <text>term: string?→--term <terminal>cache_file: string?→--cache-file <file>width: string?→--width <width>height: string?→--height <height>lines: number?→--lines <n>columns: number?→--columns <n>insensitive: boolean?→--insensitiveshow_icons: boolean?→--allow-imagesallow_markup: boolean?→--allow-markupgtk_dark: boolean?→--gtk-darknormal_window: boolean?→--normal-windowextra: string[]?→ extra argv appended after modeled options
Examples
--show drun
local Wofi = require("wardlib.app.wofi").Wofi
Wofi.show("drun", { prompt = "Run" }):output()
--dmenu
local Wofi = require("wardlib.app.wofi").Wofi
Wofi.dmenu({ prompt = "Pick" }):output()
wl-copy / wl-paste
wardlib.app.wlcopy is a thin wrapper around Wayland clipboard tools:
wl-copywl-paste
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
wl-copyreads data from stdin. This module only builds the command; feeding stdin is the caller’s responsibility.- Use
wardlib.tools.outif you want predictable parsing ofwl-pasteoutput.
Import
local Clipboard = require("wardlib.app.wlcopy").Clipboard
API
Clipboard.copy(opts)
Builds: wl-copy <opts...>
Clipboard.clear(opts)
Convenience: Clipboard.copy({ clear = true, selection = ... }).
Clipboard.paste(opts)
Builds: wl-paste <opts...>
Options
ClipboardSelectionOpts
selection: "clipboard"|"primary"|nilnil/"clipboard"uses the default clipboard"primary"sets--primary
ClipboardCopyOpts (extends ClipboardSelectionOpts)
type: string?— MIME type (--type <mime>)foreground: boolean?—--foregroundpaste_once: boolean?—--paste-onceclear: boolean?—--clearextra: string[]?— extra argv appended
ClipboardPasteOpts (extends ClipboardSelectionOpts)
type: string?— MIME type (--type <mime>)no_newline: boolean?—--no-newlineextra: string[]?— extra argv appended
Examples
Copy from stdin (default clipboard)
local Clipboard = require("wardlib.app.wlcopy").Clipboard
-- wl-copy
local cmd = Clipboard.copy()
-- Example (pseudo; feeding stdin depends on your Ward build):
-- cmd:stdin("hello\n"):run()
Copy to primary selection, set MIME type
local Clipboard = require("wardlib.app.wlcopy").Clipboard
-- wl-copy --primary --type text/plain
local cmd = Clipboard.copy({ selection = "primary", type = "text/plain" })
Copy and keep wl-copy in foreground until paste
local Clipboard = require("wardlib.app.wlcopy").Clipboard
-- wl-copy --foreground
local cmd = Clipboard.copy({ foreground = true })
Paste (no trailing newline)
local Clipboard = require("wardlib.app.wlcopy").Clipboard
local out = require("wardlib.tools.out")
-- wl-paste --no-newline
local text = out.cmd(Clipboard.paste({ no_newline = true }))
:trim()
:line()
Clear selection
local Clipboard = require("wardlib.app.wlcopy").Clipboard
-- wl-copy --clear
local cmd = Clipboard.clear()
-- wl-copy --primary --clear
local cmd2 = Clipboard.clear({ selection = "primary" })
Extra flags pass-through
local Clipboard = require("wardlib.app.wlcopy").Clipboard
-- Example: wl-paste --primary --type text/plain --watch
local cmd = Clipboard.paste({
selection = "primary",
type = "text/plain",
extra = { "--watch" }, -- if supported by your wl-paste build
})
xargs
xargs builds and executes command lines from standard input.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
- GNU/BSD
xargsdiffer slightly. Useextrato access unmodeled features. - When using NUL-separated input (
-0), ensure the producer uses-print0/--print0.
Import
local Xargs = require("wardlib.app.xargs").Xargs
API
Xargs.run(cmd, opts)
Builds: xargs <opts...> [-- <cmd...>]
- If
cmdis nil,xargsexecutes its default command (commonlyecho, implementation-dependent). - When
cmdis provided, this wrapper emits--before the command.
Xargs.raw(argv, opts)
Builds: xargs <opts...> <argv...>
Options (XargsOpts)
Common fields:
- Input parsing:
null_input (-0)ordelimiter (-d <delim>)(mutually exclusive) - Limits:
max_args (-n),max_procs (-P),max_chars (-s) - Behavior:
no_run_if_empty (-r)(GNU) - Replacement:
replace_str (-I <str>) - Debug:
verbose (-t),show_limits (--show-limits)(GNU) - Escape hatch:
extra
Examples
local Xargs = require("wardlib.app.xargs").Xargs
-- xargs -n 10 -t -- echo {}
local cmd1 = Xargs.run({ "echo", "{}" }, { max_args = 10, verbose = true })
-- xargs -0 -P 4 -- rm -f
local cmd2 = Xargs.run({ "rm", "-f" }, { null_input = true, max_procs = 4 })
Pairing with NUL-separated producers
local Find = require("wardlib.app.find").Find
local Xargs = require("wardlib.app.xargs").Xargs
-- find . -type f -name "*.tmp" -print0 | xargs -0 -- rm -f
local find = Find.run(".", { type = "f", name = "*.tmp", print0 = true })
local xargs = Xargs.run({ "rm", "-f" }, { null_input = true })
-- Pipeline wiring depends on your Ward build; the important part is the argv construction.
xbps
app.xbps is a thin wrapper around Void Linux XBPS tooling
(xbps-install, xbps-remove, xbps-query) that constructs
ward.process.cmd(...) invocations.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Xbps = require("wardlib.app.xbps").Xbps
Running with elevated privileges
Most xbps-install / xbps-remove operations require root. Instead of passing
{ sudo = true } to this module (not supported), use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Xbps = require("wardlib.app.xbps").Xbps
-- sudo -n xbps-install -S
w.with(w.middleware.sudo(), Xbps.sync()):run()
API
All functions return a ward.Cmd.
Xbps.install(pkgs, opts)
Builds: xbps-install <common opts...> <install opts...> <pkgs...>.
Xbps.sync(opts)
Builds: xbps-install <common opts...> -S.
Xbps.upgrade(opts)
Builds: xbps-install <common opts...> <install opts...> -Su.
Xbps.remove(pkgs, opts)
Builds: xbps-remove <common opts...> <remove opts...> <pkgs...>.
Xbps.remove_orphans(opts)
Builds: xbps-remove <common opts...> [-y] -o.
Xbps.clean_cache(opts, all)
Builds: xbps-remove <common opts...> [-y] -O.
If all = true, uses -OO.
Xbps.search(pattern, opts)
Builds: xbps-query <common opts...> [--regex] -Rs <pattern>.
Xbps.info(pkg, opts)
Builds: xbps-query <common opts...> -S <pkg>.
Xbps.list_installed(opts)
Builds: xbps-query <common opts...> -l.
Xbps.list_manual(opts)
Builds: xbps-query <common opts...> -m.
Options
XbpsCommonOpts
Modeled fields:
rootdir(string):-r <dir>config(string):-C <dir>cachedir(string):-c <dir>repositories(string[]): repeatable--repository <url>extra(string[]): extra args appended after modeled options
XbpsInstallOpts
Extends XbpsCommonOpts.
Modeled fields:
yes(boolean):-yautomatic(boolean):-Aforce(boolean):-f
XbpsRemoveOpts
Extends XbpsCommonOpts.
Modeled fields:
yes(boolean):-yrecursive(boolean):-Rforce(boolean):-fdry_run(boolean):-n
XbpsSearchOpts
Extends XbpsCommonOpts.
Modeled fields:
regex(boolean):--regex
Examples
local w = require("wardlib.tools.with")
local Xbps = require("wardlib.app.xbps").Xbps
-- sudo -n xbps-install -y -Su
w.with(w.middleware.sudo(), Xbps.upgrade({ yes = true })):run()
-- sudo -n xbps-install -y curl git
w.with(w.middleware.sudo(), Xbps.install({ "curl", "git" }, { yes = true })):run()
-- xbps-query --regex -Rs '^lua'
local r = Xbps.search("^lua", { regex = true }):output()
xz
xz compresses and decompresses files using LZMA2.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Notes:
Xz.run()always inserts--before paths.opts.levelis modeled as0..9and is emitted as-<level>.
Import
local Xz = require("wardlib.app.xz").Xz
API
Xz.run(paths, opts)
Builds: xz <opts...> -- <paths...>
Xz.compress(paths, opts)
Convenience: compression (ensures decompress = false).
Xz.decompress(paths, opts)
Convenience: decompression (-d).
Xz.raw(argv, opts)
Builds: xz <opts...> <argv...>
Options (XzOpts)
decompress: boolean?—-dkeep: boolean?—-kforce: boolean?—-fstdout: boolean?—-cverbose: boolean?—-vquiet: boolean?—-qextreme: boolean?—-elevel: integer?— compression level-0..-9threads: integer?—-T <n>(0 = auto)extra: string[]?— extra argv appended after modeled options
Examples
Compress with level and threads
local Xz = require("wardlib.app.xz").Xz
-- xz -e -6 -T 0 -- data.json
local cmd = Xz.compress("data.json", { level = 6, extreme = true, threads = 0 })
Decompress
local Xz = require("wardlib.app.xz").Xz
-- xz -d -k -- data.json.xz
local cmd = Xz.decompress("data.json.xz", { keep = true })
Write decompressed data to stdout
local Xz = require("wardlib.app.xz").Xz
-- xz -d -c -- file.xz
local cmd = Xz.decompress("file.xz", { stdout = true })
Pass-through extra xz flags
local Xz = require("wardlib.app.xz").Xz
-- xz --check=crc64 -- file
local cmd = Xz.run("file", { extra = { "--check=crc64" } })
yay
app.yay is a thin wrapper around yay (an Arch AUR helper) that constructs
ward.process.cmd(...) invocations.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Yay = require("wardlib.app.yay").Yay
Notes on privileges
yay typically invokes sudo internally when needed (depending on your
configuration). This wrapper does not provide a { sudo = true } option.
If you intentionally want to run yay itself under sudo / doas, wrap the
returned command using wardlib.tools.with:
local w = require("wardlib.tools.with")
local Yay = require("wardlib.app.yay").Yay
w.with(w.middleware.sudo(), Yay.upgrade()):run()
API
All functions return a ward.Cmd.
Yay.sync(opts)
Builds: yay -Sy (or -Syy if opts.refresh = true).
Yay.upgrade(opts)
Builds: yay -Syu (or -Syyu if opts.refresh = true).
Yay.install(pkgs, opts)
Builds: yay -S <pkgs...> (plus modeled options).
Yay.remove(pkgs, opts)
Builds: yay -R[flags] <pkgs...>.
Flags are derived from:
opts.nosave=>nopts.recursive=>sopts.cascade=>c
Yay.search(pattern, opts)
Builds: yay -Ss <pattern>.
Yay.info(pkg, opts)
Builds: yay -Qi <pkg>.
Options
YayCommonOpts
Modeled fields:
needed(boolean):--needednoconfirm(boolean):--noconfirmextra(string[]): extra args appended after modeled options
YaySyncOpts
Extends YayCommonOpts.
Modeled fields:
refresh(boolean): uses-Syy/-Syyuinstead of-Sy/-Syu
YayRemoveOpts
Extends YayCommonOpts.
Modeled fields:
recursive(boolean): includesflag (-Rs)nosave(boolean): includenflag (-Rn)cascade(boolean): includecflag (-Rc)
Examples
local Yay = require("wardlib.app.yay").Yay
-- yay -Syu
Yay.upgrade():run()
-- yay -S --needed --noconfirm google-chrome
Yay.install("google-chrome", { needed = true, noconfirm = true }):run()
-- yay -Ss neovim
local r = Yay.search("neovim"):output()
zypper
zypper is the package manager for openSUSE and SUSE Linux Enterprise.
This module constructs
ward.process.cmd(...)invocations; it does not parse output. consumers can usewardlib.tools.out(or their own parsing) on the:output()result.
Import
local Zypper = require("wardlib.app.zypper").Zypper
Running with elevated privileges
Most zypper operations require root. Instead of passing { sudo = true } to
this module (not supported), use wardlib.tools.with.
local w = require("wardlib.tools.with")
local Zypper = require("wardlib.app.zypper").Zypper
-- sudo -n zypper refresh
w.with(w.middleware.sudo(), Zypper.refresh()):run()
API
Zypper.refresh(opts)
Builds: zypper <opts...> refresh
Zypper.install(pkgs, opts)
Builds: zypper <opts...> install <pkgs...>
Zypper.remove(pkgs, opts)
Builds: zypper <opts...> remove <pkgs...>
Zypper.update(pkgs, opts)
Builds: zypper <opts...> update [pkgs...]
If pkgs is nil, updates all packages.
Zypper.dup(opts)
Builds: zypper <opts...> dup
Zypper.search(term, opts)
Builds: zypper <opts...> search <term>
Zypper.info(pkgs, opts)
Builds: zypper <opts...> info <pkgs...>
Zypper.repos_list(opts)
Builds: zypper <opts...> repos
Zypper.addrepo(uri, alias, opts)
Builds: zypper <opts...> addrepo <uri> <alias>
Zypper.removerepo(alias, opts)
Builds: zypper <opts...> removerepo <alias>
Zypper.cmd(subcmd, argv, opts)
Generic helper for zypper <opts...> <subcmd> [argv...].
Zypper.raw(argv, opts)
Builds: zypper <opts...> <argv...>
Options (ZypperCommonOpts)
Modeled fields:
- Interactivity:
non_interactive (--non-interactive) - Logging:
quiet (-q),verbose (-v) - Metadata:
refresh (--refresh),no_refresh (--no-refresh)(mutually exclusive) - License handling:
auto_agree_with_licenses (--auto-agree-with-licenses) - GPG handling:
gpg_auto_import_keys (--gpg-auto-import-keys),no_gpg_checks (--no-gpg-checks) - Repo restriction:
repos (-r <alias>)(repeatable) - Escape hatch:
extra
Examples
local Zypper = require("wardlib.app.zypper").Zypper
-- zypper --non-interactive refresh
local cmd1 = Zypper.refresh({ non_interactive = true })
-- sudo -n zypper --non-interactive --auto-agree-with-licenses install curl jq
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), Zypper.install({ "curl", "jq" }, {
non_interactive = true,
auto_agree_with_licenses = true,
})):run()
-- zypper -r oss -r update search kernel
local cmd3 = Zypper.search("kernel", { repos = { "oss", "update" } })
tools.cli
tools.cli provides a small, Ward-native command-line argument parser for Lua scripts.
It is designed for:
- Declarative option/argument definitions (data-first spec).
- Common GNU-style flag forms:
--long,--long=value,--long value,-s,-svalue, and short bundles like-abc. - Automatic help generation (
-h/--help). - Optional version output (
--version/-V). - Optional subcommands (including nesting).
- Subcommand aliases (via
subcommand.aliases). - Grouped option sections in help (via
option.group). - Examples and an epilog section in help (via
spec.examplesandspec.epilog). - Subcommand help inherits parent
exampleswhen the subcommand does not define its own examples. - No side effects: it does not use
ioand it does not exit the process; it returns structured results/errors.
Quick example
local cli = require("wardlib.tools.cli")
local parser = cli.new({
name = "mytool",
version = "1.2.3",
summary = "Example tool",
options = {
{ id = "verbose", short = "v", long = "verbose", kind = "count", help = "Increase verbosity" },
{ id = "config", short = "c", long = "config", kind = "value", metavar = "FILE", help = "Config file" },
{ id = "dry_run", long = "dry-run", kind = "flag", help = "Do not apply changes" },
},
positionals = {
{ id = "input", metavar = "INPUT", kind = "value", required = true, help = "Input file" },
{ id = "rest", metavar = "ARGS", kind = "values", variadic = true, help = "Extra args" },
},
}, { auto_version = true })
local ok, out = parser:parse() -- defaults to global `arg`
if not ok then
-- out.code may be "help" or "version" (non-error exits), or an error code.
print(out.text)
return
end
print(out.values.verbose)
print(out.values.config)
print(out.positionals.input)
Subcommands example
local cli = require("wardlib.tools.cli")
local parser = cli.new({
name = "mytool",
summary = "Tool with subcommands",
subcommands = {
{
name = "run",
summary = "Run the tool",
options = {
{ id = "jobs", short = "j", long = "jobs", kind = "value", type = "int", default = 1, metavar = "N" },
},
positionals = {
{ id = "target", metavar = "TARGET", kind = "value", required = true },
},
},
},
})
local ok, out = parser:parse({ [0] = "mytool", "run", "--jobs", "4", "all" })
assert(ok)
-- Root values/positionals are in out.values / out.positionals.
-- Selected command details are in out.cmd.
print(out.cmd.name) -- "run"
print(out.cmd.values.jobs) -- 4
print(out.cmd.positionals.target) -- "all"
API
cli.new(spec, opts?) -> parser
Creates a parser.
opts:
auto_help(boolean, defaulttrue): injects-h/--helpif not defined.auto_version(boolean, defaultfalse): injects--version/-Vif not defined.
parser:parse(argv?, parse_opts?) -> (ok, result_or_err)
Parses arguments.
argv:nil(default): reads_G.arg.{"--foo", "bar"}: array form.{ [0] = "script.lua", "--foo", "bar" }: Luaarg-like form.
parse_opts:
start_index(number, default1): where to begin readingargv.allow_unknown(boolean, defaultfalse): if true, unknown options are appended toresult.rest.stop_at_first_positional(boolean, defaultfalse): if true, once a positional is seen, the remaining tokens are treated as positional/rest.on_event(event, state)(function|nil): event callback.
Return values:
-
On success:
ok=true, andresult:result.values(table): parsed option values byid.result.positionals(table): parsed positional values byid.result.rest(array): remaining/unconsumed tokens (when allowed).result.cmd(table|nil): selected subcommand parse (when subcommands are defined).result.argv0(string): program/script name when available.
-
On failure/help/version:
ok=false, anderr:err.code(string): e.g."help","version","unknown_option","missing_value","invalid_value","missing_required","unknown_command","option_repeated","too_many_occurrences","mutually_exclusive","missing_one_of".err.message(string)err.token(string|nil)err.text(string): formatted message plus usage/help.
parser:usage() -> string
Returns a single-line usage string.
parser:help(help_opts?) -> string
Returns a multi-section help string.
help_opts:
width(number, default100)include_description(boolean, defaulttrue)include_defaults(boolean, defaulttrue)
Help formatting
Help output is deterministic and organized into sections. Options may be grouped by setting group on each option.
When subcommands are used, auto-injected --help/-h and --version/-V are displayed under Common options if you define at least one explicit option group.
If spec.examples is provided, an Examples section is appended. For subcommands, if the subcommand does not define examples, the parent command’s examples are used (inheritance). If the subcommand defines examples, they override the parent examples.
If spec.epilog is provided, it is appended after all other sections.
examples entries may be:
- a string command line (printed verbatim), or
- a table
{ cmd = "...", desc = "..." }(or{ "...", "..." })
Spec schema
Top-level fields
-
name(string, required): program name used in usage/help. -
summary(string|nil): one-line description. -
description(string|nil): longer description. -
version(string|nil): version string (used by--versionwhenauto_version=true). -
options(array|nil): root options. -
positionals(array|nil): root positionals. -
subcommands(array|nil): list of subcommands. -
examples(array|nil): help examples (strings or{cmd, desc}tables). -
epilog(string|nil): additional text appended to help. -
constraints(table|nil): extra validation rules applied after parsing. See Constraints below.
Options
Each element of spec.options is a table:
-
id(string, required): key inresult.values. -
long(string|nil):--longname (without--). -
short(string|nil): single-letter-s. -
kind(string, default"flag"):"flag": boolean"count": increments per occurrence"value": consumes one value"values": consumes one value per occurrence into an array
-
type(string, default"string"):"string" | "number" | "int" | "enum" -
choices(array, enum only): allowed values -
default(any|nil) -
required(boolean|nil) -
metavar(string|nil): label shown in help for value options -
group(string|nil): option help section heading (default:"Options"). -
help(string|nil) -
negatable(boolean|nil): forkind="flag"only. If true,--no-<long>is accepted and sets the flag tofalse. -
repeatable(boolean, defaulttrue): if set tofalse, repeating the option produces anoption_repeatederror (applies toflag,value, andcount). -
max_count(number|nil): forkind="count"only. If set, exceeding the limit produces atoo_many_occurrenceserror. -
validate(value)(function|nil): custom validator; returntrueon success, orfalse, reasonto reject. A thrown error is surfaced as aninvalid_valueerror. -
on(value, event, state)(function|nil): called after parsing/coercion
Positionals
Each element of spec.positionals is a table:
-
id(string, required) -
metavar(string, required) -
kind(string, default"value"):"value" | "values" -
type(string, default"string") -
required(boolean|nil) -
variadic(boolean|nil): only valid for the last positional; consumes remaining tokens -
help(string|nil) -
validate(value)(function|nil): custom validator; returntrueon success, orfalse, reasonto reject. -
on(value, event, state)(function|nil)
Constraints
spec.constraints is optional and supports:
mutex(array of groups): each group is an array of optionids that are mutually exclusive. If more than one is present, parsing fails withcode="mutually_exclusive".one_of(array of groups): each group is an array of optionids where at least one must be present. If none are present, parsing fails withcode="missing_one_of".
Example:
constraints = {
mutex = { { "json", "yaml" } },
one_of = { { "input", "stdin" } },
}
Subcommands
Each element of spec.subcommands is a table:
-
name(string, required) -
summary(string|nil) -
description(string|nil) -
options(array|nil) -
positionals(array|nil) -
subcommands(array|nil): nested subcommands -
aliases(string|array|nil): alternative tokens that invoke this command (e.g.{ "r", "execute" }).
When a subcommand is selected, parsing continues with that subcommand spec. The nested parse result is returned in result.cmd.
config
wardlib.tools.config is a small, format-aware configuration IO layer.
Ward already provides codecs under ward.convert.* (JSON, YAML, TOML, INI).
This tool focuses on common scripting workflows around those codecs:
- infer format from file extension (or override explicitly)
- read/write config as a Lua table
- patch config in-place
- merge tables (shallow or deep) via Ward core helpers
Module
local config = require("wardlib.tools.config")
Supported formats
The format is inferred by file extension:
.json->json.yaml/.yml->yaml.toml->toml.ini->ini
You can also override the format via opts.format in read, write, and patch.
Examples
Read / write
local config = require("wardlib.tools.config")
config.write("app.json", { enabled = true }, { pretty = true, mkdir = true })
local doc = config.read("app.json")
assert(doc.enabled == true)
Patch in-place
local config = require("wardlib.tools.config")
config.patch("app.json", function(doc)
doc.port = 8080
-- return nil to mutate in place
end)
Merge
local config = require("wardlib.tools.config")
local base = { a = { x = 1, y = 1 } }
local overlay = { a = { y = 2 } }
local out = config.merge(base, overlay, { mode = "deep" })
assert(out.a.x == 1)
assert(out.a.y == 2)
API
config.infer_format(path) -> string|nil
Returns "json", "yaml", "toml", "ini", or nil if the extension is not recognized.
config.read(path, opts?) -> any
Reads path as text and decodes it using the inferred (or overridden) format.
Options:
format(string|nil) - override inferred format
Errors if the file does not exist.
config.write(path, value, opts?) -> boolean
Encodes value and writes it to path.
Returns:
truewhen it wrote the filefalsewhenwrite_if_changed=trueand the file already contained identical content
Options:
format(string|nil) - override inferred formatmkdir(boolean, defaultfalse) - create parent directorywrite_if_changed(boolean, defaultfalse) - skip writing when content is identicaleof_newline(boolean, defaulttrue) - ensure output ends with\n
JSON-only options:
pretty(boolean, defaultfalse)indent(integer|nil) - spaces per indent level
config.patch(path, fn, opts?) -> any
Reads config from path, calls fn(doc), and writes the result back.
Patch function semantics:
- if
fn(doc)returns a non-nil value, it is treated as the new document - if it returns
nil, the existingdocis assumed mutated in-place
Options:
- all
write()options allow_missing(boolean, defaulttrue) - if file is missing, start fromopts.defaultor{}default(any|nil) - initial document when file is missing
config.merge(base, overlay, opts?) -> table
Merges two tables using Ward core helpers:
mode = "deep"(default) usesward.helpers.table.deep_mergemode = "shallow"usesward.helpers.table.merge
dotfiles
Declarative dotfiles management for Ward scripts.
A dotfiles definition is an ordered list of explicit step records.
Ordering is always preserved because steps is a plain array.
Design goals:
- Deterministic: steps run in the exact order you provide.
- Safe by default: existing destinations are not modified unless you pass
force = true. - Best-effort revert: changes are recorded in a manifest under the target root.
Quick start
local dotfiles = require("wardlib.tools.dotfiles")
local tpl = require("ward.template.minijinja").render
local username = "alex"
local def = dotfiles.define("My dotfiles", {
description = "Minimal personal config",
steps = {
dotfiles.content(
".config/git/config",
tpl(
[[
[user]
name = {{ username }}
]],
{ username = username }
)
),
dotfiles.link(
".config/fish",
"~/.dotfiles/fish",
{ recursive = true }
),
dotfiles.assert(function(base)
-- guardrails
return base ~= "/"
end, "refusing to apply dotfiles into /"),
},
})
def:apply("/home/alex", { force = true })
-- def:revert("/home/alex")
Definitions
Create a definition with:
local dotfiles = require("wardlib.tools.dotfiles")
local def = dotfiles.define("Preset name", {
description = "Optional description",
defaults = { ... }, -- reserved for future use
steps = { ... }, -- required; must be a non-empty array
})
Notes:
nameis used for identification in the manifest.descriptionis recorded in the manifest; it does not affect behavior.defaultsis accepted and stored on the definition, but is not currently used by the engine.stepsmust be a non-empty array. Only step records (created by the functions below) and nested definitions are allowed.
Path rules
For any destination rel_path:
- Must be relative (no leading
/,\, orC:\...). - Must not contain parent traversal (
..). - Path separators are normalized internally.
For any source path in dotfiles.link():
~and~/...are expanded using$HOMEwhen available.- Both absolute and relative paths are allowed.
Conditional execution (common option)
Most step constructors accept opts and support conditional execution:
opts.when(base) -> booleanoropts.conditions(base) -> boolean
If provided and returns false, the step is skipped.
If the function errors, the apply operation errors.
base is the root path you passed to def:apply(base, ...).
Steps reference
dotfiles.content(rel_path, content, opts?)
Writes file content to base/rel_path.
-
contentmay be:- a
string - a function
(base, abs) -> string
- a
Overwrite behavior:
-
If destination does not exist: it is created (parent directories are created automatically).
-
If destination is a directory: the step errors.
-
If destination exists:
- without
force=trueondef:apply, the step errors - with
force=true, an existing file is overwritten; an existing symlink is unlinked first
- without
The tool never writes through an existing destination symlink.
There are no dotfiles-specific file mode/permissions options; use your existing
Ward FS tooling in a custom step if you need chmod/chown.
dotfiles.link(rel_path, source, opts?)
Creates a symlink at base/rel_path pointing to source.
Options:
-
recursive = true- If
sourceis a directory, the destination is ensured to be a directory. - The tool walks the source tree and creates symlinks for all non-directory entries.
- Intermediate directories are created under the destination.
- Source directory entries are processed in sorted order for determinism.
- If
Replacement policy flags (only relevant with def:apply(..., { force = true })):
-
replace_file(defaulttrue)- Allows replacing an existing regular file at the destination.
-
replace_symlink(defaulttrue)- Allows replacing an existing symlink at the destination.
-
replace_dir(defaultfalse)- Allows replacing an existing directory at the destination, but only if the directory is empty.
Notes:
- Without
force=true, any existing destination causes an error. - Destination symlinks are never followed. In force mode, an existing symlink is removed before creating the new link.
- If
recursive=true, directories in the source that are symlinks are treated as non-directories (a symlink is created for them).
dotfiles.custom(rel_path_or_nil, fn, opts?)
Runs custom logic.
Signature:
- If
rel_path_or_nilis a string,fn(base, abs)is called whereabs = base/rel_path_or_nil. - If
rel_path_or_nilisnil,fn(base, nil)is called.
Return values:
-
nil- Treated as an imperative step. It is recorded in the manifest as
kind = "exec"and is not revertable.
- Treated as an imperative step. It is recorded in the manifest as
-
string- Only valid when a path was provided. The string is treated as file content
and written to
abs(using the same overwrite rules asdotfiles.content).
- Only valid when a path was provided. The string is treated as file content
and written to
-
DotfilesDefinition- Applied as a nested definition.
-
table- If it has
steps = {...}: treated as a meta table and converted to a definition. - Otherwise: treated as a
steps[]array.
- If it has
Nested application rules:
- If a path was provided: nested steps are applied under that path (i.e.
absbecomes the nested base). - If no path was provided: nested steps are applied under
base.
dotfiles.include(prefix_or_nil, def_or_meta, opts?)
Includes another definition under an optional prefix.
-
def_or_metacan be:- a
DotfilesDefinitionreturned bydotfiles.define() - a meta table
{ name?, description?, defaults?, steps = {...} }
- a
-
If
prefix_or_nilis non-nil, the included definition is applied underbase/prefix_or_nil.
dotfiles.group(name, steps, opts?)
Pure organizational wrapper that preserves ordering.
stepsis a steps array.opts.when/conditionscan be used to gate the entire group.- The group name is for readability in code; it is not currently persisted in the manifest.
dotfiles.assert(predicate, message, opts?)
Fail-fast precondition check.
predicate(base) -> boolean- If it returns false, the apply operation errors with
message. - Assertions are recorded in the manifest but are not reverted.
Applying and reverting
def:apply(base, opts?)
Applies the definition into base.
Options:
-
force = true- Allows replacing existing files/symlinks and (with
replace_dir=true) empty directories. - Without
force=true, any destination that already exists causes an error.
- Allows replacing existing files/symlinks and (with
-
manifest_path = "..."- Override the manifest location.
- Default:
<base>/.ward/dotfiles-manifest.json.
Side effects and caveats:
- Parent directories are created automatically using
ward.fs.mkdir(..., { recursive = true }). - The dotfiles tool does not attempt atomic writes or compare content hashes; a write in force mode always overwrites the file.
- There is no built-in backup strategy.
Return value:
- Returns the manifest table that was written.
def:revert(base, opts?)
Reverts using the stored manifest.
Options:
manifest_path = "..."(must match what was used on apply).
Revert rules:
-
Only manifest entries that have
{ path = ..., prev = ... }are reverted.exec(imperative custom) andassertentries are not reverted.
-
If a path previously did not exist, revert removes it.
- Directories are removed only if they are empty.
-
If a path previously was a symlink, revert restores the previous symlink target.
-
If a path previously was a file, revert restores the previous content.
-
If a path previously was a directory, revert ensures the directory exists.
At the end of revert, the manifest file is removed. The <base>/.ward
directory is removed only if it becomes empty.
Manifest
By default the manifest is written to:
<base>/.ward/dotfiles-manifest.json
The manifest contains:
- definition metadata (
name, optionaldescription) applied_attimestampbase- a list of
entriesin apply order
Each revertable entry records a snapshot of the previous state:
absentfilewith previous contentsymlinkwith previous targetdir
Limitations
- Revert is best-effort and intentionally conservative; it does not delete non-empty directories.
- Recursive linking can create many manifest entries (one per created directory and one per linked leaf).
- Destination conflicts are treated as errors; there is no “plan” or “diff” mode yet.
tools.ensure
tools.ensure provides fail-fast contract checks for Ward scripts.
This is intentionally not a portability layer.
- It does not negotiate distro “flavors”, package managers, or versions.
- It does not attempt to auto-install missing tools.
- It does not claim idempotent “ensure state” semantics.
It simply makes script assumptions explicit and actionable.
Example
local ensure = require("wardlib.tools.ensure")
ensure.os("linux")
ensure.bins({ "git", "tar", "ssh" })
local token = ensure.env("TOKEN")
-- If the script needs privileged operations:
ensure.root()
API
ensure.bin(name_or_path, opts?) -> string
Ensure a binary exists (either an explicit path, or a name in PATH). Returns the resolved path when possible.
ensure.bins(bins, opts?) -> table
Ensure a set of binaries exist. Returns { [name] = resolved_path }.
ensure.env(keys, opts?) -> string | table
Ensure environment variable(s) exist.
- If passed a string, returns its value.
- If passed
string[], returns a map.
Options:
allow_empty(bool, defaultfalse) - treat empty string as missing.
ensure.root(opts?) -> true
Ensure the script is running as root (Unix).
ensure.os(allowed, opts?) -> string
Ensure current OS is allowed. Supported allowed values:
"linux""darwin""freebsd""openbsd""netbsd""windows""unix"
out
wardlib.tools.out provides a fluent, fail-fast way to parse ward.process
command output.
It wraps either:
- a
ward.processcommand object (lazy, executes once via:output()), or - an already captured
CmdResult.
The API is intentionally small: chainable configuration methods, followed by a terminal extractor.
Basic usage
local p = require("ward.process")
local out = require("wardlib.tools.out")
local sha = out.cmd(p.cmd("git", "rev-parse", "HEAD"))
:label("git rev-parse HEAD")
:trim()
:line()
Wrapping an existing CmdResult
local out = require("wardlib.tools.out")
local res = require("ward.process").cmd("uname", "-r"):output()
local kernel = out.res(res):trim():line()
Selecting stderr and allowing failures
By default, out requires res.ok == true and reads from stdout.
local out = require("wardlib.tools.out")
local p = require("ward.process")
local diagnostics = out.cmd(p.cmd("some-tool", "--diagnose"))
:stderr()
:allow_fail()
:text()
Structured decoders
If a command can output machine-readable data, prefer that and decode it.
local out = require("wardlib.tools.out")
local p = require("ward.process")
local data = out.cmd(p.cmd("ip", "-j", "addr"))
:label("ip -j addr")
:json()
Supported decoders (via ward.convert.*.decode):
:json():yaml():toml():ini()
JSON Lines (NDJSON)
Some commands emit one JSON object per line (also called NDJSON or JSON Lines), for example journalctl -o json.
local out = require("wardlib.tools.out")
local Systemd = require("wardlib.app.systemd").Systemd
local entries = out.cmd(Systemd.journal("nginx.service", { output = "json", lines = 50 }))
:label("journalctl -o json")
:json_lines()
Reference
Constructors
out.cmd(cmd)wraps a command (expectscmd:output()), executes lazily, caches the result.out.res(res)wraps a captured result table.
Chainable configuration
:label(string)used in error messages.:stdout()/:stderr()selects which stream to parse (default: stdout).:ok()requiresres.ok == true(default).:allow_fail()disables theokrequirement.:trim()/:ltrim()/:rtrim()whitespace trimming applied before extraction.:normalize_newlines(true|false)convert CRLF/CR to LF (default: true).:max_preview(nbytes)limit output previews in error messages (default: 2048).
Terminal extractors
:text()returns a string.:lines()returnsstring[]split by\n(ignores a trailing empty line when output ends with a newline).:line()returns a single line, errors when output is empty or multi-line.:match(pattern)returns captures likestring.match(errors when no match).:matches(pattern)returns all matches as an array.:json()/:yaml()/:toml()/:ini()decodes structured output.:json_lines()decodes newline-delimited JSON into an array.
tools.task
tools.task is a small, Ward-native task runner for organizing scripts into
named steps with dependencies.
It is designed for:
- Deterministic execution order (definition order plus explicit dependency order).
- Simple dependency graphs (DAG) with cycle detection.
- Conditional execution (
when) and dry runs. - Structured results suitable for CI/logging.
- No side effects: it does not print and it does not exit the process.
Non-goals (for now): parallel execution, persistent state storage, and tight
coupling to tools.cli.
Quick example
local task = require("wardlib.tools.task")
local r = task.runner({
default = "apply",
})
r:define("packages", function(ctx)
-- install packages...
end, { desc = "Install packages" })
r:define("dotfiles", function(ctx)
-- apply dotfiles...
end, { desc = "Apply dotfiles" })
r:define("apply", function(ctx)
-- umbrella task
end, {
desc = "Apply workstation setup",
deps = { "packages", "dotfiles" },
})
local ok, report = r:run() -- runs default task
if not ok then
-- report.failed > 0
error("Task run failed")
end
API
task.runner(opts?) -> runner
Creates a runner.
opts:
default(string|nil): task name used whenrunner:run()is called without an explicit task name.
runner:define(name, fn, meta?) -> runner
Defines a task.
name(string, required): task name.fn(ctx, runner)(function, required): task body.meta(table|nil):desc(string|nil): human-readable description.deps(string[]|nil): dependency task names.when(ctx, runner)(function|nil): if provided and returnsfalse, the task is skipped.
Task function return conventions:
nilortrue=> success (status="ok").{ status = "ok" }=> success.{ status = "skip", reason = "..." }=> skipped.{ status = "error", error = "..." }=> recorded as failure.- Any other return value => treated as success and stored as
value.
runner:list() -> taskinfo[]
Returns tasks in definition order. Each entry has:
namedescdeps
runner:plan(names?) -> (ok, plan_or_err)
Computes a deterministic execution plan.
namesmay be:nil: usesopts.default(error if no default is set).- a string: a single task name.
- a string[]: multiple tasks.
On success: ok=true, plan is a string[] of task names in execution order.
On failure: ok=false, err is a table with:
err.code(string): e.g."missing_task"or"cycle".err.message(string)
runner:run(names?, ctx?, run_opts?) -> (ok, report)
Runs the plan and returns a structured report.
names: same asrunner:plan.ctx(table|nil): user context passed to tasks.run_opts:dry_run(boolean|nil): if true, all tasks are skipped with reason"dry_run".fail_fast(boolean|nil): if true, stop after the first task failure.on_event(ev)(function|nil): event callback.
report contains:
ok(boolean)total,failed,skipped(numbers)plan(string[])results(array): one entry per planned task, in execution order:namestatus("ok"|"skip"|"error")reason(string|nil)error(string|nil)value(any|nil)duration(number|nil) seconds when timing is available
Events
If run_opts.on_event is provided, tools.task emits events:
runner_start:{ kind="runner_start", requested={...}, plan={...} }task_start:{ kind="task_start", name=..., index=i, total=n }task_end:{ kind="task_end", name=..., status=..., duration=..., result=... }runner_end:{ kind="runner_end", ok=..., failed=..., skipped=..., results=... }
tools.platform
tools.platform provides a small, cross-platform inspection API for scripts.
It delegates to Ward core ward.host.platform and adds a few conveniences that
are commonly needed in wardlib scripts:
is_ci()CI detection via common environment variableshome()best-effort home directory resolutiontmpdir()best-effort temporary directory resolutionos_release()parsing for Linux distributions (best-effort)
Usage
local platform = require("wardlib.tools.platform")
local info = platform.info()
print(info.os, info.arch, info.is_ci)
if platform.is_macos() then
-- macOS-specific logic
end
API
platform.info() -> table
Returns the Ward core ward.host.platform.info() table, extended with:
is_ci(boolean)home(string|nil)tmpdir(string|nil)
platform.is_ci() -> boolean
Returns true if the environment looks like CI.
platform.home() -> string|nil
Home directory detection.
Resolution order:
HOMEUSERPROFILEHOMEDRIVE+HOMEPATH
platform.tmpdir() -> string|nil
Temporary directory detection.
Resolution order:
TMPDIRTEMPTMP/tmpon Unix targets
platform.os_release(opts?) -> table|nil
Linux distribution metadata.
- Reads
/etc/os-release(or/usr/lib/os-releasefallback) and returns a parsed key/value table. - On non-Linux targets, returns
nil.
Options:
path(string): override file path
platform.parse_os_release(text) -> table
Parses an os-release formatted string into a table with lower-case keys (for example ID becomes id).
tools.retry
tools.retry is a small wardlib wrapper around Ward core ward.helpers.retry.
It provides a stable wardlib-facing API, option aliases, and a
should_retry(err) hook to stop retries early.
API
local retry = require("wardlib.tools.retry")
retry.call(fn, opts?) -> any
Runs fn() and retries when it raises an error.
Options (opts table, all optional):
triesorattempts(integer, default: Ward core default)- Only one of these may be specified.
- Values <= 0 are treated as 1.
delay(duration, default: Ward core default)- Passed through to Ward core. May be a number (milliseconds) or a duration
string (for example:
"100ms","2s").
- Passed through to Ward core. May be a number (milliseconds) or a duration
string (for example:
max_delay(duration)- Passed through to Ward core.
max(duration)- Alias for
max_delay.
- Alias for
backoff(number)- Passed through to Ward core.
jitter(boolean)- Passed through to Ward core.
jitter_ratio(number)- Passed through to Ward core.
should_retry(err)(function)- When set, this function is called with the error raised by
fn(). - If it returns
false, the retry loop stops immediately and the original error is raised.
- When set, this function is called with the error raised by
Example:
local retry = require("wardlib.tools.retry")
local value = retry.call(function()
local r = require("ward.process").cmd("curl", "-fsS", "https://example.com"):output()
if not r.ok then
error("network")
end
return r.stdout
end, {
tries = 5,
delay = "200ms",
backoff = 2.0,
max_delay = "3s",
jitter = true,
should_retry = function(err)
-- do not retry for hard failures
return tostring(err) ~= "bad credentials"
end,
})
retry.pcall(fn, opts?) -> (ok, value_or_err)
Like retry.call, but returns a boolean success flag.
local retry = require("wardlib.tools.retry")
local ok, v = retry.pcall(function()
error("boom")
end, { tries = 3, delay = 0 })
if not ok then
print("failed: " .. tostring(v))
end
tools.with
This module provides helpers to temporarily install a Ward process middleware and then automatically restore the previous middleware stack.
The most common use-case is running commands through a prefix
command (e.g. sudo / doas) without permanently affecting subsequent
commands.
w.with(middleware, fn, ...) -> ...
Runs fn(...) while middleware is installed.
local process = require("ward.process")
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo(), function()
process.cmd("id"):run()
process.cmd("ls", "-la"):run()
end)
w.with(prefix, fn, ...) -> ...
Convenience form: builds a prefix middleware and runs fn(...) under it.
local w = require("wardlib.tools.with")
w.with({"sudo", "-n"}, function()
require("ward.process").cmd("whoami"):run()
end)
w.with(prefix_or_mw, cmd) -> cmd_proxy
Wraps a cmd-like object, returning a proxy where any method call runs under the given prefix/middleware.
This provides a compact syntax:
local process = require("ward.process")
local w = require("wardlib.tools.with")
local ls = w.with(process.cmd("sudo"), process.cmd("ls", "-la"))
ls:run()
If your local Ward cmd(...) object doesn’t expose argv via
.argv / .spec.argv / ._spec.argv, pass an argv array instead:
local ls = w.with({"sudo", "-n"}, process.cmd("ls", "-la"))
ls:run()
w.middleware.prefix(prefix, opts)
Creates a middleware that prefixes spec.argv (or another field) with prefix.
Options:
sep(string|nil): insert a separator token between prefix and argv (often"--").field(string): which spec field to mutate (default"argv").
local w = require("wardlib.tools.with")
local mw = w.middleware.prefix({"sudo", "-n"}, { sep = "--" })
w.middleware.sudo(opts) / w.middleware.doas(opts)
Convenience constructors.
local w = require("wardlib.tools.with")
w.with(w.middleware.sudo({ preserve_env = true }), function()
require("ward.process").cmd("env"):run()
end)