Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484//! Iterator to split a string into fields by commas, taking into account//! quotes and escapes.//!//! Supports the same escapes as in Zig literal strings.//!//! Quotes must begin and end with a double quote (`"`). It is an error to not//! end a quote that was begun. To include a double quote inside a quote (or to//! not have a double quote start a quoted section) escape it with a backslash.//!//! Single quotes (`'`) are not special, they do not begin a quoted block.//!//! Zig multiline string literals are NOT supported.//!//! Quotes and escapes are not stripped or decoded, that must be handled as a//! separate step!//!//! On Windows, backslash is only treated as an escape character inside quoted//! strings. Outside quotes, backslash is a literal character (path separator).const CommaSplitter = @This();
const builtin = @import("builtin");
/// Whether backslash acts as an escape character outside quoted strings./// On Windows, backslash is the path separator so it is always literal/// outside quotes.const escape_outside_quotes = builtin.os.tag != .windows;
pub const Error = error{ UnclosedQuote, UnfinishedEscape, IllegalEscape,};
/// the string that we are splittingstr: []const u8,/// how much of the string has been consumed so farindex: usize,
/// initialize a splitter with the given stringpub fn init(str: []const u8) CommaSplitter { return .{ .str = str, .index = 0, };}
/// return the next field, null if no more fieldspub fn next(self: *CommaSplitter) Error!?[]const u8 { if (self.index >= self.str.len) return null;
// where the current field starts const start = self.index; // state of state machine const State = enum { normal, quoted, escape, hexescape, unicodeescape, }; // keep track of the state to return to when done processing an escape // sequence. var last: State = .normal; // used to count number of digits seen in a hex escape var hexescape_digits: usize = 0; // sub-state of parsing hex escapes var unicodeescape_state: enum { start, digits, } = .start; // number of digits in a unicode escape seen so far var unicodeescape_digits: usize = 0; // accumulator for value of unicode escape var unicodeescape_value: usize = 0;
loop: switch (State.normal) { .normal => { if (self.index >= self.str.len) return self.str[start..]; switch (self.str[self.index]) { ',' => { self.index += 1; return self.str[start .. self.index - 1]; }, '"' => { self.index += 1; continue :loop .quoted; }, '\\' => { self.index += 1; if (comptime escape_outside_quotes) { last = .normal; continue :loop .escape; } continue :loop .normal; }, else => { self.index += 1; continue :loop .normal; }, } }, .quoted => { if (self.index >= self.str.len) return error.UnclosedQuote; switch (self.str[self.index]) { '"' => { self.index += 1; continue :loop .normal; }, '\\' => { self.index += 1; last = .quoted; continue :loop .escape; }, else => { self.index += 1; continue :loop .quoted; }, } }, .escape => { if (self.index >= self.str.len) return error.UnfinishedEscape; switch (self.str[self.index]) { 'n', 'r', 't', '\\', '\'', '"' => { self.index += 1; continue :loop last; }, 'x' => { self.index += 1; hexescape_digits = 0; continue :loop .hexescape; }, 'u' => { self.index += 1; unicodeescape_state = .start; unicodeescape_digits = 0; unicodeescape_value = 0; continue :loop .unicodeescape; }, else => return error.IllegalEscape, } }, .hexescape => { if (self.index >= self.str.len) return error.UnfinishedEscape; switch (self.str[self.index]) { '0'...'9', 'a'...'f', 'A'...'F' => { self.index += 1; hexescape_digits += 1; if (hexescape_digits == 2) continue :loop last; continue :loop .hexescape; }, else => return error.IllegalEscape, } }, .unicodeescape => { if (self.index >= self.str.len) return error.UnfinishedEscape; switch (unicodeescape_state) { .start => { switch (self.str[self.index]) { '{' => { self.index += 1; unicodeescape_value = 0; unicodeescape_state = .digits; continue :loop .unicodeescape; }, else => return error.IllegalEscape, } }, .digits => { switch (self.str[self.index]) { '}' => { self.index += 1; if (unicodeescape_digits == 0) return error.IllegalEscape; continue :loop last; }, '0'...'9' => |d| { self.index += 1; unicodeescape_digits += 1; unicodeescape_value <<= 4; unicodeescape_value += d - '0'; }, 'a'...'f' => |d| { self.index += 1; unicodeescape_digits += 1; unicodeescape_value <<= 4; unicodeescape_value += d - 'a'; }, 'A'...'F' => |d| { self.index += 1; unicodeescape_digits += 1; unicodeescape_value <<= 4; unicodeescape_value += d - 'A'; }, else => return error.IllegalEscape, } if (unicodeescape_value > 0x10ffff) return error.IllegalEscape; continue :loop .unicodeescape; }, } }, }}
/// Return any remaining string data, whether it has a comma or not.pub fn rest(self: *CommaSplitter) ?[]const u8 { if (self.index >= self.str.len) return null; defer self.index = self.str.len; return self.str[self.index..];}
test "splitter 1" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("a,b,c"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectEqualStrings("b", (try s.next()).?); try testing.expectEqualStrings("c", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 2" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init(""); try testing.expect(null == try s.next());}
test "splitter 3" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("a"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 4" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\x5a"); try testing.expectEqualStrings("\\x5a", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 5" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("'a',b"); try testing.expectEqualStrings("'a'", (try s.next()).?); try testing.expectEqualStrings("b", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 6" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("'a,b',c"); try testing.expectEqualStrings("'a", (try s.next()).?); try testing.expectEqualStrings("b'", (try s.next()).?); try testing.expectEqualStrings("c", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 7" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\"a,b\",c"); try testing.expectEqualStrings("\"a,b\"", (try s.next()).?); try testing.expectEqualStrings("c", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 8" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init(" a , b "); try testing.expectEqualStrings(" a ", (try s.next()).?); try testing.expectEqualStrings(" b ", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 9" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\x"); try testing.expectError(error.UnfinishedEscape, s.next());}
test "splitter 10" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\x5"); try testing.expectError(error.UnfinishedEscape, s.next());}
test "splitter 11" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\u"); try testing.expectError(error.UnfinishedEscape, s.next());}
test "splitter 12" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\u{"); try testing.expectError(error.UnfinishedEscape, s.next());}
test "splitter 13" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\u{}"); try testing.expectError(error.IllegalEscape, s.next());}
test "splitter 14" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\u{h1}"); try testing.expectError(error.IllegalEscape, s.next());}
test "splitter 15" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\u{10ffff}"); try testing.expectEqualStrings("\\u{10ffff}", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 16" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\u{110000}"); try testing.expectError(error.IllegalEscape, s.next());}
test "splitter 17" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\d"); try testing.expectError(error.IllegalEscape, s.next());}
test "splitter 18" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\\n\\r\\t\\\"\\'\\\\"); try testing.expectEqualStrings("\\n\\r\\t\\\"\\'\\\\", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 19" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\"abc'def'ghi\""); try testing.expectEqualStrings("\"abc'def'ghi\"", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 20" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("\",\",abc"); try testing.expectEqualStrings("\",\"", (try s.next()).?); try testing.expectEqualStrings("abc", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 21" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("'a','b', 'c'"); try testing.expectEqualStrings("'a'", (try s.next()).?); try testing.expectEqualStrings("'b'", (try s.next()).?); try testing.expectEqualStrings(" 'c'", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 22" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("abc\"def"); try testing.expectError(error.UnclosedQuote, s.next());}
test "splitter 23" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("title:\"Focus Split: Up\",description:\"Focus the split above, if it exists.\",action:goto_split:up"); try testing.expectEqualStrings("title:\"Focus Split: Up\"", (try s.next()).?); try testing.expectEqualStrings("description:\"Focus the split above, if it exists.\"", (try s.next()).?); try testing.expectEqualStrings("action:goto_split:up", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter 24" { const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("a,b,c,def"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectEqualStrings("b", (try s.next()).?); try testing.expectEqualStrings("c,def", s.rest().?); try testing.expect(null == try s.next());}
test "splitter 25" { if (comptime !escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("a,\\u{10,df}"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectError(error.IllegalEscape, s.next());}
// Windows-specific tests: backslash is literal outside quotes.
test "splitter: windows paths" { if (comptime escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
var s: CommaSplitter = .init("light:C:\\Users\\foo\\theme,dark:C:\\Users\\bar\\theme"); try testing.expectEqualStrings("light:C:\\Users\\foo\\theme", (try s.next()).?); try testing.expectEqualStrings("dark:C:\\Users\\bar\\theme", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter: backslash literal outside quotes on windows" { if (comptime escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
// Backslash followed by characters that would be escapes on Unix // are treated as literal on Windows outside quotes. var s: CommaSplitter = .init("\\n\\r\\t"); try testing.expectEqualStrings("\\n\\r\\t", (try s.next()).?); try testing.expect(null == try s.next());}
test "splitter: backslash still escapes inside quotes on windows" { if (comptime escape_outside_quotes) return error.SkipZigTest; const std = @import("std"); const testing = std.testing;
// Inside quotes, backslash escapes work on all platforms. var s: CommaSplitter = .init("\"hello\\nworld\""); try testing.expectEqualStrings("\"hello\\nworld\"", (try s.next()).?); try testing.expect(null == try s.next());}