diff options
Diffstat (limited to 'src/cli.zig')
| -rw-r--r-- | src/cli.zig | 543 |
1 files changed, 313 insertions, 230 deletions
diff --git a/src/cli.zig b/src/cli.zig index 8c5e37a..5b43e96 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -26,16 +26,150 @@ //! during compile time. There are two versions for a help text: a short //! and a long version. The short version displays the usage pattern of //! the command whereas the long version displays a detailed help text. +//! +//! The CLI contains global variables for argument parsing. These variables +//! need to be initialized with the `init` function before the package can be +//! used. const std = @import("std"); const assert = std.debug.assert; -var stdout_buf: [1024]u8 = undefined; -var stdout_printer: Printer = .init(std.fs.File.stdout().writer(&stdout_buf)); -var stderr_printer: Printer = .init(std.fs.File.stderr().writer(&.{})); +var stdout_buf: [256]u8 = undefined; +var stderr_buf: [0]u8 = undefined; +var stdout_printer: Printer = .init(std.posix.STDOUT_FILENO, &stdout_buf); +var stderr_printer: Printer = .init(std.posix.STDERR_FILENO, &stderr_buf); + +pub const stdout = &stdout_printer; +pub const stderr = &stderr_printer; + +/// The command we are currently in or null if we reached the end or positional arguments. +var current_cmd: ?[]const u8 = null; +var arg_iter: std.process.Args.Iterator = undefined; +var exe_name: []const u8 = ""; + +/// Initialize the global CLI variables and return the program (i.e. the main +/// entry command). +pub fn init(comptime Program: type, args: std.process.Args) Program { + const cmd_info = comptime Command_Info.from_command(Program); + var program: Program = undefined; + + arg_iter = args.iterate(); + _ = arg_iter.skip(); // skip executable name + current_cmd = parse_opts(cmd_info, &program); + exe_name = cmd_info.name; + return program; +} + +/// Return the next subcommand of the given command type. The next subcommand +/// is the name of the next argument. It also parses all options and prefills +/// them in the returned subcommand. +pub fn next_subcommand(comptime Command: type) ?Command.Subcommands { + const subcommands = comptime Command_Info.from_subcommand(Command); + comptime assert(subcommands.len > 0); + + const arg = current_cmd orelse return null; + inline for (subcommands) |subcmd| { + if (std.mem.eql(u8, arg, subcmd.name)) { + var cmd: subcmd.type = undefined; + current_cmd = parse_opts(subcmd, &cmd); + return @unionInit(Command.Subcommands, subcmd.tag.?, cmd); + } + } + fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, exe_name }); +} + +/// Return the next positional argument or `null` if there are no more arguments. +pub fn next_positional() ?[]const u8 { + if (current_cmd) |cmd| { + current_cmd = null; + return cmd; + } + return arg_iter.next(); +} + +/// Parse and fill all command options and return the current command. +fn parse_opts(comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 { + inline for (cmd_info.options) |opt| { + @field(cmd.options, opt.field).init(); + } + + args_loop: while (arg_iter.next()) |arg| { + assert(arg.len > 0); + if (arg.len < 2 or arg[0] != '-') { + // We have either a single dash or a non-option here. Treat these as + // a positional or subcommand (however the executor chooses to use it). + return arg; + } -pub var stdout = &stdout_printer; -pub var stderr = &stderr_printer; + var opt_name: []const u8 = undefined; + if (arg[1] != '-') { + // We check for a short option name. + // If we have a boolean flag or an option name longer than a + // single character, we have both the name and the value in + // a single argument. Otherwise we fetch the next argument. + opt_name = arg[1..2]; + if (opt_name[0] == 'h') { + if (arg.len > 2) { + fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg}); + } + cmd_info.print_usage(stderr); + std.process.exit(0); + } + + inline for (cmd_info.options) |opt| { + if (opt.short_name) |short_name| { + if (short_name == opt_name[0]) { + const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: { + const next_arg = arg_iter.next(); + if (next_arg == null or next_arg.?[0] == '-') { + fatal("missing value for option '{c}'", .{opt_name[0]}); + } + break :next next_arg.?; + }; + @field(cmd.options, opt.field).parse(opt_name, value); + continue :args_loop; + } + } + } + } else if (arg.len > 2) { + // We check for a long option name. + // If we don't have the format `name=value`, we get the + // next argument lazily, because for boolean flags we + // don't need any extra argument. + opt_name = arg[2..]; + var value: ?[:0]const u8 = null; + if (std.mem.findScalar(u8, arg, '=')) |eq| { + opt_name = arg[2..eq]; + value = arg[eq + 1 ..]; + } + + if (std.mem.eql(u8, opt_name, "help")) { + cmd_info.print_help(); + } + + inline for (cmd_info.options) |opt| { + if (std.mem.eql(u8, opt_name, opt.long_name)) { + value = value orelse if (opt.arg_name.len == 0) "" else next_arg: { + const next = arg_iter.next(); + if (next == null or next.?[0] == '-') { + fatal("missing value for option '{s}'", .{opt_name}); + } + break :next_arg next.?; + }; + @field(cmd.options, opt.field).parse(opt_name, value.?); + continue :args_loop; + } + } + } else { + // A single "--" was detected which means the rest of the arguments + // are interpreted as positional. We are done parsing. + return null; + } + + fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name}); + } + return null; +} pub const Command_Help = struct { pub const Section = struct { @@ -215,145 +349,6 @@ pub fn Option(comptime T: type, comptime opt_help: Option_Help(T)) type { }; } -pub const Args = struct { - /// The command we are currently in or null if we reached the end or positional arguments. - current_cmd: ?[]const u8, - iter: std.process.ArgIterator, - exe_name: []const u8, - - pub fn init(allocator: std.mem.Allocator, comptime exe_name: []const u8, program: anytype) !Args { - comptime assert(exe_name.len > 0); - - var iter = try std.process.ArgIterator.initWithAllocator(allocator); - errdefer iter.deinit(); - _ = iter.skip(); // skip executable name - const cmd_info = Command_Info.from_command(switch (@typeInfo(@TypeOf(program))) { - .pointer => |info| info.child, - else => @compileError("expected pointer, got " ++ @typeName(@TypeOf(program))), - }); - const current_cmd = parse_opts(&iter, cmd_info, program); - return Args{ - .current_cmd = current_cmd, - .iter = iter, - .exe_name = exe_name, - }; - } - - pub fn deinit(self: *Args, allocator: std.mem.Allocator) void { - _ = allocator; - self.iter.deinit(); - } - - pub fn next_subcommand(self: *Args, comptime Command: type) ?Command.Subcommands { - const subcommands = comptime Command_Info.from_subcommand(Command); - comptime assert(subcommands.len > 0); - - const arg = self.current_cmd orelse return null; - inline for (subcommands) |subcmd| { - if (std.mem.eql(u8, arg, subcmd.name)) { - var cmd: subcmd.type = undefined; - self.current_cmd = parse_opts(&self.iter, subcmd, &cmd); - return @unionInit(Command.Subcommands, subcmd.tag.?, cmd); - } - } - fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, self.exe_name }); - } - - pub fn next_positional(self: *Args) ?[]const u8 { - if (self.current_cmd) |cmd| { - self.current_cmd = null; - return cmd; - } - self.current_cmd = null; - return self.iter.next(); - } - - /// Parse and fill all command options and return the current command. - fn parse_opts(iter: *std.process.ArgIterator, comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 { - inline for (cmd_info.options) |opt| { - @field(cmd.options, opt.field).init(); - } - - args_loop: while (iter.next()) |arg| { - assert(arg.len > 0); - if (arg.len < 2 or arg[0] != '-') { - // We have either a single dash or a non-option here. Treat these as - // a positional or subcommand (however the executor chooses to use it). - return arg; - } - - var opt_name: []const u8 = undefined; - if (arg[1] != '-') { - // We check for a short option name. - // If we have a boolean flag or an option name longer than a - // single character, we have both the name and the value in - // a single argument. Otherwise we fetch the next argument. - opt_name = arg[1..2]; - if (opt_name[0] == 'h') { - if (arg.len > 2) { - fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg}); - } - cmd_info.print_usage(stderr); - std.process.exit(0); - } - - inline for (cmd_info.options) |opt| { - if (opt.short_name) |short_name| { - if (short_name == opt_name[0]) { - const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: { - const next_arg = iter.next(); - if (next_arg == null or next_arg.?[0] == '-') { - fatal("missing value for option '{c}'", .{opt_name[0]}); - } - break :next next_arg.?; - }; - @field(cmd.options, opt.field).parse(opt_name, value); - continue :args_loop; - } - } - } - } else if (arg.len > 2) { - // We check for a long option name. - // If we don't have the format `name=value`, we get the - // next argument lazily, because for boolean flags we - // don't need any extra argument. - opt_name = arg[2..]; - var value: ?[:0]const u8 = null; - if (std.mem.indexOfScalar(u8, arg, '=')) |eq| { - opt_name = arg[2..eq]; - value = arg[eq + 1 ..]; - } - - if (std.mem.eql(u8, opt_name, "help")) { - cmd_info.print_help(); - } - - inline for (cmd_info.options) |opt| { - if (std.mem.eql(u8, opt_name, opt.long_name)) { - value = value orelse if (opt.arg_name.len == 0) "" else next_arg: { - const next = iter.next(); - if (next == null or next.?[0] == '-') { - fatal("missing value for option '{s}'", .{opt_name}); - } - break :next_arg next.?; - }; - @field(cmd.options, opt.field).parse(opt_name, value.?); - continue :args_loop; - } - } - } else { - // A single "--" was detected which means the rest of the arguments - // are interpreted as positional. We are done parsing. - return null; - } - - fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name}); - } - - return null; - } -}; - pub const Print_Options = struct { /// Indentation of the first line. margin: u16 = 0, @@ -369,25 +364,30 @@ const Usage_Column = struct { }; pub const Printer = struct { - writer: std.fs.File.Writer, + fd: std.posix.fd_t, max_width: u16, + writer: std.Io.Writer, - fn init(w: std.fs.File.Writer) Printer { + fn init(fd: std.posix.fd_t, buf: []u8) Printer { return Printer{ - .writer = w, + .fd = fd, .max_width = 80, + .writer = .{ + .vtable = &.{ .drain = drain }, + .buffer = buf, + }, }; } - /// Drain the remaining buffered data and puts it to the sink. + /// Drain the remaining buffered data and put it to the sink. pub fn flush(self: *Printer) void { - self.writer.interface.flush() catch {}; + self.writer.flush() catch {}; } /// Write a value directly without splitting. This uses the formatting capabilities /// of `std.fmt`. pub fn write(self: *Printer, comptime fmt: []const u8, value: anytype) void { - self.writer.interface.print(fmt, value) catch {}; + self.writer.print(fmt, value) catch {}; } /// Write a value directly without splitting and append a newline. This uses the @@ -420,7 +420,7 @@ pub const Printer = struct { self.put('\n'); } - /// Print a formatted text and split it into multiple lines if if exceeds + /// Print a formatted text and split it into multiple lines if it exceeds /// a certain width. The splitting behaviour can be configured using the /// given print options. pub fn printf(self: *Printer, comptime format: []const u8, args: anytype, opts: Print_Options) void { @@ -435,7 +435,7 @@ pub const Printer = struct { buf[buf.len - 3] = '.'; buf[buf.len - 2] = '.'; buf[buf.len - 1] = '.'; - break :dots buf[0..]; + break :dots &buf; }, }; self.print(text, opts); @@ -452,7 +452,7 @@ pub const Printer = struct { /// given line of text. pub fn print_lines(self: *Printer, text: []const u8, opts: Print_Options) void { var rest = text; - while (std.mem.indexOfScalar(u8, rest, '\n')) |eol| { + while (std.mem.findScalar(u8, rest, '\n')) |eol| { self.print_line(rest[0..eol], opts); rest = rest[eol + 1 ..]; } @@ -463,11 +463,11 @@ pub const Printer = struct { } fn put(self: *Printer, c: u8) void { - self.writer.interface.writeByte(c) catch {}; + self.writer.writeByte(c) catch {}; } fn put_str(self: *Printer, text: []const u8) void { - self.writer.interface.writeAll(text) catch {}; + self.writer.writeAll(text) catch {}; } fn print_usage_table(self: *Printer, comptime left_column: Usage_Column, comptime right_column: Usage_Column, opts: Print_Options) void { @@ -521,12 +521,12 @@ pub const Printer = struct { if (std.mem.allEqual(u8, text[0..last_split], line_split)) { // There are leading line splits here, so we consider an indentation. // We want to keep indentation intact and print to the next split. - eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len; + eol = std.mem.findScalarPos(u8, text, width, line_split) orelse text.len; } } else { // We don't have a split char in the line. So we extend the line - // to the next space. This hopefully will not happen in the wild. - eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len; + // to the next space. This will hopefully not happen in the wild. + eol = std.mem.findScalarPos(u8, text, width, line_split) orelse text.len; } self.put_str(text[0..eol]); @@ -536,11 +536,51 @@ pub const Printer = struct { fn put_indent(self: *Printer, n: usize) void { const buf = [_]u8{' '} ** 32; var remaining = n; - while (remaining > buf.len) : (remaining -= buf.len) { + while (remaining > buf.len) { self.put_str(&buf); + remaining -= buf.len; } self.put_str(buf[0..remaining]); } + + pub fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + const self: *Printer = @alignCast(@fieldParentPtr("writer", w)); + var iovec: [8]std.posix.iovec_const = undefined; + var iovlen: usize = 0; + if (w.end > 0) { + iovec[iovlen] = .{ .base = w.buffer.ptr, .len = w.end }; + iovlen += 1; + } + var off: usize = 0; + while (iovlen < iovec.len and off < data.len - 1) { + if (data[off].len > 0) { + iovec[iovlen] = .{ .base = data[off].ptr, .len = data[off].len }; + iovlen += 1; + } + off += 1; + } + if (data[data.len - 1].len > 0) { + off = 0; + const buf = data[data.len - 1]; + while (iovlen < iovec.len and off < splat) { + iovec[iovlen] = .{ .base = buf.ptr, .len = buf.len }; + iovlen += 1; + off += 1; + } + } + + if (iovlen == 0) return 0; + + const n: usize = while (true) { + const res = std.posix.system.writev(self.fd, &iovec, @intCast(iovlen)); + if (res < 0) { + if (std.posix.errno(res) == .INTR) continue; + return error.WriteFailed; + } + break @intCast(res); + }; + return w.consume(n); + } }; pub fn fatal(comptime format: []const u8, args: anytype) noreturn { @@ -570,21 +610,32 @@ pub fn print_help(comptime Command: type) noreturn { } pub fn ask(comptime prompt: []const u8, args: anytype, buf: []u8) ?[]const u8 { - var stdin = std.fs.File.stdin().reader(buf); + assert(buf.len > 0); + + const stdin: std.posix.fd_t = std.posix.STDIN_FILENO; + var off: usize = 0; while (true) { stderr.printf(prompt, args, .{}); stderr.flush(); - const line = stdin.interface.takeDelimiterExclusive('\n') catch |err| switch (err) { - error.StreamTooLong => { - stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{}); - continue; - }, - else => unreachable, - }; - const input = std.mem.trim(u8, line, " \t\r"); - return if (input.len == 0) null else input; + const res = std.posix.system.read(stdin, buf.ptr + off, buf.len - off); + assert(res >= 0); + if (res == 0) break; + + const n: usize = @intCast(res); + if (std.mem.findScalar(u8, buf[off .. off + n], '\n')) |idx| { + off += idx; + break; + } + off += n; + if (off >= buf.len) { + stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{}); + off = 0; + } } + + const input = std.mem.trim(u8, buf[0..off], " \t\r"); + return if (input.len == 0) null else input; } pub fn ask_confirmation(comptime prompt: []const u8, args: anytype, comptime default_answer: enum { yes, no }) bool { @@ -612,7 +663,7 @@ const Pager = struct { text: []const u8, dumb: bool, - pub fn format(self: Bold, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(self: Bold, w: *std.Io.Writer) std.Io.Writer.Error!void { if (!self.dumb) try w.writeAll("\x1b[1m"); try w.writeAll(self.text); if (!self.dumb) try w.writeAll("\x1b[m"); @@ -624,56 +675,85 @@ const Pager = struct { dumb: bool, fn init(out_buf: []u8) Pager { - if (!stdout.writer.file.isTty()) { - return .{ .pid = null, .printer = stdout_printer, .dumb = true }; - } + if (std.posix.system.isatty(stdout_printer.fd) != 1) return .init_dumb(); - const fds = std.posix.pipe() catch return .{ .pid = null, .printer = stdout_printer, .dumb = true }; - const fdin = fds[0]; - const fdout = fds[1]; + var stdin_pipe: [2]std.posix.fd_t = undefined; + if (std.posix.system.pipe(&stdin_pipe) != 0) return .init_dumb(); - const pid = std.posix.fork() catch { - std.posix.close(fdin); - std.posix.close(fdout); - return .{ .pid = null, .printer = stdout_printer, .dumb = true }; - }; + const pid = std.posix.system.fork(); + switch (pid) { + -1 => { + _ = std.posix.system.close(stdin_pipe[0]); + _ = std.posix.system.close(stdin_pipe[1]); + return .init_dumb(); + }, + 0 => { + // pager process + if (std.posix.system.dup2(stdin_pipe[0], std.posix.STDIN_FILENO) == -1) unreachable; + _ = std.posix.system.close(stdin_pipe[0]); + _ = std.posix.system.close(stdin_pipe[1]); - if (pid == 0) { - // pager process - std.posix.close(fdout); - std.posix.dup2(fdin, std.posix.STDIN_FILENO) catch unreachable; + var env: [512:null]?[*:0]const u8 = undefined; + var envlen: usize = 0; + inline for (&[_][*:0]const u8{ + "LESSCHARSET=utf-8", + }) |predefined_env| { + env[envlen] = predefined_env; + envlen += 1; + } - var env: [512:null]?[*:0]const u8 = undefined; - var envlen: usize = 0; - const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511]; - for (environ) |e| { - env[envlen] = e; - envlen += 1; - } - env[envlen] = null; + while (std.posix.system.environ[envlen]) |e| { + env[envlen] = e; + envlen += 1; + if (envlen >= env.len - 1) break; + } + env[envlen] = null; - // TODO: Allow environment PAGER to define custom pagers. - const pager_argv: [*:null]const ?[*:0]const u8 = &.{ "less", "-R", null }; + // TODO: Allow environment PAGER to define custom pagers. + const Pager_Prog = struct { + path: [:0]const u8, + argv: [*:null]const ?[*:0]const u8, + }; - std.posix.execvpeZ(pager_argv[0].?, pager_argv, &env) catch unreachable; - std.process.exit(1); - } else { - // application process - std.posix.close(fdin); - var out: std.fs.File = .{ .handle = fdout }; - return .{ - .pid = pid, - .printer = Printer.init(out.writer(out_buf)), - .dumb = if (std.posix.getenv("TERM")) |term| std.mem.eql(u8, term, "dumb") else true, - }; + for ([_]Pager_Prog{ + .{ .path = "/usr/bin/less", .argv = &.{ "less", "-R", null } }, + .{ .path = "/bin/cat", .argv = &.{ "cat", null } }, + }) |pager| { + const res = std.posix.system.execve(pager.path, pager.argv, &env); + switch (std.posix.errno(res)) { + .NOENT => continue, + else => |err| fatal("error executing pager: {}", .{err}), + } + } + fatal("no pager found", .{}); + }, + else => { + // application process + _ = std.posix.system.close(stdin_pipe[0]); + var dumb = true; + if (std.posix.system.getenv("TERM")) |term| { + dumb = term[0] == 'd' and term[1] == 'u' and term[2] == 'm' and term[3] == 'b'; + } + + return .{ + .pid = pid, + .printer = .init(stdin_pipe[1], out_buf), + .dumb = dumb, + }; + }, } } + fn init_dumb() Pager { + return .{ .pid = null, .printer = stdout_printer, .dumb = true }; + } + fn deinit(self: *Pager) noreturn { self.printer.flush(); if (self.pid) |pid| { - self.printer.writer.file.close(); - _ = std.posix.waitpid(pid, 0); + var status: c_int = undefined; + _ = std.posix.system.close(self.printer.fd); + _ = std.posix.system.waitpid(pid, &status, 0); } std.process.exit(0); } @@ -808,7 +888,7 @@ const Command_Info = struct { }; } - fn from_subcommand(comptime Command: type) []Command_Info { + fn from_subcommand(comptime Command: type) []const Command_Info { if (!@hasDecl(Command, "Subcommands")) { return &.{}; } @@ -819,12 +899,15 @@ const Command_Info = struct { }; assert(union_info.tag_type != null); - var subcmds: [union_info.fields.len]Command_Info = undefined; - for (union_info.fields, 0..) |field, i| { - subcmds[i] = Command_Info.from_command(field.type); - subcmds[i].tag = field.name; - } - return &subcmds; + const infos = comptime sub: { + var subcmds: [union_info.fields.len]Command_Info = undefined; + for (union_info.fields, 0..) |field, i| { + subcmds[i] = Command_Info.from_command(field.type); + subcmds[i].tag = field.name; + } + break :sub subcmds; + }; + return &infos; } /// Flushes the printer @@ -863,7 +946,7 @@ const Command_Info = struct { p.put('\n'); // subcommands - const subcmds = Command_Info.from_subcommand(cmd.type); + const subcmds = comptime Command_Info.from_subcommand(cmd.type); if (subcmds.len > 0) { const name_col = comptime Usage_Table.column(Command_Info, subcmds, struct { fn extract(comptime subcmd: Command_Info) []const u8 { @@ -886,7 +969,7 @@ const Command_Info = struct { if (cmd.options.len > 0) { const title_col = comptime Usage_Table.column(Option_Info, cmd.options, struct { fn extract(comptime opt: Option_Info) []const u8 { - const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">"; + const argname = if (opt.arg_name.len == 0) "" else " ⟨" ++ opt.arg_name ++ "⟩"; return if (opt.short_name) |short| std.fmt.comptimePrint("-{c}, --{s}{s}", .{ short, opt.long_name, argname }) else @@ -940,7 +1023,7 @@ const Command_Info = struct { if (cmd.options.len > 0) { p.printf_line("{f}", .{pager.bold("OPTIONS")}, .{ .margin = margin0 }); inline for (cmd.options) |opt| { - const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">"; + const argname = if (opt.arg_name.len == 0) "" else " ⟨" ++ opt.arg_name ++ "⟩"; if (opt.short_name) |short| { p.printf_line("-{f}, --{f}{s}", .{ pager.bold(&[1]u8{short}), pager.bold(opt.long_name), argname }, .{ .margin = margin1 }); } else { @@ -952,7 +1035,7 @@ const Command_Info = struct { } // subcommands - const subcmds = Command_Info.from_subcommand(cmd.type); + const subcmds = comptime Command_Info.from_subcommand(cmd.type); if (subcmds.len > 0) { p.printf_line("{f}", .{pager.bold("COMMANDS")}, .{ .margin = margin0 }); inline for (subcmds) |subcmd| { @@ -1065,16 +1148,6 @@ const Option_Info = struct { }; return &options; } - - fn contains_whitespace(comptime text: []const u8) bool { - for (text) |c| { - switch (c) { - ' ', '\t', '\r', '\n' => return true, - else => {}, - } - } - return false; - } }; fn sanitize_name(comptime name: []const u8) []const u8 { @@ -1138,6 +1211,16 @@ fn contains_eol(comptime text: []const u8) bool { return false; } +fn contains_whitespace(comptime text: []const u8) bool { + for (text) |c| { + switch (c) { + ' ', '\t', '\r', '\n' => return true, + else => {}, + } + } + return false; +} + fn is_uppercase(comptime text: []const u8) bool { for (text) |c| { if ('a' <= c and c <= 'z') return false; @@ -1151,7 +1234,7 @@ fn join_enum_values(comptime E: type) []const u8 { 0 => unreachable, 1 => "'" ++ fields[0].name ++ "'", 2 => "'" ++ fields[0].name ++ "' or '" ++ fields[1].name ++ "'", - 3 => str: { + else => str: { var text: []const u8 = ""; for (fields[0 .. fields.len - 1]) |field| { text = text ++ "'" ++ field.name ++ "', "; |