zlob 1.3.3-dev.4

SIMD optimized glob pattern matching library faster than glob crate
Documentation
const std = @import("std");
const builtin = @import("builtin");
const zlob = @import("zlob");
const build_options = @import("build_options");
const Io = std.Io;
const File = std.Io.File;

const version = build_options.version;

const Options = struct {
    pattern: ?[]const u8 = null,
    path: ?[]const u8 = null,
    show_all: bool = false,
    mark_dirs: bool = false,
    sorted: bool = false,
    no_escape: bool = false,
    no_brace: bool = false,
    no_gitignore: bool = false,
    hidden: bool = false,
    dirs_only: bool = false,
    count_only: bool = false,
    show_help: bool = false,
    show_version: bool = false,
    limit: usize = 100,
};

const BufferedWriter = struct {
    file_writer: File.Writer,

    fn init(io: Io, file: File, buf: []u8) BufferedWriter {
        return .{ .file_writer = file.writer(io, buf) };
    }

    fn writer(self: *BufferedWriter) *Io.Writer {
        return &self.file_writer.interface;
    }

    fn flush(self: *BufferedWriter) void {
        self.file_writer.interface.flush() catch {};
    }
};

fn printVersion(w: *Io.Writer) void {
    w.print("zlob {s}\n", .{version}) catch {};
}

fn printHelp(w: *Io.Writer, program_name: []const u8) void {
    w.print(
        \\{s} - A fast SIMD-accelerated glob pattern matcher
        \\
        \\USAGE:
        \\    {s} [OPTIONS] <PATTERN> [PATH]
        \\
        \\ARGS:
        \\    <PATTERN>    Glob pattern to match (e.g., '**/*.zig', 'src/*.c')
        \\    [PATH]       Directory to search (default: current directory)
        \\
        \\OPTIONS:
        \\    -a, --all            Show all results (default: first 100)
        \\    -n, --limit <NUM>    Limit results to NUM entries (default: 100)
        \\    -H, --hidden         Include hidden files (match files starting with '.')
        \\    -c, --count          Print only the count of matched files
        \\    -d, --dirs-only      Only match directories
        \\    -m, --mark           Append '/' to directory names
        \\    -E, --no-escape      Treat backslash as literal character
        \\    -s, --sorted         Sort results alphabetically (default: unsorted)
        \\    -B, --no-brace       Disable brace expansion (default: enabled)
        \\    -G, --no-gitignore   Don't respect .gitignore rules (default: enabled)
        \\    -h, --help           Print help information
        \\    -V, --version        Print version information
        \\
        \\PATTERN SYNTAX:
        \\    *            Match any sequence of characters (except '/')
        \\    **           Match any sequence including '/' (recursive)
        \\    ?            Match any single character
        \\    [abc]        Match any character in the set
        \\    [a-z]        Match any character in the range
        \\    [!abc]       Match any character NOT in the set
        \\    {{a,b,c}}      Match any of the comma-separated patterns
        \\
        \\EXAMPLES:
        \\    {s} '**/*.zig'              Find all .zig files recursively
        \\    {s} '*.txt' /path/to/dir    Find .txt files in specified directory
        \\    {s} -G '**/*.ts'            Find .ts files, ignoring .gitignore
        \\    {s} -a '**/*'               List all files (no limit)
        \\    {s} -n 50 '**/*.c'          Show first 50 .c files
        \\    {s} 'src/*.{{c,h}}'           Find .c and .h files in src/
        \\
    , .{ program_name, program_name, program_name, program_name, program_name, program_name, program_name, program_name }) catch {};
}

fn parseArgs(args_iter: *std.process.Args.Iterator, stderr: *Io.Writer) !Options {
    var opts = Options{};
    _ = args_iter.next(); // program name

    while (args_iter.next()) |arg| {
        if (std.mem.startsWith(u8, arg, "-")) {
            if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
                opts.show_help = true;
            } else if (std.mem.eql(u8, arg, "-V") or std.mem.eql(u8, arg, "--version")) {
                opts.show_version = true;
            } else if (std.mem.eql(u8, arg, "-a") or std.mem.eql(u8, arg, "--all")) {
                opts.show_all = true;
            } else if (std.mem.eql(u8, arg, "-n") or std.mem.eql(u8, arg, "--limit")) {
                const limit_str = args_iter.next() orelse {
                    return error.MissingArgument;
                };
                opts.limit = std.fmt.parseInt(usize, limit_str, 10) catch {
                    return error.InvalidNumber;
                };
            } else if (std.mem.eql(u8, arg, "-m") or std.mem.eql(u8, arg, "--mark")) {
                opts.mark_dirs = true;
            } else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--sorted")) {
                opts.sorted = true;
            } else if (std.mem.eql(u8, arg, "-E") or std.mem.eql(u8, arg, "--no-escape")) {
                opts.no_escape = true;
            } else if (std.mem.eql(u8, arg, "-B") or std.mem.eql(u8, arg, "--no-brace")) {
                opts.no_brace = true;
            } else if (std.mem.eql(u8, arg, "-G") or std.mem.eql(u8, arg, "--no-gitignore")) {
                opts.no_gitignore = true;
            } else if (std.mem.eql(u8, arg, "-H") or std.mem.eql(u8, arg, "--hidden")) {
                opts.hidden = true;
            } else if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--count")) {
                opts.count_only = true;
            } else if (std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--dirs-only")) {
                opts.dirs_only = true;
            } else {
                stderr.print("error: unknown option '{s}'\n", .{arg}) catch {};
                stderr.print("For more information, try '--help'\n", .{}) catch {};
                stderr.flush() catch {};
                std.process.exit(1);
            }
        } else {
            // Positional argument
            if (opts.pattern == null) {
                opts.pattern = arg;
            } else if (opts.path == null) {
                opts.path = arg;
            } else {
                stderr.print("error: unexpected argument '{s}'\n", .{arg}) catch {};
                stderr.print("For more information, try '--help'\n", .{}) catch {};
                stderr.flush() catch {};
                std.process.exit(1);
            }
        }
    }

    return opts;
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const allocator = init.arena.allocator();

    // Buffered writers for stdout and stderr
    var stdout_buf: [8192]u8 = undefined;
    var stderr_buf: [4096]u8 = undefined;
    var stdout_writer = BufferedWriter.init(io, File.stdout(), &stdout_buf);
    var stderr_writer = BufferedWriter.init(io, File.stderr(), &stderr_buf);
    const stdout = stdout_writer.writer();
    const stderr = stderr_writer.writer();

    var args_iter = try std.process.Args.Iterator.initAllocator(init.minimal.args, init.gpa);
    defer args_iter.deinit();

    const opts = parseArgs(&args_iter, stderr) catch |err| {
        stderr.print("error: {}\n", .{err}) catch {};
        stderr_writer.flush();
        std.process.exit(1);
    };

    if (opts.show_version) {
        printVersion(stdout);
        stdout_writer.flush();
        return;
    }

    if (opts.show_help) {
        printHelp(stdout, "zlob");
        stdout_writer.flush();
        return;
    }

    const pattern = opts.pattern orelse {
        printHelp(stderr, "zlob");
        stderr_writer.flush();
        std.process.exit(1);
    };

    var full_pattern: []const u8 = undefined;
    if (opts.path) |path| {
        // Combine path and pattern: trim trailing separators (/ on all platforms, \ on Windows)
        const sep_chars = if (builtin.os.tag == .windows) "/\\" else "/";
        const trimmed_path = std.mem.trimEnd(u8, path, sep_chars);
        full_pattern = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ trimmed_path, pattern });
    } else {
        full_pattern = pattern;
    }

    // Start with recommended settings, then apply CLI options
    var flags = zlob.ZlobFlags.recommended();
    flags.nosort = !opts.sorted;
    flags.brace = !opts.no_brace;
    flags.gitignore = !opts.no_gitignore;
    flags.mark = opts.mark_dirs;
    flags.noescape = opts.no_escape;
    flags.period = opts.hidden;
    flags.onlydir = opts.dirs_only;
    flags.nomagic = false; // always enable magic, since we require a pattern argument
    _ = &flags; // suppress unused warning if match accepts anytype

    var match_result = zlob.match(allocator, io, full_pattern, flags) catch |err| {
        stderr.print("error: glob failed: {}\n", .{err}) catch {};
        stderr_writer.flush();
        std.process.exit(1);
    };

    if (match_result) |*result| {
        defer result.deinit();

        const total = result.len();

        if (opts.count_only) {
            stdout.print("{d}\n", .{total}) catch {};
            stdout_writer.flush();
        } else {
            const display_limit = if (opts.show_all) total else @min(opts.limit, total);

            var displayed: usize = 0;
            var it = result.iterator();
            while (it.next()) |path| {
                if (displayed >= display_limit) break;
                stdout.print("{s}\n", .{path}) catch {};
                displayed += 1;
            }

            stdout_writer.flush();

            if (!opts.show_all and total > opts.limit) {
                stderr.print("\n... and {d} more ({d} total). Use -a to show all.\n", .{ total - opts.limit, total }) catch {};
                stderr_writer.flush();
            }
        }
    } else {
        stderr.print("No matches found for pattern: {s}\n", .{full_pattern}) catch {};
        stderr_writer.flush();
        std.process.exit(1);
    }
}