diff --git a/kitty/launcher/shlex.h b/kitty/launcher/shlex.h index 7b6d13ebb..653def5f2 100644 --- a/kitty/launcher/shlex.h +++ b/kitty/launcher/shlex.h @@ -15,7 +15,7 @@ typedef enum { NORMAL, WORD, STRING_WITHOUT_ESCAPES, STRING_WITH_ESCAPES, ANSI_C typedef struct { const char *src; - bool support_ansi_c_quoting; + bool support_ansi_c_quoting, allow_empty; char *buf; size_t src_sz, src_pos, word_start, buf_pos; ShlexEnum state; @@ -90,6 +90,7 @@ static size_t get_word(ShlexState *self) { size_t ans = self->buf_pos; self->buf_pos = 0; self->buf[ans] = 0; + self->allow_empty = false; return ans; } @@ -201,7 +202,7 @@ next_word(ShlexState *self) { break; case WORD: switch(ch) { - case WHITESPACE: set_state(self, NORMAL); if (self->buf_pos) return get_word(self); break; + case WHITESPACE: set_state(self, NORMAL); if (self->buf_pos || self->allow_empty) return get_word(self); break; case STRING_WITH_ESCAPES_DELIM: set_state(self, STRING_WITH_ESCAPES); break; case STRING_WITHOUT_ESCAPES_DELIM: if (self->support_ansi_c_quoting && prev_word_ch == '$') { self->buf_pos--; set_state(self, ANSI_C_QUOTED); } @@ -212,18 +213,18 @@ next_word(ShlexState *self) { } break; case STRING_WITHOUT_ESCAPES: switch(ch) { - case STRING_WITHOUT_ESCAPES_DELIM: set_state(self, WORD); break; + case STRING_WITHOUT_ESCAPES_DELIM: set_state(self, WORD); self->allow_empty = true; break; default: write_ch(self, ch); break; } break; case STRING_WITH_ESCAPES: switch(ch) { - case STRING_WITH_ESCAPES_DELIM: set_state(self, WORD); break; + case STRING_WITH_ESCAPES_DELIM: set_state(self, WORD); self->allow_empty = true; break; case ESCAPE_CHAR: write_escaped_or_fail(); break; default: write_ch(self, ch); break; } break; case ANSI_C_QUOTED: switch(ch) { - case STRING_WITHOUT_ESCAPES_DELIM: set_state(self, WORD); break; + case STRING_WITHOUT_ESCAPES_DELIM: set_state(self, WORD); self->allow_empty = true; break; case ESCAPE_CHAR: if (!write_ansi_escape_ch(self)) return -1; break; default: write_ch(self, ch); break; } break; @@ -232,7 +233,7 @@ next_word(ShlexState *self) { switch (self->state) { case WORD: self->state = NORMAL; - if (self->buf_pos) return get_word(self); + if (self->buf_pos || self->allow_empty) return get_word(self); break; case STRING_WITH_ESCAPES: case STRING_WITHOUT_ESCAPES: case ANSI_C_QUOTED: self->err = "Unterminated string at the end of input"; diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index bb5de3849..d0fc02890 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -683,6 +683,12 @@ class TestDataTypes(BaseTest): tuple(shlex_split(bad)) for q, expected in { + 'a""': ((0, 'a'),), + 'a""b': ((0, 'ab'),), + '-1 "" 2': ((0, '-1'), (3, ''), (6, '2')), + "-1 '' 2": ((0, '-1'), (3, ''), (6, '2')), + 'a ""': ((0, 'a'), (2, '')), + '""': ((0, ''),), '"ab"': ((0, 'ab'),), r'x "ab"y \m': ((0, 'x'), (2, 'aby'), (8, 'm')), r'''x'y"\z'1''': ((0, 'xy"\\z1'),), @@ -694,11 +700,11 @@ class TestDataTypes(BaseTest): 'šŸ˜€ a': ((0, 'šŸ˜€'), (2, 'a')), ' \tšŸ˜€a': ((2, 'šŸ˜€a'),), }.items(): - actual = tuple(shlex_split_with_positions(q)) - self.ae(expected, actual, f'Failed for text: {q!r}') ex = tuple(x[1] for x in expected) actual = tuple(shlex_split(q)) self.ae(ex, actual, f'Failed for text: {q!r}') + actual = tuple(shlex_split_with_positions(q)) + self.ae(expected, actual, f'Failed for text: {q!r}') for q, expected in { "$'ab'": ((0, 'ab'),),