# "CLI Applications"
_Path: en/tutorials/cli_
> "Build command-line tools that read input, write output, and interact with users."
## Table of Contents
- CLI Applications
## Content
# CLI Applications
Build a command-line process that writes to the terminal, then extend it with input, color, system information, and named commands.
**Classification:** Runnable tutorial. The greeting application is complete. The
later sections are optional replacements for `src/cli.lua` or the `app:cli` entry,
as stated in each section.
## What We're Building
A CLI process that prints a greeting:
```
$ wippy run -x app:cli
Hello from CLI!
```
## Prerequisites
- Wippy runtime `v0.3.32a` available as `wippy`. Confirm it with
`wippy version --short`.
- An interactive terminal. Input examples require stdin, and color examples require
a terminal that displays ANSI escape sequences.
## Project Structure
```
cli-app/
├── wippy.lock
└── src/
├── _index.yaml
└── cli.lua
```
## Step 1: Create Project
```bash
mkdir cli-app && cd cli-app
mkdir src
```
## Step 2: Entry Definitions
Create `src/_index.yaml`:
```yaml
version: "1.0"
namespace: app
entries:
# Terminal host connects processes to stdin/stdout
- name: terminal
kind: terminal.host
lifecycle:
auto_start: true
# CLI process
- name: cli
kind: process.lua
source: file://cli.lua
method: main
modules:
- io
```
The terminal.host bridges your Lua process to the terminal. Without it, io.print() has nowhere to write.
## Step 3: CLI Code
Create `src/cli.lua`:
```lua
local io = require("io")
local function main()
io.print("Hello from CLI!")
return 0
end
return { main = main }
```
## Step 4: Run It
```bash
wippy init
wippy run -x app:cli
```
Expected output:
```
Hello from CLI!
```
The -x flag runs the process as a command. It auto-detects the only
terminal.host in the registry; use --host when more than one
terminal host exists. With no logging flag, command mode suppresses runtime logs so
the process output stays readable.
## Reading User Input
Replace `src/cli.lua` with this version. It reports terminal read and write errors
instead of treating them as empty input:
```lua
local io = require("io")
local function main()
local _, write_err = io.write("Enter your name: ")
if write_err then
io.eprint("Cannot write prompt:", write_err)
return 1
end
local _, flush_err = io.flush()
if flush_err then
io.eprint("Cannot flush prompt:", flush_err)
return 1
end
local name, read_err = io.readline()
if read_err then
io.eprint("Cannot read input:", read_err)
return 1
end
if name and #name > 0 then
io.print("Hello, " .. name .. "!")
else
io.print("Hello, stranger!")
end
return 0
end
return { main = main }
```
## Colored Output
Replace `src/cli.lua` with this version to use ANSI escape codes for colors:
```lua
local io = require("io")
local reset = "\027[0m"
local function red(s) return "\027[31m" .. s .. reset end
local function green(s) return "\027[32m" .. s .. reset end
local function yellow(s) return "\027[33m" .. s .. reset end
local function cyan(s) return "\027[36m" .. s .. reset end
local function bold(s) return "\027[1m" .. s .. reset end
local function main()
io.print(bold(cyan("Welcome!")))
local _, write_err = io.write(yellow("Enter a number: "))
if write_err then
io.eprint("Cannot write prompt:", write_err)
return 1
end
local _, flush_err = io.flush()
if flush_err then
io.eprint("Cannot flush prompt:", flush_err)
return 1
end
local input, read_err = io.readline()
if read_err then
io.eprint("Cannot read input:", read_err)
return 1
end
local n = tonumber(input)
if n then
io.print("Squared: " .. green(tostring(n * n)))
return 0
else
io.print(red("Error: ") .. "not a number")
return 1
end
end
return { main = main }
```
## System Information
Access runtime stats with the `system` module. Every read is guarded by the `system.read` action, so the process also needs a policy that allows it:
```yaml
# Add to entries
- name: system_read
kind: security.policy
policy:
actions:
- system.read
resources: "*"
effect: allow
# Update the CLI entry
- name: cli
kind: process.lua
source: file://cli.lua
method: main
security:
policies:
- app:system_read
modules:
- io
- system
```
Then replace `src/cli.lua`:
```lua
local io = require("io")
local system = require("system")
local function main()
local hostname, hostname_err = system.process.hostname()
if hostname_err then
io.eprint("Cannot read hostname:", hostname_err)
return 1
end
local cpu_count, cpu_err = system.runtime.cpu_count()
if cpu_err then
io.eprint("Cannot read CPU count:", cpu_err)
return 1
end
local goroutines, goroutine_err = system.runtime.goroutines()
if goroutine_err then
io.eprint("Cannot read goroutine count:", goroutine_err)
return 1
end
local mem, memory_err = system.memory.stats()
if memory_err then
io.eprint("Cannot read memory stats:", memory_err)
return 1
end
io.print("Host: " .. hostname)
io.print("CPUs: " .. cpu_count)
io.print("Goroutines: " .. goroutines)
io.print("Memory: " .. string.format("%.1f MB", mem.heap_alloc / 1024 / 1024))
return 0
end
return { main = main }
```
## Named Commands
To invoke the process by name instead of using `-x app:cli`, add command metadata:
Replace the `app:cli` entry with this version. Keep the `terminal.host` entry from
the base project.
```yaml
- name: cli
kind: process.lua
meta:
command:
name: greet
short: Greet the user
source: file://cli.lua
method: main
modules:
- io
```
Run the named command:
```bash
wippy run greet
```
List all available commands:
```bash
wippy run list
```
```
Available commands:
greet Greet the user (app:cli)
Run with: wippy run
```
## Exit Codes
Return a number from `main()` to set the process exit code:
```lua
local function main()
if error_occurred then
return 1 -- Error
end
return 0 -- Success
end
```
## I/O Reference
| Function | Returns | Description |
|----------|---------|-------------|
| `io.print(...)` | `boolean` or `nil, error` without terminal context | Write to stdout with tabs and a trailing newline |
| `io.write(...)` | `boolean, error` | Write to stdout without separators or a newline |
| `io.eprint(...)` | `boolean` or `nil, error` without terminal context | Write to stderr with tabs and a trailing newline |
| `io.readline()` | `string, error` | Read a line without its trailing newline; EOF with no data is an error |
| `io.flush()` | `boolean, error` | Flush stdout when the stream supports it |
## CLI Flags
| Flag | Description |
|------|-------------|
| `wippy run -x app:cli` | Run CLI process (auto-detects terminal.host) |
| `wippy run -x app:cli --host app:terminal` | Explicit terminal host |
| `wippy run -x app:cli -v` | With verbose logging |
## Troubleshooting and Cleanup
- `no terminal host found` means the registry has no `terminal.host`; use the entry
from Step 2. If multiple hosts exist, pass `--host app:terminal`.
- `no terminal context` means the process was not launched through a terminal host.
Use `wippy run -x app:cli`, not a background `process.service`.
- Input errors at EOF are expected when stdin is closed. Run the command in an
interactive terminal for the input examples.
- If ANSI sequences appear as literal characters, use the non-color example or a
terminal with ANSI support.
- The command exits after `main()` returns. After leaving the directory, delete
`cli-app/` if it was only a disposable exercise.
## Next Steps
- [I/O Module](lua/system/io.md) — I/O API reference
- [System Module](lua/system/system.md) — Runtime and system information
- [Echo Service](tutorials/echo-service.md) — Build a multi-process application
## Navigation
Previous: "Hello World" (tutorials/hello-world)
Next: "Terminal UI" (tutorials/tty)