diff options
author | Yorhel <git@yorhel.nl> | 2021-04-29 18:59:25 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2021-04-29 18:59:25 +0200 |
commit | e2805da07630d2e7e020abab3db73b955c269c33 (patch) | |
tree | 83927f307a11939b586dd48ede72d81a8e89beaf | |
parent | 0783d357937e5e705321c0f92f6c4043f317b909 (diff) |
Add CLI argument parsing
-rw-r--r-- | src/main.zig | 165 | ||||
-rw-r--r-- | src/scan.zig | 10 |
2 files changed, 171 insertions, 4 deletions
diff --git a/src/main.zig b/src/main.zig index 65ab30d..4d5c056 100644 --- a/src/main.zig +++ b/src/main.zig @@ -24,6 +24,88 @@ pub const Config = struct { pub var config = Config{}; +fn die(comptime fmt: []const u8, args: anytype) noreturn { + _ = std.io.getStdErr().writer().print(fmt, args) catch {}; + std.process.exit(1); +} + +// Simple generic argument parser, supports getopt_long() style arguments. +// T can be any type that has a 'fn next(T) ?[]const u8' method, e.g.: +// var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); +fn Args(T: anytype) type { + return struct { + it: T, + short: ?[]const u8 = null, // Remainder after a short option, e.g. -x<stuff> (which may be either more short options or an argument) + last: ?[]const u8 = null, + last_arg: ?[]const u8 = null, // In the case of --option=<arg> + shortbuf: [2]u8 = undefined, + argsep: bool = false, + + const Self = @This(); + const Option = struct { + opt: bool, + val: []const u8, + + fn is(self: @This(), cmp: []const u8) bool { + return self.opt and std.mem.eql(u8, self.val, cmp); + } + }; + + fn init(it: T) Self { + return Self{ .it = it }; + } + + pub fn shortopt(self: *Self, s: []const u8) Option { + self.shortbuf[0] = '-'; + self.shortbuf[1] = s[0]; + self.short = if (s.len > 1) s[1..] else null; + self.last = &self.shortbuf; + return .{ .opt = true, .val = &self.shortbuf }; + } + + /// Return the next option or positional argument. + /// 'opt' indicates whether it's an option or positional argument, + /// 'val' will be either -x, --something or the argument. + pub fn next(self: *Self) ?Option { + if (self.last_arg != null) die("Option '{s}' does not expect an argument.\n", .{ self.last.? }); + if (self.short) |s| return self.shortopt(s); + const val = self.it.next() orelse return null; + if (self.argsep or val.len == 0 or val[0] != '-') return Option{ .opt = false, .val = val }; + if (val.len == 1) die("Invalid option '-'.\n", .{}); + if (val.len == 2 and val[1] == '-') { + self.argsep = true; + return self.next(); + } + if (val[1] == '-') { + if (std.mem.indexOfScalar(u8, val, '=')) |sep| { + if (sep == 2) die("Invalid option '{s}'.\n", .{val}); + self.last_arg = val[sep+1.. :0]; + self.last = val[0..sep]; + return Option{ .opt = true, .val = self.last.? }; + } + self.last = val; + return Option{ .opt = true, .val = val }; + } + return self.shortopt(val[1..]); + } + + /// Returns the argument given to the last returned option. Dies with an error if no argument is provided. + pub fn arg(self: *Self) []const u8 { + if (self.short) |a| { + defer self.short = null; + return a; + } + if (self.last_arg) |a| { + defer self.last_arg = null; + return a; + } + if (self.it.next()) |o| return o; + die("Option '{s}' requires an argument.\n", .{ self.last.? }); + } + }; +} + + // For debugging fn writeTree(out: anytype, e: *model.Entry, indent: u32) @TypeOf(out).Error!void { var i: u32 = 0; @@ -61,12 +143,93 @@ fn writeTree(out: anytype, e: *model.Entry, indent: u32) @TypeOf(out).Error!void } } +fn version() noreturn { + // TODO: don't hardcode this version here. + _ = std.io.getStdOut().writer().writeAll("ncdu 2.0\n") catch {}; + std.process.exit(0); +} + +fn help() noreturn { + // TODO + _ = std.io.getStdOut().writer().writeAll("ncdu 2.0\n") catch {}; + std.process.exit(0); +} + pub fn main() anyerror!void { + var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); + var scan_dir: ?[]const u8 = null; + _ = args.next(); // program name + while (args.next()) |opt| { + if (!opt.opt) { + // XXX: ncdu 1.x doesn't error, it just silently ignores all but the last argument. + if (scan_dir != null) die("Multiple directories given, see ncdu -h for help.\n", .{}); + scan_dir = opt.val; + continue; + } + if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help() + else if(opt.is("-v") or opt.is("-V") or opt.is("--version")) version() + else if(opt.is("-q")) config.update_delay = 2000 + else if(opt.is("-x")) config.same_fs = true + else if(opt.is("-e")) config.extended = true + else if(opt.is("-r") and config.read_only) config.can_shell = false + else if(opt.is("-r")) config.read_only = true + else if(opt.is("--si")) config.si = true + else if(opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true + else if(opt.is("--exclude-caches")) config.exclude_caches = true + else if(opt.is("--exclude-kernfs")) config.exclude_kernfs = true + else if(opt.is("--confirm-quit")) config.confirm_quit = true + else die("Unrecognized option '{s}'.\n", .{opt.val}); + // TODO: -o, -f, -0, -1, -2, --exclude, -X, --exclude-from, --color + } + std.log.info("align={}, Entry={}, Dir={}, Link={}, File={}.", .{@alignOf(model.Dir), @sizeOf(model.Entry), @sizeOf(model.Dir), @sizeOf(model.Link), @sizeOf(model.File)}); - try scan.scanRoot("/"); + try scan.scanRoot(scan_dir orelse "."); //var out = std.io.bufferedWriter(std.io.getStdOut().writer()); //try writeTree(out.writer(), &model.root.entry, 0); //try out.flush(); } + + +test "argument parser" { + const L = struct { + lst: []const [:0]const u8, + idx: usize = 0, + fn next(s: *@This()) ?[:0]const u8 { + if (s.idx == s.lst.len) return null; + defer s.idx += 1; + return s.lst[s.idx]; + } + }; + const lst = [_][:0]const u8{ "a", "-abcd=e", "--opt1=arg1", "--opt2", "arg2", "-x", "foo", "", "--", "--arg", "", "-", }; + const l = L{ .lst = &lst }; + const T = struct { + a: Args(L), + fn opt(self: *@This(), isopt: bool, val: []const u8) void { + const o = self.a.next().?; + std.testing.expectEqual(isopt, o.opt); + std.testing.expectEqualStrings(val, o.val); + std.testing.expectEqual(o.is(val), isopt); + } + fn arg(self: *@This(), val: []const u8) void { + std.testing.expectEqualStrings(val, self.a.arg()); + } + }; + var t = T{ .a = Args(L).init(l) }; + t.opt(false, "a"); + t.opt(true, "-a"); + t.opt(true, "-b"); + t.arg("cd=e"); + t.opt(true, "--opt1"); + t.arg("arg1"); + t.opt(true, "--opt2"); + t.arg("arg2"); + t.opt(true, "-x"); + t.arg("foo"); + t.opt(false, ""); + t.opt(false, "--arg"); + t.opt(false, ""); + t.opt(false, "-"); + std.testing.expectEqual(t.a.next(), null); +} diff --git a/src/scan.zig b/src/scan.zig index dacb7ff..c0c02b4 100644 --- a/src/scan.zig +++ b/src/scan.zig @@ -124,10 +124,14 @@ fn scanDir(parents: *model.Parents, parent: std.fs.Dir) std.mem.Allocator.Error! } } -pub fn scanRoot(path: [:0]const u8) !void { - const stat = try readStat(std.fs.cwd(), path); +pub fn scanRoot(path: []const u8) !void { + // XXX: Both realpathAlloc() and toPosixPath are limited to PATH_MAX. + // Oh well, I suppose we can accept that as limitation for the top-level dir we're scanning. + const full_path = try std.os.toPosixPath(try std.fs.realpathAlloc(main.allocator, path)); + + const stat = try readStat(std.fs.cwd(), &full_path); if (!stat.dir) return error.NotADirectory; - model.root = (try model.Entry.create(.dir, false, path)).dir().?; + model.root = (try model.Entry.create(.dir, false, &full_path)).dir().?; model.root.entry.blocks = stat.blocks; model.root.entry.size = stat.size; model.root.dev = try model.getDevId(stat.dev); |