Skip to content

Commit fb6bf56

Browse files
Merge pull request #7573 from roc-lang/zig-cli
Improve roc cli
2 parents 015e30b + 2a7713e commit fb6bf56

File tree

4 files changed

+299
-84
lines changed

4 files changed

+299
-84
lines changed

build.zig

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,13 @@ pub fn build(b: *std.Build) void {
44
const target = b.standardTargetOptions(.{});
55
const optimize = b.standardOptimizeOption(.{});
66

7-
const main_path = b.path("src/main.zig");
8-
97
const exe = b.addExecutable(.{
108
.name = "roc",
11-
.root_source_file = main_path,
9+
.root_source_file = b.path("src/main.zig"),
1210
.target = target,
1311
.optimize = optimize,
1412
});
1513

16-
const main_tests = b.addTest(.{ .root_source_file = main_path });
17-
const test_cmd = b.addRunArtifact(main_tests);
18-
19-
const test_step = b.step("test", "Run tests");
20-
test_step.dependOn(&test_cmd.step);
21-
2214
b.installArtifact(exe);
2315

2416
const run_cmd = b.addRunArtifact(exe);
@@ -31,4 +23,22 @@ pub fn build(b: *std.Build) void {
3123

3224
const run_step = b.step("run", "Build and run the roc cli");
3325
run_step.dependOn(&run_cmd.step);
26+
27+
const all_tests = b.addTest(.{
28+
.root_source_file = b.path("src/test.zig"),
29+
.target = target,
30+
.optimize = optimize,
31+
});
32+
33+
// Install the test binary so we can run separately
34+
// ```sh
35+
// $ zig build && ./zig-out/bin/test
36+
// ```
37+
b.installArtifact(all_tests);
38+
39+
const run_tests = b.addRunArtifact(all_tests);
40+
41+
const test_step = b.step("test", "Run all tests included in src/tests.zig");
42+
43+
test_step.dependOn(&run_tests.step);
3444
}

src/command.zig

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
const std = @import("std");
2+
const testing = std.testing;
3+
const mem = std.mem;
4+
5+
pub const RocCmd = enum {
6+
roc_run,
7+
roc_build,
8+
roc_test,
9+
roc_repl,
10+
roc_format,
11+
roc_version,
12+
roc_check,
13+
roc_docs,
14+
roc_glue,
15+
roc_help,
16+
17+
pub fn parse(str: []const u8) ?RocCmd {
18+
const map = std.static_string_map.StaticStringMap(RocCmd).initComptime(.{
19+
.{ "run", .roc_run },
20+
.{ "build", .roc_build },
21+
.{ "test", .roc_test },
22+
.{ "repl", .roc_repl },
23+
.{ "format", .roc_format },
24+
.{ "version", .roc_version },
25+
.{ "check", .roc_check },
26+
.{ "docs", .roc_docs },
27+
.{ "glue", .roc_glue },
28+
.{ "help", .roc_help },
29+
});
30+
31+
if (map.get(str)) |cmd| {
32+
return cmd;
33+
}
34+
return null;
35+
}
36+
};
37+
38+
test "parse cli subcommands" {
39+
try testing.expectEqual(RocCmd.parse("build").?, .roc_build);
40+
try testing.expectEqual(RocCmd.parse(""), null);
41+
}
42+
43+
pub const RocOpt = struct {
44+
opt: enum {
45+
none,
46+
size,
47+
speed,
48+
} = .none,
49+
emit_llvm_ir: bool = false,
50+
profiling: bool = false,
51+
timing: bool = false,
52+
fuzzing: bool = false,
53+
54+
pub fn parse(args: []const []const u8) !struct { opt: RocOpt, next_index: usize } {
55+
var opt = RocOpt{};
56+
var i: usize = 0;
57+
while (i < args.len) : (i += 1) {
58+
const arg = args[i];
59+
60+
// If argument doesn't start with '-', we're done parsing options
61+
if (arg.len == 0 or arg[0] != '-') {
62+
return .{ .opt = opt, .next_index = i };
63+
}
64+
65+
if (mem.eql(u8, arg, "--opt")) {
66+
// Check if there's a next argument
67+
if (i + 1 >= args.len) {
68+
return error.MissingOptValue;
69+
}
70+
i += 1;
71+
const value = args[i];
72+
73+
if (mem.eql(u8, value, "none")) {
74+
opt.opt = .none;
75+
} else if (mem.eql(u8, value, "size")) {
76+
opt.opt = .size;
77+
} else if (mem.eql(u8, value, "speed")) {
78+
opt.opt = .speed;
79+
} else {
80+
return error.InvalidOptValue;
81+
}
82+
} else if (mem.eql(u8, arg, "--emit-llvm-ir")) {
83+
opt.emit_llvm_ir = true;
84+
} else if (mem.eql(u8, arg, "--profiling")) {
85+
opt.profiling = true;
86+
} else if (mem.eql(u8, arg, "--time")) {
87+
opt.timing = true;
88+
} else if (mem.eql(u8, arg, "--fuzz")) {
89+
opt.fuzzing = true;
90+
} else {
91+
return error.InvalidArgument;
92+
}
93+
}
94+
95+
return .{ .opt = opt, .next_index = i };
96+
}
97+
};
98+
99+
test "default options" {
100+
try testing.expectEqual(RocOpt{}, RocOpt{
101+
.opt = .none,
102+
.emit_llvm_ir = false,
103+
.profiling = false,
104+
.timing = false,
105+
.fuzzing = false,
106+
});
107+
}
108+
109+
// Testing helper, split a string into arguments ignoring whitespace and empty strings
110+
fn splitArgs(allocator: std.mem.Allocator, str: []const u8) ![]const []const u8 {
111+
var args = std.ArrayList([]const u8).init(allocator);
112+
errdefer args.deinit();
113+
114+
var iter = std.mem.split(u8, str, " ");
115+
while (iter.next()) |arg| {
116+
if (arg.len > 0) {
117+
try args.append(arg);
118+
}
119+
}
120+
121+
return args.toOwnedSlice();
122+
}
123+
124+
test "parsing cli options" {
125+
const TestCase = struct {
126+
args: []const u8,
127+
expected: union(enum) {
128+
ok: struct {
129+
opt: RocOpt,
130+
next_index: usize,
131+
},
132+
err: anyerror,
133+
},
134+
};
135+
136+
const test_cases = &[_]TestCase{
137+
.{
138+
.args = "",
139+
.expected = .{ .ok = .{ .opt = RocOpt{}, .next_index = 0 } },
140+
},
141+
.{
142+
.args = "--opt size",
143+
.expected = .{ .ok = .{ .opt = RocOpt{ .opt = .size }, .next_index = 2 } },
144+
},
145+
.{
146+
.args = "--opt speed app.roc",
147+
.expected = .{ .ok = .{ .opt = RocOpt{ .opt = .speed }, .next_index = 2 } },
148+
},
149+
.{
150+
.args = "--time build app.roc",
151+
.expected = .{ .ok = .{ .opt = RocOpt{ .timing = true }, .next_index = 1 } },
152+
},
153+
.{
154+
.args = "--opt size --time app.roc",
155+
.expected = .{ .ok = .{
156+
.opt = RocOpt{ .opt = .size, .timing = true },
157+
.next_index = 3,
158+
} },
159+
},
160+
.{
161+
.args = "build --time",
162+
.expected = .{ .ok = .{ .opt = RocOpt{}, .next_index = 0 } },
163+
},
164+
.{
165+
.args = "--opt",
166+
.expected = .{ .err = error.MissingOptValue },
167+
},
168+
.{
169+
.args = "--opt invalid",
170+
.expected = .{ .err = error.InvalidOptValue },
171+
},
172+
.{
173+
.args = "--unknown-flag",
174+
.expected = .{ .err = error.InvalidArgument },
175+
},
176+
.{
177+
.args = "app.roc --invalid",
178+
.expected = .{ .ok = .{ .opt = RocOpt{}, .next_index = 0 } },
179+
},
180+
};
181+
182+
for (test_cases) |tc| {
183+
const args = try splitArgs(testing.allocator, tc.args);
184+
defer testing.allocator.free(args);
185+
186+
switch (tc.expected) {
187+
.ok => |expected| {
188+
const result = try RocOpt.parse(args);
189+
try testing.expectEqual(expected.opt, result.opt);
190+
try testing.expectEqual(expected.next_index, result.next_index);
191+
},
192+
.err => |expected_err| {
193+
const result = RocOpt.parse(args);
194+
try testing.expectError(expected_err, result);
195+
},
196+
}
197+
}
198+
}

0 commit comments

Comments
 (0)