const std = @import("std"); fn usage(file: std.Io.File, io: std.Io) !void { var buf: [1024]u8 = undefined; var w = file.writer(io, &buf); try w.interface.writeAll( \\usage: have [NAME...] \\ \\List executable command names found in PATH, one per line. \\With NAME args, print the names that are available and exit 1 if any are missing. \\This inspects PATH only; it does not know shell builtins, aliases, functions, or packages. \\ \\stdout: \\ one command name per line \\ \\examples: \\ have | grep '^zig$' \\ have zig git curl \\ ); try w.interface.flush(); } fn die(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { var buf: [512]u8 = undefined; var w = std.Io.File.stderr().writer(io, &buf); w.interface.print(fmt, args) catch {}; w.interface.flush() catch {}; std.process.exit(1); } fn pathEnv() []const u8 { const raw = std.c.getenv("PATH") orelse return ""; return std.mem.span(raw); } fn commandName(name: []const u8) bool { return name.len != 0 and std.mem.indexOfScalar(u8, name, '/') == null; } fn executableFileInDir(dir: std.Io.Dir, io: std.Io, name: []const u8) bool { const stat = dir.statFile(io, name, .{}) catch return false; if (stat.kind != .file) return false; dir.access(io, name, .{ .execute = true }) catch return false; return true; } fn haveOne(io: std.Io, name: []const u8) bool { if (!commandName(name)) return false; var dirs = std.mem.splitScalar(u8, pathEnv(), ':'); while (dirs.next()) |dir_path| { const actual_dir = if (dir_path.len == 0) "." else dir_path; var dir = std.Io.Dir.cwd().openDir(io, actual_dir, .{}) catch continue; defer dir.close(io); if (executableFileInDir(dir, io, name)) return true; } return false; } fn listDir(io: std.Io, writer: *std.Io.Writer, seen: *std.StringHashMap(void), dir_path: []const u8) !void { var dir = std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }) catch return; defer dir.close(io); var it = dir.iterate(); while (try it.next(io)) |entry| { if (!commandName(entry.name)) continue; if (seen.contains(entry.name)) continue; if (!executableFileInDir(dir, io, entry.name)) continue; const owned = try seen.allocator.dupe(u8, entry.name); errdefer seen.allocator.free(owned); try seen.put(owned, {}); try writer.print("{s}\n", .{entry.name}); } } fn listAll(allocator: std.mem.Allocator, io: std.Io, writer: *std.Io.Writer) !void { var seen = std.StringHashMap(void).init(allocator); defer { var keys = seen.keyIterator(); while (keys.next()) |key| allocator.free(key.*); seen.deinit(); } var dirs = std.mem.splitScalar(u8, pathEnv(), ':'); while (dirs.next()) |dir_path| { const actual_dir = if (dir_path.len == 0) "." else dir_path; try listDir(io, writer, &seen, actual_dir); } } pub fn main(init: std.process.Init) !void { const allocator = init.gpa; var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator); defer args.deinit(); _ = args.skip(); var out_buf: [8192]u8 = undefined; var out = std.Io.File.stdout().writer(init.io, &out_buf); var names = std.ArrayList([]const u8).empty; defer names.deinit(allocator); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { try usage(std.Io.File.stdout(), init.io); return; } try names.append(allocator, arg); } if (names.items.len == 0) { listAll(allocator, init.io, &out.interface) catch |err| die(init.io, "have: {s}\n", .{@errorName(err)}); try out.interface.flush(); return; } var missing = false; for (names.items) |name| { if (haveOne(init.io, name)) { try out.interface.print("{s}\n", .{name}); } else { missing = true; } } try out.interface.flush(); if (missing) std.process.exit(1); } test commandName { try std.testing.expect(commandName("zig")); try std.testing.expect(commandName(".")); try std.testing.expect(!commandName("")); try std.testing.expect(!commandName("./zig")); try std.testing.expect(!commandName("bin/zig")); }