Skip to content

Commit 036341c

Browse files
committed
Deinit AppRunner in examples
Fixes #73
1 parent 4841cda commit 036341c

5 files changed

Lines changed: 66 additions & 66 deletions

File tree

README.md

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,53 @@
1-
# ZIG-CLI
2-
[![Zig Docs](https://img.shields.io/badge/docs-zig-%23f7a41d)](https://sam701.github.io/zig-cli)
1+
# zig-cli
32

3+
[![Zig Docs](https://img.shields.io/badge/docs-zig-%23f7a41d)](https://sam701.github.io/zig-cli)
44

55
A simple package for building command line apps in Zig.
66

77
Inspired by [urfave/cli](https://github.com/urfave/cli) Go package.
88

99
## Features
10-
* command line arguments are parsed into zig values
11-
* long and short options: `--option1`, `-o`
12-
* optional `=` sign: `--address=127.0.0.1` equals `--address 127.0.0.1`
13-
* concatenated short options: `-a -b -c` equals `-abc`
14-
* subcommands: `command1 -option1 subcommand2 -option2`
15-
* multiple option values: `--opt val1 --opt val2 --opt val3`
16-
* enums as option values: `--opt EnumValue1`
17-
* options value can be read from environment variables with a configured prefix
18-
* positional arguments can be mixed with options: `--opt1 val1 arg1 -v`
19-
* stops option parsing after `--`: `command -- --abc` will consider `--abc` as a positional argument to `command`.
20-
* errors on missing required options: `ERROR: option 'ip' is required`
21-
* prints help with `--help`
22-
* colored help messages when TTY is attached
10+
11+
- Arguments parsed directly into Zig values
12+
- Long and short options: `--option`, `-o`
13+
- Optional `=` sign: `--address=127.0.0.1` equals `--address 127.0.0.1`
14+
- Concatenated short options: `-a -b -c` equals `-abc`
15+
- Subcommands: `cmd -opt subcmd -opt2`
16+
- Multiple option values: `--opt val1 --opt val2 --opt val3`
17+
- Enums as option values: `--opt EnumValue1`
18+
- Positional arguments (required and optional), mixed with options: `--opt val arg1 -v`
19+
- Option values read from environment variables with a configured prefix
20+
- Stops option parsing after `--`: `cmd -- --abc` treats `--abc` as a positional argument
21+
- Errors on missing required options: `ERROR: missing required option '--port'`
22+
- App version and author metadata
23+
- Built-in `--help` flag with colored output when a TTY is attached
2324

2425
## Usage
26+
2527
[API Documentation](https://sam701.github.io/zig-cli)
28+
2629
```zig
2730
const std = @import("std");
2831
const cli = @import("cli");
2932
30-
// Define a configuration structure with default values.
3133
var config = struct {
3234
host: []const u8 = "localhost",
3335
port: u16 = undefined,
3436
}{};
3537
3638
pub fn main(init: std.process.Init) !void {
3739
var r = cli.AppRunner.init(&init);
40+
defer r.deinit();
3841
39-
// Create an App with a command named "short" that takes host and port options.
4042
const app = cli.App{
4143
.command = cli.Command{
42-
.name = "short",
44+
.name = "server",
4345
.options = try r.allocOptions(&.{
44-
// Define an Option for the "host" command-line argument.
4546
.{
4647
.long_name = "host",
4748
.help = "host to listen on",
4849
.value_ref = r.mkRef(&config.host),
4950
},
50-
51-
// Define an Option for the "port" command-line argument.
5251
.{
5352
.long_name = "port",
5453
.help = "port to bind to",
@@ -57,32 +56,40 @@ pub fn main(init: std.process.Init) !void {
5756
},
5857
}),
5958
.target = cli.CommandTarget{
60-
.action = cli.CommandAction{ .exec = run_server },
59+
.action = cli.CommandAction{ .exec = run },
6160
},
6261
},
6362
};
6463
return r.run(&app);
6564
}
6665
67-
// Action function to execute when the "short" command is invoked.
68-
fn run_server() !void {
69-
// Log a debug message indicating the server is listening on the specified host and port.
70-
std.log.debug("server is listening on {s}:{d}", .{ config.host, config.port });
66+
fn run() !void {
67+
std.log.info("listening on {s}:{d}", .{ config.host, config.port });
7168
}
7269
```
7370

74-
### Using with the Zig package manager
75-
Add `cli` to your `build.zig.zon`
76-
```
77-
# For zig master
71+
### Installing with the Zig package manager
72+
73+
**1. Fetch the package:**
74+
75+
```sh
76+
# For zig master and zig 0.16
7877
zig fetch --save git+https://github.com/sam701/zig-cli
7978

8079
# For zig 0.15
8180
zig fetch --save git+https://github.com/sam701/zig-cli#zig-0.15
8281
```
8382

84-
## Printing help
85-
See [`simple.zig`](./examples/simple.zig)
83+
**2. Wire it up in `build.zig`:**
84+
85+
```zig
86+
const cli_dep = b.dependency("cli", .{});
87+
exe.root_module.addImport("cli", cli_dep.module("cli"));
88+
```
89+
90+
## Help output
91+
92+
See [`simple.zig`](./examples/simple.zig) for a full example with subcommands.
8693

8794
```
8895
$ ./zig-out/bin/simple sub1 --help
@@ -107,4 +114,5 @@ OPTIONS:
107114
```
108115

109116
## License
117+
110118
MIT

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .cli,
33
.fingerprint = 0x48f6513cff9ee2d9,
4-
.version = "0.10.0",
4+
.version = "0.11.0",
55
.paths = .{ "./src", "./build.zig", "./build.zig.zon" },
66
}

examples/short.zig

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
const std = @import("std");
22
const cli = @import("cli");
33

4-
// Define a configuration structure with default values.
54
var config = struct {
65
host: []const u8 = "localhost",
76
port: u16 = undefined,
87
}{};
98

109
pub fn main(init: std.process.Init) !void {
1110
var r = cli.AppRunner.init(&init);
11+
defer r.deinit();
1212

13-
// Create an App with a command named "short" that takes host and port options.
1413
const app = cli.App{
1514
.command = cli.Command{
16-
.name = "short",
15+
.name = "server",
1716
.options = try r.allocOptions(&.{
18-
// Define an Option for the "host" command-line argument.
1917
.{
2018
.long_name = "host",
2119
.help = "host to listen on",
2220
.value_ref = r.mkRef(&config.host),
2321
},
24-
25-
// Define an Option for the "port" command-line argument.
2622
.{
2723
.long_name = "port",
2824
.help = "port to bind to",
@@ -31,15 +27,13 @@ pub fn main(init: std.process.Init) !void {
3127
},
3228
}),
3329
.target = cli.CommandTarget{
34-
.action = cli.CommandAction{ .exec = run_server },
30+
.action = cli.CommandAction{ .exec = run },
3531
},
3632
},
3733
};
3834
return r.run(&app);
3935
}
4036

41-
// Action function to execute when the "short" command is invoked.
42-
fn run_server() !void {
43-
// Log a debug message indicating the server is listening on the specified host and port.
44-
std.log.debug("server is listening on {s}:{d}", .{ config.host, config.port });
37+
fn run() !void {
38+
std.log.info("listening on {s}:{d}", .{ config.host, config.port });
4539
}

examples/simple.zig

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,7 @@ fn sub3(r: *cli.AppRunner) !cli.Command {
4949
};
5050
}
5151

52-
fn parseArgs(init: *const std.process.Init) cli.AppRunner.Error!cli.ExecFn {
53-
// This allocator will be used to allocate config.ip and config.arg2.
54-
var r = cli.AppRunner.init(init);
55-
52+
fn parseArgs(r: *cli.AppRunner) cli.AppRunner.Error!cli.ExecFn {
5653
const sub2 = cli.Command{
5754
.name = "sub2",
5855
.target = cli.CommandTarget{
@@ -62,10 +59,9 @@ fn parseArgs(init: *const std.process.Init) cli.AppRunner.Error!cli.ExecFn {
6259
},
6360
};
6461

65-
// Since we call r.getAction in this fuction, all r.alloc* invocation are unnecessary.
66-
// We can directly pass slices of commands, options, and posititional arguments,
62+
// Since we call r.getAction in this function, all r.alloc* invocation are unnecessary.
63+
// We can directly pass slices of commands, options, and positional arguments,
6764
// like `.options = &.{....}`
68-
6965
const app = cli.App{
7066
.option_envvar_prefix = "CLI_",
7167
.command = cli.Command{
@@ -113,7 +109,7 @@ fn parseArgs(init: *const std.process.Init) cli.AppRunner.Error!cli.ExecFn {
113109
},
114110
}),
115111
.target = cli.CommandTarget{
116-
.subcommands = try r.allocCommands(&.{ sub2, try sub3(&r) }),
112+
.subcommands = try r.allocCommands(&.{ sub2, try sub3(r) }),
117113
},
118114
},
119115
}),
@@ -127,20 +123,23 @@ fn parseArgs(init: *const std.process.Init) cli.AppRunner.Error!cli.ExecFn {
127123
}
128124

129125
pub fn main(init: std.process.Init) anyerror!void {
130-
const action = try parseArgs(&init);
126+
var r = cli.AppRunner.init(&init);
127+
defer r.deinit();
128+
129+
const action = try parseArgs(&r);
131130
return action();
132131
}
133132

134133
fn run_sub3() !void {
135134
const c = &config;
136-
std.log.debug("int: {}", .{c.int});
137-
std.log.debug("sub3: arg1: {}", .{c.arg1});
135+
std.log.info("int: {}", .{c.int});
136+
std.log.info("sub3: arg1: {}", .{c.arg1});
138137
for (c.arg2) |arg| {
139-
std.log.debug("sub3: arg2: {s}", .{arg});
138+
std.log.info("sub3: arg2: {s}", .{arg});
140139
}
141140
}
142141

143142
fn run_sub2() !void {
144143
const c = &config;
145-
std.log.debug("running sub2: ip={s}, bool={any}, float={any}", .{ c.ip, c.bool, c.float });
144+
std.log.info("running sub2: ip={s}, bool={any}, float={any}", .{ c.ip, c.bool, c.float });
146145
}

src/app_runner.zig

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ const Printer = @import("Printer.zig");
1010
const command = @import("command.zig");
1111
const help = @import("./help.zig");
1212

13+
/// Owns all memory allocated during argument parsing and action setup.
14+
/// Call `deinit` to free everything at once (typically via `defer r.deinit()`).
1315
pub const AppRunner = struct {
14-
// Arena allocator for temporary data during parsing (ValueRefs, argument slices, etc.)
15-
// that is freed immediately after parsing completes.
16-
// The original allocator is used for data that outlives the parsing phase.
16+
/// Arena that backs all allocations made by this runner, including parsed values
17+
/// that outlive `getAction`. Publicly accessible so callers can allocate additional
18+
/// data with the same lifetime and have it freed together on `deinit`.
1719
arena: ArenaAllocator,
18-
orig_allocator: Allocator,
1920
io: std.Io,
2021
environ: *const std.process.Environ.Map,
2122
args: *const std.process.Args,
@@ -24,7 +25,6 @@ pub const AppRunner = struct {
2425
pub fn init(orig_init: *const std.process.Init) Self {
2526
return .{
2627
.arena = ArenaAllocator.init(orig_init.gpa),
27-
.orig_allocator = orig_init.gpa,
2828
.io = orig_init.io,
2929
.environ = orig_init.environ_map,
3030
.args = &orig_init.minimal.args,
@@ -63,11 +63,10 @@ pub const AppRunner = struct {
6363
const iter = try self.args.iterateAllocator(self.arena.allocator());
6464

6565
// Here we pass the child allocator because any values allocated on the client behalf may not be freed.
66-
var cr = try Parser(std.process.Args.Iterator).init(app, iter, self.io, self.orig_allocator, self.environ);
66+
var cr = try Parser(std.process.Args.Iterator).init(app, iter, self.io, self.arena.allocator(), self.environ);
6767
defer cr.deinit();
6868

6969
if (cr.parse()) |action| {
70-
self.deinit();
7170
return action;
7271
} else |err| {
7372
var buffer: [4096]u8 = undefined;
@@ -79,9 +78,9 @@ pub const AppRunner = struct {
7978
try help.print_command_help(
8079
&printer,
8180
app,
82-
try cr.command_path.toOwnedSlice(self.orig_allocator),
81+
try cr.command_path.toOwnedSlice(self.arena.allocator()),
8382
cr.global_options,
84-
self.orig_allocator,
83+
self.arena.allocator(),
8584
);
8685
}
8786
std.process.exit(1);

0 commit comments

Comments
 (0)