Segmentation Fault Parsing Command Line Arguments

Hi, I’ve been reading Zig compiler code for a long in hope of writing idiomatic Zig code, thus I’m using a nightly Zig version (0.17.0-dev.1127+9b5851f00).

I start by writing some small utility programs like ls(1), cat(1), etc. (FreeBSD ones, because macOS ones are based on the former)

When implementing an ArgsIteratorWithOpt which is able to parse optstring for short options, I want use a std.StaticStringMap(Option.Tag).initComptime(list) to decide whether next argument is “option_argument” based on the last character if current argument is “option”.

Part of ArgsIteratorWithOpt (Full is at end):

    // ArgsIteratorWithOpt.kind()
    pub fn kind(self: *Self, arg: []const u8) Tag {
        if (self.index != 0) switch (self.tags.items[self.index - 1]) {
            .operand,
            .end_of_options,
            => return .operand,
            // To be implemented.
            // The `option_argument` is returned if `optmap.get(arg[arg.len]).@"1"` is `.req_arg`.
            .option => {},
        };

        if (arg.len >= 2 and mem.startsWith(u8, arg, "-") and !mem.eql(u8, arg[1], "-")) {
            return .option;
        } else if (arg.len >= 3 and mem.startsWith(u8, arg, "--")) {
            return .option_long;
        } else if (mem.eql(u8, arg, "--")) {
            return .end_of_options;
        } else {
            return .operand;
        }
    }

where ArgsIteratorWithOpt.Tag is an enum:

    // ArgsIteratorWithOpt.Tag
    const Tag = enum(u3) {
        option,
        option_argument,
        operand,
        end_of_options,
        // 
        option_long,
    };

The problem is at Option.buildList which should output a compile-known list to be consumed by StaticStringMap:

// test_list_from_optstring.zig
const std = @import("std");
const testing = std.testing;

const Option = struct {
    tag: Tag,

    const Tag = enum(u2) {
        /// no argument to the option is expected
        no_arg,
        /// an argument to the option is required
        req_arg,
        /// an argument to the option may be presented
        opt_arg,
    };

    const Error = error{
        FindNone,
        DuplicateOption,
        /// Historically, a leading ':' in optstring for getopt(3) function is
        /// used to distinguish runtime-known error "unknown option" and  "missing option argument"
        /// Thus it is not necessary here.
        LeadingColon,
        TooManyColons,
    };

    // E.g, Validate and parse "a:bc::d" to a list
    // `.{'a', .req_arg}, .{'b', .no_arg}, .{'c', .opt_arg}, .{'d', .no_arg}`.
    pub fn buildList(comptime opts: []const u8) Error![]struct { u8, Tag } {
        if (opts.len == 0) return error.FindNone;
        if (opts[0] == ':') return error.LeadingColon;

        var seen: std.bit_set.Integer(256) = .empty;

        var list: []struct { u8, Tag } = undefined;
        var i: usize = 0;
        var list_i: usize = 0;
        while (i < opts.len) : (i += 1) {
            const opt = opts[i];
            const tag: Tag = .no_arg;

            if (seen.isSet(opt)) return error.DuplicateOption;

            if (opt != ':') {
                seen.set(opt);
                list[list_i] = .{ opt, tag };
                list_i += 1;
            } else {
                if (opts[i - 1] != ':') {
                    list[list_i - 1].@"1" = .req_arg;
                } else {
                    if (i < 2 or opts[i - 2] == ':') return error.TooManyColons;
                    list[list_i - 1].@"1" = .opt_arg;
                }
            }
        }

        // It is going to be consumed by
        // `const optmap = std.StaticStringMap(Option.Tag).initComptime(list);` at caller.
        return list[0..list_i];
    }
};

test "list basic" {
    const Tag = Option.Tag;
    {
        const opts = "a:bc::d";
        const list = try Option.buildList(opts);
        try testing.expectEqual('a', list[0].@"0");
        try testing.expectEqual(Tag.req_arg, list[0].@"1");
        try testing.expectEqual('b', list[1].@"0");
        try testing.expectEqual(Tag.no_arg, list[1].@"1");
        try testing.expectEqual('c', list[2].@"0");
        try testing.expectEqual(Tag.opt_arg, list[2].@"1");
        try testing.expectEqual('d', list[3].@"0");
        try testing.expectEqual(Tag.no_arg, list[3].@"1");
    }
}

It failed the test and I did’t know why from error message.

Segmentation fault at address 0xaaaaaaaaaaaaaaaa
/Users/zeeq/Code/cmds/example/list_from_optstring.zig:45:21: 0x1029b3ad0 in buildList__anon_34228 (test)
                list[list_i] = .{ opt, tag };
                    ^
/Users/zeeq/Code/cmds/example/list_from_optstring.zig:67:42: 0x1029b311f in test.list basic (test)
        const list = try Option.buildList(opts);

Full ArgsIteratorWithOpt:

const std = @import("std");
const mem = std.mem;
const fatal = std.process.fatal;
const testing = std.testing;

const Allocator = mem.Allocator;
const ArrayList = std.ArrayList;
const StaticStringMap = std.StaticStringMap;

const ArgsIteratorWithOpt = struct {
    args: []const []const u8,
    index: usize,
    tags: ArrayList(Tag),
    optmap: []struct { u8, Option.Tag },
    gpa: Allocator,

    const Tag = enum(u3) {
        option,
        option_argument,
        operand,
        end_of_options,

        option_long,
    };
    const Self = @This();

    /// MUST use `deinit()`.
    pub fn init(args: []const []const u8, comptime opts: []const u8, gpa: Allocator) Option.Error!Self {
        const list = try Option.buildList(opts);
        const optmap = StaticStringMap(Option.Tag).initComptime(list);

        return Self{
            .args = args,
            .index = 0,
            .tags = .empty,
            .optmap = optmap,
            .gpa = gpa,
        };
    }

    pub fn next(self: *Self) ?struct { []const u8, Tag } {
        if (self.index >= self.args.len) return null;
        defer self.index += 1;
        const arg = self.args[self.index];
        const tag = self.kind(arg);
        try self.tags.append(self.gpa, tag);
        return .{ arg, tag };
    }

    pub fn kind(self: *Self, arg: []const u8) Tag {
        if (self.index != 0) switch (self.tags.items[self.index - 1]) {
            .operand,
            .end_of_options,
            => return .operand,
            // To be implemented.
            // The `option_argument` is returned if `optmap.get(arg[arg.len]).@"1"` is `.req_arg`.
            .option => {},
        };

        if (arg.len >= 2 and mem.startsWith(u8, arg, "-") and !mem.eql(u8, arg[1], "-")) {
            return .option;
        } else if (arg.len >= 3 and mem.startsWith(u8, arg, "--")) {
            return .option_long;
        } else if (mem.eql(u8, arg, "--")) {
            return .end_of_options;
        } else {
            return .operand;
        }
    }

    pub fn deinit(self: *Self) void {
        self.tags.deinit(self.gpa);
    }
};

The title should be about the actual problem

And the background is not helpful and maybe too detailed, but you are certainly welcome to include it, maybe put it after the issue description and labelled as such.


[]T is a pointer + len pair
undefined is setting the pointer and len themselves to hex aa.. (only in debug mode)

Instead, initialise list with an empty slice, and since you’re doing comptime you can list = list ++ &.{...} to append to it.
(will require some code changes to ensure things are comptime where they need to be)

alternatively you could calculate the length upfront and define list as list: [length]T = undefined

1 Like

Yeah you’re right, now I wrap body of buildList into a comptime block, initialize the list as var list: []struct { u8, Tag } = &.{} because I saw that’s how ArrayList.empty did, and made list = list ++ &.{.{opt, .no_arg }};, though there is an error I currently didn’t find how to solve it but I’ll check it tomorrow. Thanks vulpesx!

 example/list_from_optstring.zig:45:36: error: expected indexable; found '*const struct { comptime struct { comptime u8 = 97, comptime @EnumLiteral() = .no_arg } = .{ 97, .no_arg } }'
                    list = list ++ &.{.{ opt, .no_arg }};
                                   ^~~~~~~~~~~~~~~~~~~~~

ah make list: []const T, it’s not coercing because it expects a mutable pointer but pointers to temporaries/literals are always const.