Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 use wardlib.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-cache
  • update_cache (boolean): --update-cache
  • virtual (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 use wardlib.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): -y
  • quiet (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): uses purge instead of remove

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 use wardlib.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 — For create only: -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 use wardlib.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 the awk binary).

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). When nil, 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 .awk script paths.

Options (AwkOpts)

General:

  • field_sep: string|nil-F <sep>.
  • vars: table|nil-v name=value repeated. Accepts:
    • array form: { "k=v", "x=1" }
    • map form: { k = "v", x = 1 } (sorted by key for deterministic argv)
  • assigns: table|nil — post-program assignments name=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 use wardlib.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?-i
  • center: boolean?-c
  • fork: boolean?-f
  • no_cursor: boolean?-C
  • extra: 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 use wardlib.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. When nil, blkid probes 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 use wardlib.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 use wardlib.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 use wardlib.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_root and no_preserve_root are 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 use wardlib.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 owner is nil and group is not nil, 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_root and no_preserve_root are 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 use wardlib.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. When nil, chroot runs 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 ... (when engine = "podman")

This module constructs ward.process.cmd(...) invocations; it does not parse output. consumers can use wardlib.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 = nil or engine = "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: string
  • argv: 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: string
  • cmdline: string|string[]|nil — command inside the container; when nil, 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 use wardlib.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:

  • force and interactive are 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 use wardlib.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, curl runs with only the configured options.

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 out is provided, uses -o <out>.
  • Otherwise uses -O (remote name).

Curl.post(url, data, opts)

Convenience for POSTing a request body.

  • If opts.request is not set, defaults to POST.
  • If data is 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 use wardlib.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, use Dconf.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/false
  • number -> number literal
  • Dconf.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 use wardlib.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?-b
  • fast: boolean?-f
  • insensitive: 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 use wardlib.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 use wardlib.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=true and supply stdin via your own pipeline/middleware.
  • 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), extra
  • DockerRmiOpts: force (-f), no_prune (--no-prune), extra
  • DockerStopOpts: time (-t <seconds>), extra
  • DockerInspectOpts: format (-f <format>), size (-s), type (--type), extra
  • DockerLoginOpts: 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 running dunst instance

This module constructs ward.process.cmd(...) invocations; it does not parse output. consumers can use wardlib.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/summary
  • opts.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 close
  • DunstCtl.closeAll()dunstctl close-all
  • DunstCtl.context()dunstctl context
  • DunstCtl.historyPop([id])dunstctl history-pop [id]
  • DunstCtl.historyRm(id)dunstctl history-rm <id>
  • DunstCtl.historyClear()dunstctl history-clear
  • DunstCtl.isPaused()dunstctl is-paused
  • DunstCtl.setPaused(v)dunstctl set-paused <true|false|toggle>
    • v may be boolean (true/false) or a string ("true"|"false"|"toggle")
  • DunstCtl.getPauseLevel()dunstctl get-pause-level
  • DunstCtl.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 = true adds --json
  • DunstCtl.reload([files])dunstctl reload [dunstrc ...]
    • files may be a string or string[]
  • DunstCtl.debug()dunstctl debug
  • DunstCtl.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 use wardlib.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) where XXXX is 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)
    • bootorder accepts 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 use wardlib.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 "".
  • paths: string|string[]|nil
    • If nil, fd searches the current directory.

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 use wardlib.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, feh runs with only options.

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 use wardlib.tools.out (or their own parsing) on the :output() result.

Notes:

  • find variants (GNU/BSD/BusyBox) differ. Use expr / extra_expr to access unmodeled features.
  • Traversal mode flags -P, -L, -H must 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 {"."}.
  • 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 --) and extra_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 use wardlib.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-prompt
  • placeholder: string?--placeholder <text>
  • search: string?--search <text>
  • no_icons: boolean?-I
  • anchor: string?-a <anchor>
  • lines: number?-l <n>
  • width: number?-w <n>
  • no_sort: boolean?--no-sort
  • extra: 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 use wardlib.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 use wardlib.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 -.
  • inputs: string|string[]|nil
    • If nil, grep reads stdin.

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 with after_context/before_context.

Binary / NUL:

  • null (-Z), null_data (-z, GNU)
  • text (-a), binary_without_match (-I)

GNU-specific convenience:

  • color (--color[=WHEN]): true maps 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 use wardlib.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 use wardlib.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?-d
  • keep: boolean?-k
  • force: boolean?-f
  • stdout: boolean?-c
  • recursive: boolean?-r
  • test: boolean?-t
  • list: boolean?-l
  • verbose: boolean?-v
  • quiet: boolean?-q
  • suffix: string?-S <suffix>
  • level: integer? — compression level -1..-9
  • extra: string[]? — appended after modeled options

Notes:

  • paths must be non-empty.
  • When level is 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 use wardlib.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.
  • inet6: boolean?
    • Adds -6 (IPv6 only).
    • Mutually exclusive with inet4.
  • family: string?
    • Adds -f <family>.
    • Typical values: "inet", "inet6", "link", "bridge", "mpls".

Namespace and batch execution

  • netns: string?
    • Adds -n <netns>.
    • Executes the ip command in the context of the specified network namespace.
  • batch: string?
    • Adds -b <file>.
    • Executes commands from a batch file.

Output formatting

  • json: boolean?
    • Adds -j.
    • Requests JSON output.
  • pretty: boolean?
    • Adds -p.
    • Pretty-prints JSON output (typically meaningful only with json = true).
  • oneline: boolean?
    • Adds -o.
    • One-line output formatting.
  • brief: boolean?
    • Adds -br.
    • Brief output.
  • details: boolean?
    • Adds -d.
    • Show details.
  • human: boolean?
    • Adds -h.
    • Human-readable output.
  • resolve: boolean?
    • Adds -r.
    • Resolve names (where applicable).
  • color: boolean|string?
    • If true, adds -c.
    • If string, adds -c <mode>.
    • Common modes: "auto", "always", "never".

Timestamps and statistics

  • timestamp: boolean?
    • Adds -t.
  • timestamp_short: boolean?
    • Adds -ts.
  • stats: boolean|number?
    • Adds -s.
    • If true, adds -s once.
    • If a number n, adds -s n times (iproute2 uses repeated -s to increase verbosity).

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 ip objects/subcommands.

Example

-- ip -d link show dev eth0
local cmd = Ip.raw({ "link", "show", "dev", "eth0" }, { details = true })

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>.

Options: IpLinkShowOpts

Extends IpOpts and adds:

  • up: boolean?
    • Adds selector up.
  • master: string?
    • Adds master <ifname> selector.
  • vrf: string?
    • Adds vrf <name> selector.
  • type: string?
    • Adds type <kind> selector.
  • group: string|number?
    • Adds group <group> selector.

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 })

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.
  • down: boolean?
    • Adds down.
    • Mutually exclusive with up.
  • mtu: number?
    • Adds mtu <n>.
  • qlen: number?
    • Adds txqueuelen <n>.
  • name: string?
    • Adds name <newname>.
  • alias: string?
    • Adds alias <text>.
  • address: string?
    • Adds address <lladdr>.
  • broadcast: string?
    • Adds broadcast <lladdr>.
  • master: string?
    • Adds master <ifname>.
  • nomaster: boolean?
    • Adds nomaster.
  • set_netns: string?
    • Adds netns <name> (moves interface to namespace).
  • extra_after: string[]?
    • Appended after modeled link set arguments.

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>.

Options: IpAddrShowOpts

Extends IpOpts and adds:

  • up: boolean?
    • Adds selector up.
  • scope: string?
    • Adds scope <scope> selector (e.g. "global", "link").
  • label: string?
    • Adds label <pattern> selector.
  • to: string?
    • Adds to <prefix> selector.

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>.
  • broadcast: string?
    • Adds broadcast <addr>.
  • anycast: string?
    • Adds anycast <addr>.
  • scope: string?
    • Adds scope <scope>.
  • valid_lft: string|number?
    • Adds valid_lft <time> (e.g. "forever").
  • preferred_lft: string|number?
    • Adds preferred_lft <time>.
  • noprefixroute: boolean?
    • Adds noprefixroute.
  • 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>.

Options: IpAddrFlushOpts

Extends IpOpts and adds:

  • scope: string?
    • Adds scope <scope> selector.
  • label: string?
    • Adds label <pattern> selector.
  • to: string?
    • Adds to <prefix> selector.
  • 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.
  • vrf: string?
    • Adds vrf <name> selector.
  • dev: string?
    • Adds dev <ifname> selector.
  • proto: string?
    • Adds proto <proto> selector.
  • scope: string?
    • Adds scope <scope> selector.
  • type: string?
    • Adds type <type> selector.
  • 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>.
  • iif: string?
    • Adds iif <ifname>.
  • oif: string?
    • Adds oif <ifname>.
  • vrf: string?
    • Adds vrf <name>.
  • mark: string|number?
    • Adds mark <fwmark>.
  • uid: string|number?
    • Adds uid <uid>.
  • 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>.
  • dev: string?
    • Adds dev <ifname>.
  • src: string?
    • Adds src <addr>.
  • metric: number?
    • Adds metric <n>.
  • table: string|number?
    • Adds table <id>.
  • proto: string?
    • Adds proto <proto>.
  • scope: string?
    • Adds scope <scope>.
  • type: string?
    • Adds type <type>.
  • onlink: boolean?
    • Adds onlink.
  • mtu: number?
    • Adds mtu <n>.
  • advmss: number?
    • Adds advmss <n>.
  • initcwnd: number?
    • Adds initcwnd <n>.
  • initrwnd: number?
    • Adds initrwnd <n>.
  • realm: string?
    • Adds realm <realm>.
  • preference: string?
    • Adds pref <pref> (common for IPv6: "low", "medium", "high").
  • 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.
  • proxy: boolean?
    • Adds proxy selector.
  • router: boolean?
    • Adds router selector.
  • 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>.
  • router: boolean?
    • Adds router.
  • proxy: boolean?
    • Adds proxy.
  • extra_after: string[]?
    • Appended after modeled args.

Options: IpNeighFlushOpts

Extends IpOpts and adds:

  • nud: string?
    • Adds nud <state> selector.
  • proxy: boolean?
    • Adds proxy selector.
  • 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.
  • 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>.
  • from: string?
    • Adds from <prefix>.
  • to: string?
    • Adds to <prefix>.
  • iif: string?
    • Adds iif <ifname>.
  • oif: string?
    • Adds oif <ifname>.
  • fwmark: string|number?
    • Adds fwmark <mark>.
  • table: string|number?
    • Adds table <id>.
  • lookup: string|number?
    • Alias for table. If table is not set, lookup is used.
  • suppress_prefixlength: number?
    • Adds suppress_prefixlength <n>.
  • uidrange: string?
    • Adds uidrange <start>-<end>.
  • 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, uses ip monitor default set.
    • If set, each object is appended (e.g. "link", "addr", "route").

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 use wardlib.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?-c
  • raw_output: boolean?-r
  • join_output: boolean?-j
  • sort_keys: boolean?-S
  • monochrome_output: boolean?-M
  • color_output: boolean?-C
  • exit_status: boolean?-e (set exit status based on output)
  • ascii_output: boolean?-a
  • tab: boolean?--tab
  • indent: integer?--indent <n> (validated as integer >= 0)

Notes:

  • color_output and monochrome_output are 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 filter is nil, it defaults to ".".
  • The wrapper always emits -- before the filter to avoid ambiguity when a filter starts with -.
  • If inputs is nil, 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 PID
  • killall — signal processes by name
  • pkill — signal processes by pattern

This module constructs ward.process.cmd(...) invocations; it does not parse output. consumers can use wardlib.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

  • Signalstring|number
    • Examples: "TERM", "SIGKILL", 9.

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?-e exact match
  • ignore_case: boolean?-I ignore case
  • interactive: boolean?-i ask before killing
  • wait: boolean?-w wait for processes to die
  • regexp: boolean?-r interpret names as regex
  • user: string?-u <user>
  • verbose: boolean?-v
  • quiet: boolean?-q
  • extra: string[]? — appended after modeled options

PkillOpts

  • signal: Signal? — added as -<sig> (compact pkill form)
  • full: boolean?-f match full command line
  • exact: boolean?-x match whole name
  • newest: boolean?-n select newest
  • oldest: boolean?-o select oldest
  • parent: 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?-v invert match
  • count: boolean?-c count matches
  • list_name: boolean?-l list pid and name
  • list_full: boolean?-a list 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 pids is nil, 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 use wardlib.tools.out (or their own parsing) on the :output() result.

Import

local Ls = require("wardlib.app.ls").Ls

Options: LsOpts

  • all: boolean?-a
  • almost_all: boolean?-A (mutually exclusive with all)
  • long: boolean?-l
  • human: boolean?-h (GNU; typically used with -l)
  • classify: boolean?-F
  • one_per_line: boolean?-1
  • recursive: boolean?-R
  • directory: boolean?-d (list directories themselves, not contents)
  • reverse: boolean?-r

Sorting (mutually exclusive):

  • sort_time: boolean?-t
  • sort_size: boolean?-S (GNU)
  • no_sort: boolean?-U (BSD); GNU uses -f (use extra for 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 paths is nil, 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 use wardlib.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 / --json
  • output: string|string[]?-o <cols> (string or array joined by commas)
  • bytes: boolean?-b
  • paths: boolean?-p
  • fs: boolean?-f
  • all: boolean?-a
  • nodeps: boolean?-d
  • list: boolean?-l
  • raw: boolean?-r
  • noheadings: boolean?-n
  • sort: string?--sort <col>
  • tree: boolean?--tree
  • extra: 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 devices is nil, lsblk enumerates 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 use wardlib.tools.out (or their own parsing) on the :output() result.

Import

local Mkdir = require("wardlib.app.mkdir").Mkdir

Options: MkdirOpts

  • parents: boolean?-p
  • verbose: boolean?-v
  • mode: 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 use wardlib.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 use wardlib.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? — adds ro to -o
  • bind: boolean? — adds --bind
  • rbind: boolean? — adds --rbind
  • move: boolean? — adds --move
  • verbose: boolean? — adds -v
  • fake: boolean? — adds -f
  • extra: string[]? — appended before positional args

UmountOpts

  • lazy: boolean? — adds -l
  • force: boolean? — adds -f
  • recursive: boolean? — adds -R
  • verbose: boolean? — adds -v
  • extra: 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 source and target are nil, this corresponds to plain mount (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 use wardlib.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 with interactive)
  • interactive: boolean?-i
  • no_clobber: boolean?-n (GNU; mutually exclusive with force/interactive)

Other behavior:

  • update: boolean?-u
  • verbose: boolean?-v

GNU-only:

  • backup: boolean?--backup
  • suffix: 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:

  • -t is GNU-style. If your platform does not support it, prefer Mv.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 use wardlib.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 => n
  • opts.recursive => s
  • opts.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): --noconfirm
  • extra (string[]): extra args appended after modeled options

PacmanSyncOpts

Extends PacmanCommonOpts.

Modeled fields:

  • refresh (boolean): uses -Syy / -Syyu instead of -Sy / -Syu

PacmanInstallOpts

Extends PacmanCommonOpts.

Modeled fields:

  • needed (boolean): --needed

PacmanRemoveOpts

Extends PacmanCommonOpts.

Modeled fields:

  • recursive (boolean): include s flag (-Rs)
  • nosave (boolean): include n flag (-Rn)
  • cascade (boolean): include c flag (-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 use wardlib.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?-4
  • inet6: boolean?-6 (mutually exclusive with inet4)
  • 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 for interface (same -I flag)
  • preload: number?-l <n>
  • flood: boolean?-f
  • adaptive: boolean?-A
  • quiet: boolean?-q
  • verbose: boolean?-v
  • audible: boolean?-a
  • numeric: boolean?-n
  • timestamp: boolean?-D
  • record_route: boolean?-R
  • pmtudisc: 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 use wardlib.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 ... play
  • Playerctl.pause(opts)playerctl ... pause
  • Playerctl.play_pause(opts)playerctl ... play-pause
  • Playerctl.next(opts)playerctl ... next
  • Playerctl.previous(opts)playerctl ... previous
  • Playerctl.stop(opts)playerctl ... stop
  • Playerctl.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-players
  • ignore: 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 use wardlib.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.
  • 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?-d
  • interactive: boolean?-i
  • tty: boolean?-t
  • rm: boolean?--rm
  • name: 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?--privileged
  • cap_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?-d
  • interactive: boolean?-i
  • tty: boolean?-t
  • user: 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?--pull
  • no_cache: boolean?--no-cache
  • layers: boolean?--layers
  • format: string?--format <format>
  • extra: string[]?

PodmanPsOpts

  • all: boolean?-a
  • quiet: boolean?-q
  • no_trunc: boolean?--no-trunc
  • latest: boolean?-l
  • last: integer?-n <n>
  • size: boolean?-s
  • format: string?--format <fmt>
  • filter: string|string[]?--filter <filter> (repeatable)
  • extra: string[]?

PodmanImagesOpts

  • all: boolean?-a
  • quiet: boolean?-q
  • no_trunc: boolean?--no-trunc
  • digests: boolean?--digests
  • format: string?--format <fmt>
  • filter: string|string[]?--filter <filter> (repeatable)
  • extra: string[]?

PodmanLogsOpts

  • follow: boolean?-f
  • timestamps: boolean?-t
  • since: string?--since <time>
  • until: string?--until <time>
  • tail: string|integer?--tail <n|all>
  • extra: string[]?

PodmanRmOpts

  • force: boolean?-f
  • volumes: boolean?-v
  • extra: string[]?

PodmanRmiOpts

  • force: boolean?-f
  • extra: string[]?

PodmanStopOpts

  • time: integer?-t <seconds>
  • extra: string[]?

PodmanInspectOpts

  • format: string?-f <format>
  • size: boolean?-s
  • type: string?--type <type>
  • extra: string[]?

PodmanLoginOpts

  • username: string?-u <user>
  • password_stdin: boolean?--password-stdin
  • extra: 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 use wardlib.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 table that is encoded into a stable head,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_ becomes if (because if is 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 key backend
  • fsdev: required key driver
  • object: required key type
  • kv: generic k=v,k2=v2 encoder

System emulation

Qemu.system(arch, opts) -> ward.Cmd

Runs qemu-system-<arch> with a structured opts table.

  • arch examples: "x86_64", "aarch64", "riscv64", …
  • opts.bin can 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: -nographic
  • display: -display <val>
  • serial, monitor, qmp: repeatable
  • drive, 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: -snapshot
  • daemonize: -daemonize
  • pidfile: -pidfile <path>
  • gdb: -gdb <dev>
  • start_paused: -S
  • rtc: -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: -c
  • progress: -p
  • quiet: -q
  • unsafe: -U
  • image_opts: --image-opts
  • target_image_opts: --target-image-opts
  • extra: 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/nbdX binding (Linux) via -c/--connect and -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, socket
  • read_only, shared, persistent, fork, pid_file
  • export_name, description
  • object, tls_creds, tls_authz, tls_hostname
  • image_opts + filename as 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, monitor
  • object
  • blockdev
  • nbd_server
  • export
  • pidfile, daemonize
  • extra

Storage daemon options (QemuStorageDaemonOpts)

  • chardev: --chardev <spec> (repeatable; string or table via qemu.chardev)
  • monitor: --monitor <spec> (repeatable)
  • object: --object <spec> (repeatable; string or table via qemu.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: --daemonize
  • extra: 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 use wardlib.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 -.
  • paths: string|string[]|nil
    • If nil, ripgrep searches the current directory.

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?-i
  • smart_case: boolean?-S
  • case_sensitive: boolean?-s
  • word: boolean?-w
  • line: boolean?-x
  • invert: boolean?-v

Output / formatting:

  • count: boolean?-c (count matching lines)
  • count_matches: boolean?--count-matches
  • quiet: boolean?-q
  • line_number: boolean?-n
  • column: boolean?--column
  • heading: boolean?--heading
  • no_filename: boolean?--no-filename (mutually exclusive with with_filename)
  • with_filename: boolean?--with-filename (mutually exclusive with no_filename)
  • vimgrep: boolean?--vimgrep
  • json: boolean?--json (JSON Lines / NDJSON)

Context / limits:

  • after_context: number?-A <n>
  • before_context: number?-B <n>
  • context: number?-C <n> (mutually exclusive with after_context/before_context)
  • max_count: number?-m <n>
  • threads: number?-j <n>

Filesystem behavior:

  • follow: boolean?-L (follow symlinks)
  • hidden: boolean?--hidden
  • no_ignore: boolean?--no-ignore
  • no_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-matches
  • files_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>.
  • extra: string[]? → extra argv appended after modeled options

Examples

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.
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 use wardlib.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 with interactive)
  • interactive: boolean?-i (mutually exclusive with force)
  • recursive: boolean?-r / -R
  • dir: 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 use wardlib.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-icons
  • terminal: 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?-i
  • only_match: boolean?-only-match
  • no_custom: boolean?-no-custom
  • format: string?-format <fmt>
  • select: string?-select <string>
  • mesg: string?-mesg <msg>
  • password: boolean?-password
  • markup_rows: boolean?-markup-rows
  • multi_select: boolean?-multi-select
  • sync: boolean?-sync
  • input: 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 use wardlib.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?-a
  • compress: boolean?-z
  • verbose: boolean?-v
  • progress: boolean?--progress
  • delete: boolean?--delete
  • dry_run: boolean?--dry-run
  • checksum: boolean?--checksum
  • partial: boolean?--partial
  • excludes: 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 use wardlib.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 models in_place so it works on GNU sed and is typically accepted on BSD sed as well. If you need strict BSD behavior, pass extra = { "-i", ".bak" }.

API

Sed.bin

Executable name or path (default: "sed").

Sed.run(inputs, opts)

Builds: sed <opts...> [inputs...]

  • If inputs is nil, sed reads stdin.
  • inputs accepts string|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.

  • inputs must be string|string[] (a real file path list).
  • backup_suffix may be nil (GNU-style -i) or a string like .bak.

Options

SedOpts

  • extended: boolean?-E
  • quiet: boolean?-n
  • in_place: boolean|string?-i or -i<suffix>
  • backup_suffix: string? → alias for in_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 use wardlib.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?--force
  • no_reread: boolean?--no-reread
  • no_act: boolean?--no-act
  • quiet: boolean?--quiet
  • lock: boolean|"yes"|"no"|"nonblock"|nil--lock[=mode]
    • true / "yes"--lock
    • false / "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|nil
  • size: string|integer|nil
  • type: string|nil
  • uuid: string|nil
  • name: string|nil
  • attrs: string|nil
  • bootable: boolean|nil → emits bootable
  • extra: table<string, string|number|boolean>|nil → appended at end (sorted by key)

SfdiskTable

Header fields are emitted as key: value lines.

  • label: string|nil
  • label_id: string|nillabel-id:
  • unit: string|nilunit: (e.g. "sectors")
  • first_lba: integer|nilfirst-lba:
  • last_lba: integer|nillast-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 use wardlib.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 files is nil, sha256sum reads stdin.
  • If files is provided, the wrapper inserts -- before the file list.

Sha256sum.check(check_files, opts)

Builds: sha256sum -c <opts...> [-- <check_files...>]

  • If check_files is nil, sha256sum reads the checksum list from stdin.
  • If check_files is 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: binary and text.
  • tag: boolean?--tag
  • zero: boolean?-z (NUL line terminator)

Check-mode options (when using Sha256sum.check / -c):

  • quiet: boolean?--quiet
  • status: boolean?--status
  • warn: boolean?--warn
  • strict: boolean?--strict
  • ignore_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 use wardlib.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).

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?-4
  • inet6: boolean?-6 (mutually exclusive with inet4)
  • family: string?-f <family> (e.g. "inet", "inet6", "unix", "link")

Socket types:

  • tcp: boolean?-t
  • udp: boolean?-u
  • raw: boolean?-w
  • unix: boolean?-x

Selection:

  • all: boolean?-a
  • listening: boolean?-l

Output formatting/details:

  • numeric: boolean?-n
  • resolve: boolean?-r
  • no_header: boolean?-H
  • extended: boolean?-e
  • info: boolean?-i
  • memory: boolean?-m
  • timers: boolean?-o
  • summary: boolean?-s

Process / packet (-p):

  • process: boolean?-p (show process using socket)
  • packet: boolean?-p (packet sockets; mutually exclusive with process)

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 use wardlib.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.out to parse stdout/stderr.

Import

local Ssh = require("wardlib.app.ssh").Ssh

API

Ssh.target(host, opts)

Builds a target string:

  • host (default)
  • user@host when opts.user is provided

Ssh.remote(host, path, opts)

Builds a remote scp path string:

  • host:/path
  • user@host:/path when opts.user is provided

Ssh.exec(host, remote, opts)

Builds: ssh <common-opts...> <target> [remote...]

  • If remote is nil, builds an interactive session.
  • If remote is a string, it is passed as a single argument.
  • If remote is 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 as user@host)
  • port: integer? — port (ssh -p, scp -P)
  • identity_file: string? — identity file (-i <path>)
  • batch: boolean?-o BatchMode=yes
  • strict_host_key_checking: boolean|string?-o StrictHostKeyChecking=<...>
    • trueyes, falseno, 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?-r
  • preserve_times: boolean?-p
  • compress: boolean?-C
  • quiet: 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 area
  • swapon — enable swap
  • swapoff — disable swap

This module constructs ward.process.cmd(...) invocations; it does not parse output. consumers can use wardlib.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 targets is nil, runs swapon with only modeled options (useful for --show).
  • If targets is a string array, all targets are appended.

Swap.swapoff(targets, opts)

Builds: swapoff <opts...> [targets...]

  • If targets is nil, runs swapoff with 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?-f
  • check: boolean?-c (check bad blocks)
  • extra: string[]? — extra argv appended after modeled options

SwaponOpts

  • all: boolean?-a
  • discard: string?--discard[=<policy>]
    • pass "" (empty string) to emit bare --discard
  • fixpgsz: boolean?--fixpgsz
  • priority: number?-p <prio>

Formatting for --show:

  • show: boolean?--show
  • noheadings: boolean?--noheadings
  • raw: boolean?--raw
  • bytes: boolean?--bytes
  • output: string|string[]?--output <cols> (array is joined by ,)

Escape hatch:

  • extra: string[]? — extra argv appended after modeled options

SwapoffOpts

  • all: boolean?-a
  • verbose: boolean?-v
  • extra: 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 use wardlib.tools.out (or their own parsing) on the :output() result.

Notes:

  • swaybg is 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 image
  • mode: SwaybgMode? — scaling mode
  • color: 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:

  • systemctl
  • journalctl

It returns ward.process.cmd(...) objects.

This module constructs ward.process.cmd(...) invocations; it does not parse output. consumers can use wardlib.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 to true)
  • full: boolean?--full

SystemdJournalOpts (extends SystemdCommonOpts)

  • follow: boolean?-f
  • lines: 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 to true)
  • 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 use wardlib.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-match
  • fuzzy_match: boolean?--fuzzy-match
  • width: string?--width <w>
  • height: string?--height <h>
  • font: string?--font <font>
  • defines: table<string, any>? → additional --key <value> pairs
    • stable key order
    • true emits --key
    • false/nil are 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 use wardlib.tools.out (or their own parsing) on the :output() result.

Notes:

  • Address family flags -4 and -6 are mutually exclusive.
  • Probe type flags are mutually exclusive: -I (ICMP), -T (TCP), -U (UDP).
  • Use extra to 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?-4
  • inet6: boolean?-6
  • numeric: boolean?-n
  • as_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 use wardlib.tools.out (or their own parsing) on the :output() result.

Safety note:

  • Info-ZIP unzip does not support -- as end-of-options. For safety, this wrapper rejects zip paths (and optional file lists) that start with -.
  • opts.password uses unzip -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 extract
  • exclude: string|string[]? — patterns appended after -x
  • overwrite: boolean?-o
  • never_overwrite: boolean?-n (mutually exclusive with overwrite)
  • quiet: boolean?-q
  • junk_paths: boolean?-j
  • list: boolean?-l (low-level; prefer Unzip.list() )
  • test: boolean?-t (low-level; prefer Unzip.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 use wardlib.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 extra for the rest.

Import

local Wget = require("wardlib.app.wget").Wget

API

Wget.fetch(urls, opts)

Builds: wget <opts...> [urls...]

  • If urls is nil, runs wget with only the configured options (useful with -i).
  • If urls is a string array, all URLs are appended.

Wget.download(url, out, opts)

Convenience for a single URL.

  • If out is provided, sets opts.output_document (-O <out>).

Wget.mirror_site(url, dir, opts)

Convenience for mirroring.

  • Always sets opts.mirror = true (-m).
  • If dir is provided, sets opts.directory_prefix (-P <dir>).

Options (WgetOpts)

Verbosity:

  • quiet: boolean?-q
  • verbose: boolean?-v
  • no_verbose: boolean?-nv

Download behavior:

  • continue: boolean?-c
  • timestamping: boolean?-N
  • no_clobber: boolean?-nc
  • spider: 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?-4
  • inet6_only: boolean?-6
  • timeout: number?--timeout=<sec>
  • wait: number?--wait=<sec>
  • tries: number?--tries=<n>

Recursion / mirroring:

  • recursive: boolean?-r
  • level: number?-l <n>
  • no_parent: boolean?-np
  • mirror: boolean?-m
  • page_requisites: boolean?-p
  • convert_links: boolean?-k
  • adjust_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 use wardlib.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?--insensitive
  • show_icons: boolean?--allow-images
  • allow_markup: boolean?--allow-markup
  • gtk_dark: boolean?--gtk-dark
  • normal_window: boolean?--normal-window
  • extra: 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-copy
  • wl-paste

This module constructs ward.process.cmd(...) invocations; it does not parse output. consumers can use wardlib.tools.out (or their own parsing) on the :output() result.

Notes:

  • wl-copy reads data from stdin. This module only builds the command; feeding stdin is the caller’s responsibility.
  • Use wardlib.tools.out if you want predictable parsing of wl-paste output.

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"|nil
    • nil/"clipboard" uses the default clipboard
    • "primary" sets --primary

ClipboardCopyOpts (extends ClipboardSelectionOpts)

  • type: string? — MIME type (--type <mime>)
  • foreground: boolean?--foreground
  • paste_once: boolean?--paste-once
  • clear: boolean?--clear
  • extra: string[]? — extra argv appended

ClipboardPasteOpts (extends ClipboardSelectionOpts)

  • type: string? — MIME type (--type <mime>)
  • no_newline: boolean?--no-newline
  • extra: 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 use wardlib.tools.out (or their own parsing) on the :output() result.

Notes:

  • GNU/BSD xargs differ slightly. Use extra to 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 cmd is nil, xargs executes its default command (commonly echo, implementation-dependent).
  • When cmd is provided, this wrapper emits -- before the command.

Xargs.raw(argv, opts)

Builds: xargs <opts...> <argv...>

Options (XargsOpts)

Common fields:

  • Input parsing: null_input (-0) or delimiter (-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 use wardlib.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): -y
  • automatic (boolean): -A
  • force (boolean): -f

XbpsRemoveOpts

Extends XbpsCommonOpts.

Modeled fields:

  • yes (boolean): -y
  • recursive (boolean): -R
  • force (boolean): -f
  • dry_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 use wardlib.tools.out (or their own parsing) on the :output() result.

Notes:

  • Xz.run() always inserts -- before paths.
  • opts.level is modeled as 0..9 and 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?-d
  • keep: boolean?-k
  • force: boolean?-f
  • stdout: boolean?-c
  • verbose: boolean?-v
  • quiet: boolean?-q
  • extreme: boolean?-e
  • level: integer? — compression level -0..-9
  • threads: 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 use wardlib.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 => n
  • opts.recursive => s
  • opts.cascade => c

Yay.search(pattern, opts)

Builds: yay -Ss <pattern>.

Yay.info(pkg, opts)

Builds: yay -Qi <pkg>.

Options

YayCommonOpts

Modeled fields:

  • needed (boolean): --needed
  • noconfirm (boolean): --noconfirm
  • extra (string[]): extra args appended after modeled options

YaySyncOpts

Extends YayCommonOpts.

Modeled fields:

  • refresh (boolean): uses -Syy / -Syyu instead of -Sy / -Syu

YayRemoveOpts

Extends YayCommonOpts.

Modeled fields:

  • recursive (boolean): include s flag (-Rs)
  • nosave (boolean): include n flag (-Rn)
  • cascade (boolean): include c flag (-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 use wardlib.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.examples and spec.epilog).
  • Subcommand help inherits parent examples when the subcommand does not define its own examples.
  • No side effects: it does not use io and 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, default true): injects -h/--help if not defined.
  • auto_version (boolean, default false): injects --version/-V if 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" }: Lua arg-like form.

parse_opts:

  • start_index (number, default 1): where to begin reading argv.
  • allow_unknown (boolean, default false): if true, unknown options are appended to result.rest.
  • stop_at_first_positional (boolean, default false): 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, and result:

    • result.values (table): parsed option values by id.
    • result.positionals (table): parsed positional values by id.
    • 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, and err:

    • 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, default 100)
  • include_description (boolean, default true)
  • include_defaults (boolean, default true)

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 --version when auto_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 in result.values.

  • long (string|nil): --long name (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): for kind="flag" only. If true, --no-<long> is accepted and sets the flag to false.

  • repeatable (boolean, default true): if set to false, repeating the option produces an option_repeated error (applies to flag, value, and count).

  • max_count (number|nil): for kind="count" only. If set, exceeding the limit produces a too_many_occurrences error.

  • validate(value) (function|nil): custom validator; return true on success, or false, reason to reject. A thrown error is surfaced as an invalid_value error.

  • 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; return true on success, or false, reason to reject.

  • on(value, event, state) (function|nil)

Constraints

spec.constraints is optional and supports:

  • mutex (array of groups): each group is an array of option ids that are mutually exclusive. If more than one is present, parsing fails with code="mutually_exclusive".
  • one_of (array of groups): each group is an array of option ids where at least one must be present. If none are present, parsing fails with code="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:

  • true when it wrote the file
  • false when write_if_changed=true and the file already contained identical content

Options:

  • format (string|nil) - override inferred format
  • mkdir (boolean, default false) - create parent directory
  • write_if_changed (boolean, default false) - skip writing when content is identical
  • eof_newline (boolean, default true) - ensure output ends with \n

JSON-only options:

  • pretty (boolean, default false)
  • 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 existing doc is assumed mutated in-place

Options:

  • all write() options
  • allow_missing (boolean, default true) - if file is missing, start from opts.default or {}
  • 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) uses ward.helpers.table.deep_merge
  • mode = "shallow" uses ward.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:

  • name is used for identification in the manifest.
  • description is recorded in the manifest; it does not affect behavior.
  • defaults is accepted and stored on the definition, but is not currently used by the engine.
  • steps must 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 /, \, or C:\...).
  • Must not contain parent traversal (..).
  • Path separators are normalized internally.

For any source path in dotfiles.link():

  • ~ and ~/... are expanded using $HOME when available.
  • Both absolute and relative paths are allowed.

Conditional execution (common option)

Most step constructors accept opts and support conditional execution:

  • opts.when(base) -> boolean or opts.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.

  • content may be:

    • a string
    • a function (base, abs) -> string

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=true on def:apply, the step errors
    • with force=true, an existing file is overwritten; an existing symlink is unlinked first

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 source is 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.

Replacement policy flags (only relevant with def:apply(..., { force = true })):

  • replace_file (default true)

    • Allows replacing an existing regular file at the destination.
  • replace_symlink (default true)

    • Allows replacing an existing symlink at the destination.
  • replace_dir (default false)

    • 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_nil is a string, fn(base, abs) is called where abs = base/rel_path_or_nil.
  • If rel_path_or_nil is nil, 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.
  • string

    • Only valid when a path was provided. The string is treated as file content and written to abs (using the same overwrite rules as dotfiles.content).
  • 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.

Nested application rules:

  • If a path was provided: nested steps are applied under that path (i.e. abs becomes 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_meta can be:

    • a DotfilesDefinition returned by dotfiles.define()
    • a meta table { name?, description?, defaults?, steps = {...} }
  • If prefix_or_nil is non-nil, the included definition is applied under base/prefix_or_nil.

dotfiles.group(name, steps, opts?)

Pure organizational wrapper that preserves ordering.

  • steps is a steps array.
  • opts.when/conditions can 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.
  • 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) and assert entries 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, optional description)
  • applied_at timestamp
  • base
  • a list of entries in apply order

Each revertable entry records a snapshot of the previous state:

  • absent
  • file with previous content
  • symlink with previous target
  • dir

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, default false) - 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.process command 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 (expects cmd: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() requires res.ok == true (default).
  • :allow_fail() disables the ok requirement.
  • :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() returns string[] 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 like string.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 when runner: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 returns false, the task is skipped.

Task function return conventions:

  • nil or true => 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:

  • name
  • desc
  • deps

runner:plan(names?) -> (ok, plan_or_err)

Computes a deterministic execution plan.

  • names may be:
    • nil: uses opts.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 as runner: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:
    • name
    • status ("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 variables
  • home() best-effort home directory resolution
  • tmpdir() best-effort temporary directory resolution
  • os_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:

  1. HOME
  2. USERPROFILE
  3. HOMEDRIVE + HOMEPATH

platform.tmpdir() -> string|nil

Temporary directory detection.

Resolution order:

  1. TMPDIR
  2. TEMP
  3. TMP
  4. /tmp on Unix targets

platform.os_release(opts?) -> table|nil

Linux distribution metadata.

  • Reads /etc/os-release (or /usr/lib/os-release fallback) 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):

  • tries or attempts (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").
  • max_delay (duration)
    • Passed through to Ward core.
  • max (duration)
    • Alias for max_delay.
  • 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.

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)