diff --git a/common/flags.zig b/common/flags.zig index 561d52b..de5304f 100644 --- a/common/flags.zig +++ b/common/flags.zig @@ -1,105 +1,100 @@ -// This file is part of river, a dynamic tiling wayland compositor. +// Zero allocation argument parsing for unix-like systems. +// Released under the Zero Clause BSD (0BSD) license: // -// Copyright 2021 The River Developers +// Copyright 2023 Isaac Freund // -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 3. +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. // -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. const std = @import("std"); -const cstr = std.cstr; +const mem = std.mem; pub const Flag = struct { - name: [*:0]const u8, + name: []const u8, kind: enum { boolean, arg }, }; -pub fn ParseResult(comptime flags: []const Flag) type { +pub fn parser(comptime Arg: type, comptime flags: []const Flag) type { + switch (Arg) { + // TODO consider allowing []const u8 + [:0]const u8, [*:0]const u8 => {}, // ok + else => @compileError("invalid argument type: " ++ @typeName(Arg)), + } return struct { - const Self = @This(); + pub const Result = struct { + /// Remaining args after the recognized flags + args: []const Arg, + /// Data obtained from parsed flags + flags: Flags, - const FlagData = struct { - name: [*:0]const u8, - value: union { - boolean: bool, - arg: ?[*:0]const u8, - }, + pub const Flags = flags_type: { + var fields: []const std.builtin.TypeInfo.StructField = &.{}; + inline for (flags) |flag| { + const field: std.builtin.TypeInfo.StructField = switch (flag.kind) { + .boolean => .{ + .name = flag.name, + .field_type = bool, + .default_value = false, + .is_comptime = false, + .alignment = @alignOf(bool), + }, + .arg => .{ + .name = flag.name, + .field_type = ?[:0]const u8, + .default_value = @as(??[:0]const u8, @as(?[:0]const u8, null)), + .is_comptime = false, + .alignment = @alignOf(?[:0]const u8), + }, + }; + fields = fields ++ [_]std.builtin.TypeInfo.StructField{field}; + } + break :flags_type @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = fields, + .decls = &.{}, + .is_tuple = false, + } }); + }; }; - /// Remaining args after the recognized flags - args: [][*:0]const u8, - /// Data obtained from parsed flags - flag_data: [flags.len]FlagData = blk: { - // Init all flags to false/null - var flag_data: [flags.len]FlagData = undefined; - inline for (flags) |flag, i| { - flag_data[i] = switch (flag.kind) { - .boolean => .{ - .name = flag.name, - .value = .{ .boolean = false }, - }, - .arg => .{ - .name = flag.name, - .value = .{ .arg = null }, - }, - }; - } - break :blk flag_data; - }, + pub fn parse(args: []const Arg) !Result { + var result_flags: Result.Flags = .{}; - pub fn boolFlag(self: Self, flag_name: [*:0]const u8) bool { - for (self.flag_data) |flag_data| { - if (cstr.cmp(flag_data.name, flag_name) == 0) return flag_data.value.boolean; - } - unreachable; // Invalid flag_name - } - - pub fn argFlag(self: Self, flag_name: [*:0]const u8) ?[:0]const u8 { - for (self.flag_data) |flag_data| { - if (cstr.cmp(flag_data.name, flag_name) == 0) { - return std.mem.span(flag_data.value.arg); + var i: usize = 0; + while (i < args.len) : (i += 1) { + var parsed_flag = false; + inline for (flags) |flag| { + if (mem.eql(u8, flag.name, mem.span(args[i]))) { + switch (flag.kind) { + .boolean => @field(result_flags, flag.name) = true, + .arg => { + i += 1; + if (i == args.len) { + std.log.err("option '" ++ flag.name ++ + "' requires an argument but none was provided!", .{}); + return error.MissingFlagArgument; + } + @field(result_flags, flag.name) = mem.span(args[i]); + }, + } + parsed_flag = true; + } } + if (!parsed_flag) break; } - unreachable; // Invalid flag_name + + return Result{ + .args = args[i..], + .flags = result_flags, + }; } }; } - -pub fn parse(args: [][*:0]const u8, comptime flags: []const Flag) !ParseResult(flags) { - var ret: ParseResult(flags) = .{ .args = undefined }; - - var arg_idx: usize = 0; - while (arg_idx < args.len) : (arg_idx += 1) { - var parsed_flag = false; - inline for (flags) |flag, flag_idx| { - if (cstr.cmp(flag.name, args[arg_idx]) == 0) { - switch (flag.kind) { - .boolean => ret.flag_data[flag_idx].value.boolean = true, - .arg => { - arg_idx += 1; - if (arg_idx == args.len) { - std.log.err("option '" ++ flag.name ++ - "' requires an argument but none was provided!", .{}); - return error.MissingFlagArgument; - } - ret.flag_data[flag_idx].value.arg = args[arg_idx]; - }, - } - parsed_flag = true; - } - } - if (!parsed_flag) break; - } - - ret.args = args[arg_idx..]; - - return ret; -} diff --git a/river/main.zig b/river/main.zig index d9fc443..ad53c2a 100644 --- a/river/main.zig +++ b/river/main.zig @@ -44,16 +44,16 @@ pub var server: Server = undefined; pub fn main() anyerror!void { // This line is here because of https://github.com/ziglang/zig/issues/7807 const argv: [][*:0]const u8 = os.argv; - const result = flags.parse(argv[1..], &[_]flags.Flag{ + const result = flags.parser([*:0]const u8, &.{ .{ .name = "-h", .kind = .boolean }, .{ .name = "-version", .kind = .boolean }, .{ .name = "-c", .kind = .arg }, .{ .name = "-log-level", .kind = .arg }, - }) catch { + }).parse(argv[1..]) catch { try io.getStdErr().writeAll(usage); os.exit(1); }; - if (result.boolFlag("-h")) { + if (result.flags.@"-h") { try io.getStdOut().writeAll(usage); os.exit(0); } @@ -63,11 +63,11 @@ pub fn main() anyerror!void { os.exit(1); } - if (result.boolFlag("-version")) { + if (result.flags.@"-version") { try io.getStdOut().writeAll(build_options.version ++ "\n"); os.exit(0); } - if (result.argFlag("-log-level")) |level| { + if (result.flags.@"-log-level") |level| { if (mem.eql(u8, level, std.log.Level.err.asText())) { runtime_log_level = .err; } else if (mem.eql(u8, level, std.log.Level.warn.asText())) { @@ -83,7 +83,7 @@ pub fn main() anyerror!void { } } const startup_command = blk: { - if (result.argFlag("-c")) |command| { + if (result.flags.@"-c") |command| { break :blk try util.gpa.dupeZ(u8, command); } else { break :blk try defaultInitPath(); diff --git a/riverctl/main.zig b/riverctl/main.zig index 853b94e..4cc6717 100644 --- a/riverctl/main.zig +++ b/riverctl/main.zig @@ -65,18 +65,18 @@ pub fn main() !void { fn _main() !void { // This line is here because of https://github.com/ziglang/zig/issues/7807 const argv: [][*:0]const u8 = os.argv; - const result = flags.parse(argv[1..], &[_]flags.Flag{ + const result = flags.parser([*:0]const u8, &.{ .{ .name = "-h", .kind = .boolean }, .{ .name = "-version", .kind = .boolean }, - }) catch { + }).parse(argv[1..]) catch { try io.getStdErr().writeAll(usage); os.exit(1); }; - if (result.boolFlag("-h")) { + if (result.flags.@"-h") { try io.getStdOut().writeAll(usage); os.exit(0); } - if (result.boolFlag("-version")) { + if (result.flags.@"-version") { try io.getStdOut().writeAll(@import("build_options").version ++ "\n"); os.exit(0); } diff --git a/rivertile/main.zig b/rivertile/main.zig index bf28bf7..a5f4ce0 100644 --- a/rivertile/main.zig +++ b/rivertile/main.zig @@ -306,7 +306,7 @@ const Output = struct { pub fn main() !void { // https://github.com/ziglang/zig/issues/7807 const argv: [][*:0]const u8 = os.argv; - const result = flags.parse(argv[1..], &[_]flags.Flag{ + const result = flags.parser([*:0]const u8, &[_]flags.Flag{ .{ .name = "-h", .kind = .boolean }, .{ .name = "-version", .kind = .boolean }, .{ .name = "-view-padding", .kind = .arg }, @@ -314,37 +314,37 @@ pub fn main() !void { .{ .name = "-main-location", .kind = .arg }, .{ .name = "-main-count", .kind = .arg }, .{ .name = "-main-ratio", .kind = .arg }, - }) catch { + }).parse(argv[1..]) catch { try std.io.getStdErr().writeAll(usage); os.exit(1); }; - if (result.boolFlag("-h")) { + if (result.flags.@"-h") { try std.io.getStdOut().writeAll(usage); os.exit(0); } if (result.args.len != 0) fatalPrintUsage("unknown option '{s}'", .{result.args[0]}); - if (result.boolFlag("-version")) { + if (result.flags.@"-version") { try std.io.getStdOut().writeAll(@import("build_options").version ++ "\n"); os.exit(0); } - if (result.argFlag("-view-padding")) |raw| { + if (result.flags.@"-view-padding") |raw| { view_padding = fmt.parseUnsigned(u31, raw, 10) catch fatalPrintUsage("invalid value '{s}' provided to -view-padding", .{raw}); } - if (result.argFlag("-outer-padding")) |raw| { + if (result.flags.@"-outer-padding") |raw| { outer_padding = fmt.parseUnsigned(u31, raw, 10) catch fatalPrintUsage("invalid value '{s}' provided to -outer-padding", .{raw}); } - if (result.argFlag("-main-location")) |raw| { + if (result.flags.@"-main-location") |raw| { default_main_location = std.meta.stringToEnum(Location, raw) orelse fatalPrintUsage("invalid value '{s}' provided to -main-location", .{raw}); } - if (result.argFlag("-main-count")) |raw| { + if (result.flags.@"-main-count") |raw| { default_main_count = fmt.parseUnsigned(u31, raw, 10) catch fatalPrintUsage("invalid value '{s}' provided to -main-count", .{raw}); } - if (result.argFlag("-main-ratio")) |raw| { + if (result.flags.@"-main-ratio") |raw| { default_main_ratio = fmt.parseFloat(f64, raw) catch { fatalPrintUsage("invalid value '{s}' provided to -main-ratio", .{raw}); };