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.

1 Like

Finally, I made it.

Initializeing list as an array with length of optstring.len to undefined, update the item each time iterating the optstring (forward if a new character is met, backward if ':' character is met) as well as the end position list_i, so that the list is a pointer to an array of T return list[0..list_i]. More importantly, warp the function body into comptime block and add comptime keyword at callsite otherwise it will emit error

list_from_optstring.zig:59:13: error: function called at runtime cannot return value at comptime
            return list[0..list_i];
            ^~~~~~~~~~~~~~~~~~~~~~

I tried lots of ways but finally this works for me currently. Thanks vulpesx!

Guessing since you didn’t share the code, but:

you don’t need to wrap the function body in a comptime block, just need list_i to be a comptime var and the loop to be inline
this will also allow you to call it at runtime (ofc optstring still has to be comptime)

1 Like

Yeah it works. And the code using a nightly 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 listFromOptstring(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;

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

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

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

        return list[0..list_i];
    }
};

test "list basic" {
    const Tag = Option.Tag;
    {
        const opts = "a:bc::d";
        const list = try comptime Option.listFromOptstring(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");
    }
}

Well both list or list_i be a comptime variable works :slight_smile: