New process.Args.Iterator.Posix doesn't allow "rewinding" anymore

The 0.15.2 implementation had an index field which allowed to rewind the iterator by decrementing it, while the new one seems to discard previous values as it goes.

Is there a “hard” reason for this change or could it be reverted? This might be something minor but it’s very dear to me.

ummmm was that portable? (i.e. did it work on Windows as well as POSIX?)

I have tested only on Linux (I’m targeting only POSIX). Is it a requirement for this to be portable? Couldn’t the Iterator.Posix returned by iterate be a “superset” and then have it’s functionality limited when using the more general Iterator if it’s not portable? That way a more featureful API is available in case you only care about POSIX.

It wasn’t exactly the same, but the same principle applied. Here’s the portable approach I’ve been using up to v0.15.2 in my CLI library. The key part being in the inline else :

    /// Peek at the next argument token without advancing this Iterator.
    pub fn peek(self: *@This()) ?[:0]const u8 {
        switch (self.*) {
            .raw => return self.raw.peek(),
            inline else => |*iter| {
                if (builtin.os.tag != .windows) {
                    const peek_arg = iter.next();
                    iter.inner.index -= 1;
                    return peek_arg;
                }
                else {
                    const iter_idx = iter.inner.index;
                    const iter_start = iter.inner.start;
                    const iter_end = iter.inner.end;
                    const peek_arg = iter.next();
                    iter.inner.index = iter_idx;
                    iter.inner.start = iter_start;
                    iter.inner.end = iter_end;
                    return peek_arg;
                }
            },
        }
    }

Mind, I haven’t taken a look at the new way it’s done in v0.16, but I assume this will break given what I’ve read recently (including this post).

Oops, no - the posix one of course does not need to be portable outside of posix.

Feel free to send a patch if you’re up for it.

Feel free to send a patch if you’re up for it.

I have never tried something like this before, so if anyone wants to help or discuss this I would appreciate.

I was thinking about just reverting to the previous implementation and maybe adding an explicit previous and remaining functions if that’s alright.

1 Like

This is what I came up with. Suggestions and sanity checks are appreciated.

--- Args.zig.master	2026-02-06 16:32:54.045685708 -0300
+++ Args.zig	2026-02-06 18:02:45.823981053 -0300
@@ -34,10 +34,10 @@
 
     /// Initialize the args iterator. Consider using `initAllocator` instead
     /// for cross-platform compatibility.
-    pub fn init(a: Args) Iterator {
+    pub fn init(a: Args) Posix {
         if (native_os == .wasi) @compileError("In WASI, use initAllocator instead.");
         if (native_os == .windows) @compileError("In Windows, use initAllocator instead.");
-        return .{ .inner = .init(a) };
+        return .init(a);
     }
 
     pub const InitError = Inner.InitError;
@@ -349,26 +349,49 @@
     };
 
     pub const Posix = struct {
-        remaining: Vector,
+        args: Vector,
+        index: usize,
+        count: usize,
 
         pub const InitError = error{};
 
         pub fn init(a: Args) Posix {
-            return .{ .remaining = a.vector };
+            return Posix{
+                .args = a.vector,
+                .index = 0,
+                .count = a.vector.len
+            };
         }
 
-        pub fn next(it: *Posix) ?[:0]const u8 {
-            if (it.remaining.len == 0) return null;
-            const arg = it.remaining[0];
-            it.remaining = it.remaining[1..];
+		/// Returns the next argument and advances the iterator.
+        pub fn next(self: *Posix) ?[:0]const u8 {
+            if (self.index == self.count) return null;
+
+            const arg = self.args[self.index];
+            self.index += 1;
             return std.mem.sliceTo(arg, 0);
         }
 
-        pub fn skip(it: *Posix) bool {
-            if (it.remaining.len == 0) return false;
-            it.remaining = it.remaining[1..];
+		/// Skips the next argument and advances the iterator.
+        pub fn skip(self: *Posix) bool {
+            if (self.index == self.count) return false;
+
+            self.index += 1;
             return true;
         }
+
+		/// Regresses the iterator back to the previous argument.
+        pub fn backtrack(self: *Posix) bool {
+            if (self.index == 0) return false;
+
+            self.index -= 1;
+            return true;
+        }
+
+		/// Amount of arguments remaining.
+        pub fn remaining(self: *Posix) usize {
+            return self.count - self.index;
+        }
     };
 
     pub const Wasi = struct {
@@ -453,7 +476,7 @@
 
 /// Holds the command-line arguments, with the program name as the first entry.
 /// Use `iterateAllocator` for cross-platform code.
-pub fn iterate(a: Args) Iterator {
+pub fn iterate(a: Args) Iterator.Posix {
     return .init(a);
 }
 

I find the backtrack() function, similar to skip(), a bit ugly to use because it requires discarding the return value, and I’m not aware of a way around it. So I wouldn’t mind not having it and just going back to manually decrementing the index.

If I understood correctly, Wasm uses the Posix implementation if libc is linked. Maybe this can potentially cause some problem?

I changed the return type of non-allocating inits to the more specific Iterator.Posix. This may go against the intent of the larger API or style-guide.

The count field is redundant, it is duplicating the information stored in args.len. iter.args.len seems reasonable unless you want a cross-platform way to get it in which case use a function.

1 Like

I added a remaining() function, as the current implementation seems to care about it given that the Vector field is named remaining. But of course feel free to discart it if that’s not the case.

I also changed the return type of non-allocating inits to the more specific Iterator.Posix, so that it can be accessed directly from Args.

Args.patch.zig (2.3 KB)

--- Args.zig.master	2026-02-06 16:32:54.045685708 -0300
+++ Args.zig	2026-02-07 19:53:55.673249626 -0300
@@ -34,10 +34,10 @@
 
     /// Initialize the args iterator. Consider using `initAllocator` instead
     /// for cross-platform compatibility.
-    pub fn init(a: Args) Iterator {
+    pub fn init(a: Args) Posix {
         if (native_os == .wasi) @compileError("In WASI, use initAllocator instead.");
         if (native_os == .windows) @compileError("In Windows, use initAllocator instead.");
-        return .{ .inner = .init(a) };
+        return .init(a);
     }
 
     pub const InitError = Inner.InitError;
@@ -349,26 +349,39 @@
     };
 
     pub const Posix = struct {
-        remaining: Vector,
+        args: []const [*:0]const u8,
+        index: usize,
 
         pub const InitError = error{};
 
         pub fn init(a: Args) Posix {
-            return .{ .remaining = a.vector };
+            return .{
+                .args = a.vector,
+                .index = 0,
+            };
         }
 
-        pub fn next(it: *Posix) ?[:0]const u8 {
-            if (it.remaining.len == 0) return null;
-            const arg = it.remaining[0];
-            it.remaining = it.remaining[1..];
+        /// Returns the next argument and advances the iterator.
+        pub fn next(self: *Posix) ?[:0]const u8 {
+            if (self.index == self.args.len) return null;
+
+            const arg = self.args[self.index];
+            self.index += 1;
             return std.mem.sliceTo(arg, 0);
         }
 
-        pub fn skip(it: *Posix) bool {
-            if (it.remaining.len == 0) return false;
-            it.remaining = it.remaining[1..];
+        /// Skips the next argument and advances the iterator.
+        pub fn skip(self: *Posix) bool {
+            if (self.index == self.args.len) return false;
+
+            self.index += 1;
             return true;
         }
+
+        /// Returns the amount of arguments remaining.
+        pub fn remaining(self: *Posix) usize {
+            return self.args.len - self.index;
+        }
     };
 
     pub const Wasi = struct {
@@ -453,7 +466,7 @@
 
 /// Holds the command-line arguments, with the program name as the first entry.
 /// Use `iterateAllocator` for cross-platform code.
-pub fn iterate(a: Args) Iterator {
+pub fn iterate(a: Args) Iterator.Posix {
     return .init(a);
 }
 

Hey, Andrew. Is this offer still up? Could it be that I took you too literally on the patch thing, but you meant a PR instead? I don’t have a Codeberg account, but I could consider making one if that would help.

I noticed that using vector as the field name for Vector made more sense, since args seems like a common name for the Iterator itself, and it would be weird to do args.args when accessing Vector.

--- Args.zig.master	2026-02-06 16:32:54.045685708 -0300
+++ Args.zig	2026-02-07 19:53:55.673249626 -0300
@@ -34,10 +34,10 @@
 
     /// Initialize the args iterator. Consider using `initAllocator` instead
     /// for cross-platform compatibility.
-    pub fn init(a: Args) Iterator {
+    pub fn init(a: Args) Posix {
         if (native_os == .wasi) @compileError("In WASI, use initAllocator instead.");
         if (native_os == .windows) @compileError("In Windows, use initAllocator instead.");
-        return .{ .inner = .init(a) };
+        return .init(a);
     }
 
     pub const InitError = Inner.InitError;
@@ -349,26 +349,39 @@
     };
 
     pub const Posix = struct {
-        remaining: Vector,
+        vector: []const [*:0]const u8,
+        index: usize,
 
         pub const InitError = error{};
 
         pub fn init(a: Args) Posix {
-            return .{ .remaining = a.vector };
+            return .{
+                .vector = a.vector,
+                .index = 0,
+            };
         }
 
-        pub fn next(it: *Posix) ?[:0]const u8 {
-            if (it.remaining.len == 0) return null;
-            const arg = it.remaining[0];
-            it.remaining = it.remaining[1..];
+        /// Returns the next argument and advances the iterator.
+        pub fn next(self: *Posix) ?[:0]const u8 {
+            if (self.index == self.vector.len) return null;
+
+            const arg = self.vector[self.index];
+            self.index += 1;
             return std.mem.sliceTo(arg, 0);
         }
 
-        pub fn skip(it: *Posix) bool {
-            if (it.remaining.len == 0) return false;
-            it.remaining = it.remaining[1..];
+        /// Skips the next argument and advances the iterator.
+        pub fn skip(self: *Posix) bool {
+            if (self.index == self.vector.len) return false;
+
+            self.index += 1;
             return true;
         }
+
+        /// Returns the amount of arguments remaining.
+        pub fn remaining(self: *Posix) usize {
+            return self.vector.len - self.index;
+        }
     };
 
     pub const Wasi = struct {
@@ -453,7 +466,7 @@
 
 /// Holds the command-line arguments, with the program name as the first entry.
 /// Use `iterateAllocator` for cross-platform code.
-pub fn iterate(a: Args) Iterator {
+pub fn iterate(a: Args) Iterator.Posix {
     return .init(a);
 }

i’m not andrew, but if i were managing a project of the magnitude of the Zig compiler, using one method for reviewing and merging changes would really help save my time for what’s important. make a codeberg account.

2 Likes

Yeah, I should’ve been more sensible and just done that from the beginning.