This repository has been archived on 2026-07-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
have/have.zig
T
2026-07-04 20:44:28 +02:00

137 lines
4.3 KiB
Zig

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"));
}