Open8

zigでlinuxコマンドを実装してみる

ピン留めされたアイテム
RiiiMRiiiM

注意

⚠️ 本記事のコードや内容は、筆者の学習・調査メモとしてまとめたものであり、正確性や安全性を保証するものではありません。
誤りや古い情報、個人的な解釈が含まれる可能性があります。
また、執筆時点でZigのバージョンは0.15.0であるため、今後の破壊的変更によりコードが古くなる可能性があります。

RiiiMRiiiM

ls

ls #パス指定未実装
const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    const cwd = std.fs.cwd();
    var dir = try cwd.openDir(".", .{ .iterate = true });
    defer dir.close();
    var it = dir.iterate();
    while (try it.next()) |entry| {
        try stdout.print("{s}\n", .{entry.name});
    }
}

コメント

安全で高レベルな std.fs からでなく、libcやgetdents呼び出しにすると勉強になりそう

RiiiMRiiiM

cat

cat <file path> <file path>
const std = @import("std");
pub fn main() !void {
    const allocator = std.heap.page_allocator;
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);
    if (args.len < 2) {
        std.debug.print("Usage: cat <file>\n", .{});
        return;
    }
    var stdout = std.io.getStdOut().writer();
    for (args[1..]) |filename| {
        const file = std.fs.cwd().openFile(filename, .{ .mode = .read_only, .lock = .none }) catch |err| {
            std.debug.print("Error opening file '{s}': {s}\n", .{ filename, @errorName(err) });
            continue;
        };
        defer file.close();

        const fileReader = file.reader();
        var bufferReader = std.io.bufferedReader(fileReader);
        const reader = bufferReader.reader();

        var line_buller: [1024]u8 = undefined;
        while (try reader.readUntilDelimiterOrEof(line_buller[0..], '\n')) |line| {
            try stdout.print("{s}\n", .{line});
        }
    }
}

コメント

line readerにしているが、バッファが1024なのでオーバーフロー対策必要

var line_buller: [1024]u8 = undefined;
RiiiMRiiiM

wc

wc -lwc <file path>
wc -c -
const std = @import("std");

const Allocator = std.mem.Allocator;

const Counts = struct {
    lines: usize,
    words: usize,
    bytes: usize,

    pub fn add(self: *Counts, other: Counts) void {
        self.lines += other.lines;
        self.words += other.words;
        self.bytes += other.bytes;
    }
};

const Flags = struct {
    show_lines: bool = false,
    show_words: bool = false,
    show_bytes: bool = false,
};

fn parseFlagsAndPaths(allocator: std.mem.Allocator, args_it: *std.process.ArgIterator) !struct { flags: Flags, paths: std.ArrayList([]const u8) } {
    var flags = Flags{};
    var paths = std.ArrayList([]const u8).init(allocator);

    _ = args_it.next(); // Skip the program name
    var arg_opt: ?[]const u8 = args_it.next();
    while (arg_opt) |arg| {
        if (arg.len > 0 and arg[0] == '-') {
            if (arg.len == 1) {
                try paths.append(arg); // single '-' → stdin
            } else {
                var i: usize = 1;
                while (i < arg.len) : (i += 1) {
                    switch (arg[i]) {
                        'l' => flags.show_lines = true,
                        'w' => flags.show_words = true,
                        'c' => flags.show_bytes = true,
                        else => {
                            try std.io.getStdErr().writer().print("wc: invalid option '{c}'\n", .{arg[i]});
                            return error.InvalidOption;
                        },
                    }
                }
            }
        } else {
            try paths.append(arg);
        }
        arg_opt = args_it.next();
    }

    if (!flags.show_lines and !flags.show_words and !flags.show_bytes) {
        flags = .{ .show_lines = true, .show_words = true, .show_bytes = true };
    }

    return .{ .flags = flags, .paths = paths };
}

fn read(path: []const u8) !Counts {
    if (path.len == 1 and path[0] == '-') {
        return try processReader(std.io.getStdIn().reader());
    } else {
        const file = std.fs.cwd().openFile(path, .{}) catch |err| {
            try std.io.getStdErr().writer().print("wc: cannot open {s}: {s}\n", .{ path, @errorName(err) });
            return Counts{ .lines = 0, .words = 0, .bytes = 0 };
        };
        defer file.close();
        return try processReader(file.reader());
    }
}

fn processReader(reader: anytype) !Counts {
    var counts = Counts{ .lines = 0, .words = 0, .bytes = 0 };
    var line_buffer: [1024]u8 = undefined;

    while (true) {
        const bytes_read = reader.readUntilDelimiterOrEof(&line_buffer, '\n') catch |err| switch (err) {
            error.StreamTooLong => {
                counts.bytes += line_buffer.len;
                var in_word = false;
                for (line_buffer) |c| {
                    if (std.ascii.isWhitespace(c)) {
                        if (in_word) {
                            counts.words += 1;
                            in_word = false;
                        }
                    } else {
                        in_word = true;
                    }
                }
                if (in_word) counts.words += 1;
                continue;
            },
            else => return err,
        };

        if (bytes_read == null) break; // EOF
        const bytes = bytes_read.?;

        counts.bytes += bytes.len;
        counts.lines += 1;

        var in_word = false;
        for (bytes) |c| {
            if (std.ascii.isWhitespace(c)) {
                if (in_word) {
                    counts.words += 1;
                    in_word = false;
                }
            } else {
                in_word = true;
            }
        }
        if (in_word) counts.words += 1; // last word in the line
    }

    return counts;
}

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    var args_it = std.process.args();
    defer args_it.deinit();

    const parsed = try parseFlagsAndPaths(allocator, &args_it);
    if (parsed.paths.items.len == 0) {
        try std.io.getStdErr().writer().print("wc: no input files specified\n", .{});
        return error.NoInputFiles;
    }
    defer parsed.paths.deinit();

    var total_counts = Counts{ .lines = 0, .words = 0, .bytes = 0 };

    for (parsed.paths.items) |path| {
        const counts = try read(path);
        total_counts.add(counts);
        if (parsed.flags.show_lines) {
            try std.io.getStdOut().writer().print("{d} ", .{counts.lines});
        }
        if (parsed.flags.show_words) {
            try std.io.getStdOut().writer().print("{d} ", .{counts.words});
        }
        if (parsed.flags.show_bytes) {
            try std.io.getStdOut().writer().print("{d} ", .{counts.bytes});
        }
        try std.io.getStdOut().writer().print("{s}\n", .{path});
    }

    if (parsed.paths.items.len > 1) {
        if (parsed.flags.show_lines) {
            try std.io.getStdOut().writer().print("{d} ", .{total_counts.lines});
        }
        if (parsed.flags.show_words) {
            try std.io.getStdOut().writer().print("{d} ", .{total_counts.words});
        }
        if (parsed.flags.show_bytes) {
            try std.io.getStdOut().writer().print("{d} ", .{total_counts.bytes});
        }
        try std.io.getStdOut().writer().print("total\n", .{});
    }
}

コメント

書いてみたら思ったより長くなった。
入力の読み取りに苦戦。Allocatorについてもう少し勉強必要
今の書き方は冗長かも

RiiiMRiiiM

pwd

pwd
const std = @import("std");
const c = std.c;
pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    const stderr = std.io.getStdErr().writer();

    var cwd_buffer: [1024]u8 = undefined;

    if (c.getcwd(&cwd_buffer, cwd_buffer.len) == null) {
        const c_err_int = c._errno().*;
        const err_enum = std.posix.errno(c_err_int);

        try stderr.print("Error getting current working directory: [{d}] {s}\n", .{ c_err_int, @tagName(err_enum) });
        std.process.exit(1);
    }
    const len = std.mem.indexOfScalar(u8, &cwd_buffer, 0).?;
    try stdout.print("{s}\n", .{cwd_buffer[0..len]});
}

コメント

CやPOSIX APIのラッパーであるstd.cを使ってみた
darwin aarch64でのエラー番号 c_intstd.posix.errno で正しくenum変換されなかったので、ヘッダファイルを読ませる必要がありそう
c._errno() でCライブラリ側のスレッドローカル errno へのポインタが取得できる。同スレッドでstd.cの関数実行のたびにerrnoが書き換えられる

RiiiMRiiiM

ビルドの変更

ここで、プロジェクト内でsrc/の単一ファイル(例: ls.zig)を外部ライブラリ込みでビルドできるようにしたくなってきた

build.zig を修正

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // コマンドライン引数からソースファイルパスを取得
    const source_file = b.option([]const u8, "source", "Source file path (.zig)") orelse {
        std.log.err("Usage: zig build -Dsource=<path/to/file.zig>", .{});
        std.process.exit(1);
    };

    // ソースファイルのパスから、ファイル名(拡張子なし)を抽出
    const basename = std.fs.path.basename(source_file);
    if (!std.mem.endsWith(u8, basename, ".zig")) {
        std.log.err("Error: Source file must have .zig extension. Got: {s}", .{basename});
        std.process.exit(1);
    }
    const exe_name = basename[0 .. basename.len - 4];
    const exe = b.addExecutable(.{
        .name = exe_name,
        .root_source_file = b.path(source_file),
        .target = target,
        .optimize = optimize,
    });

    // zig fetch 済みの外部ライブラリを追加
    // const ardParser = b.dependency("args", .{
    //     .target = target,
    //     .optimize = optimize,
    // });
    // exe.root_module.addImport("args", ardParser.module("args"));

    b.installArtifact(exe);
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

# build
zig build -Dsource=<target_source_file>
# build & run
zig build run -Dsource=<target_source_file>
RiiiMRiiiM

echo

echo 'test me'
echo -n 'test me'
const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    var args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);
    const is_no_newline_option = parseOption(args[1..]);
    const content_args = if (is_no_newline_option) args[2..] else args[1..];

    const stdout = std.io.getStdOut().writer();
    const result = try joinargs(allocator, content_args);
    defer allocator.free(result);
    if (is_no_newline_option) {
        try stdout.writeAll(result);
    } else {
        try stdout.print("{s}\n", .{result});
    }
}

fn parseOption(args: []const []const u8) bool {
    if (args.len == 0) return false;
    const n_option: []const u8 = "-n";
    return std.mem.eql(u8, args[0], n_option);
}

pub fn joinargs(allocator: std.mem.Allocator, args: []const []const u8) ![]u8 {
    var buffer = std.ArrayList(u8).init(allocator);
    // allocator内のスライスを返すのでここではdeinitしない。deinitしても実行は問題なかった
    for (args) |arg| {
        if (buffer.items.len > 0) {
            try buffer.append(' ');
        }
        try buffer.appendSlice(arg);
    }
    return buffer.toOwnedSlice();
}

コメント

allocatorにまだ慣れない
引数にallocatorをとり、その中の値を返すときはdeferタイミング注意

RiiiMRiiiM

mv (renameのみ)

mv_rename test.txt test1.txt
const std = @import("std");
const Dir = std.fs.Dir;

const Error = error{
    InvalidArguments,
    RenameFailed,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len != 3) {
        try std.io.getStdErr().writer().print(
            "Usage: {s} <source> <destination>\n",
            .{args[0]},
        );
        return Error.InvalidArguments;
    }

    const src = args[1];
    const dst = args[2];

    const cwd: Dir = std.fs.cwd();
    // renameのため以下は失敗する
    // defer cwd.close();
    try std.fs.Dir.rename(cwd, src, dst);
}

コメント

std.fs.Dir.renameがあったのでそれを使用
cwd内で行っているため同ディレクトリのみ有効
std.fs.renameだと異ディレクトリ間でrename可能
エラーのenumがシンプルで扱いやすい。
なぜエラーをenumで事前に定義するか解説されている記事があった。これ見て納得👇
https://zenn.dev/funatsufumiya/articles/26850c2b827b25

defer cwd.close()はrename後のファイルパスが変わっているため失敗するので今回はつけていないが、
これがないと違和感もある。
inodeは名前変更で不変なため、これを参照して.close()する実装にできないのかと思ったが、close(2)はファイルディスクリプタを引数に受け取るのでそれはできない