Fix handling of empty quoted string in shlex

This commit is contained in:
Kovid Goyal
2025-04-26 08:55:32 +05:30
parent e942bc817b
commit 2093fb1310
2 changed files with 15 additions and 8 deletions

View File

@@ -15,7 +15,7 @@ typedef enum { NORMAL, WORD, STRING_WITHOUT_ESCAPES, STRING_WITH_ESCAPES, ANSI_C
typedef struct { typedef struct {
const char *src; const char *src;
bool support_ansi_c_quoting; bool support_ansi_c_quoting, allow_empty;
char *buf; char *buf;
size_t src_sz, src_pos, word_start, buf_pos; size_t src_sz, src_pos, word_start, buf_pos;
ShlexEnum state; ShlexEnum state;
@@ -90,6 +90,7 @@ static size_t
get_word(ShlexState *self) { get_word(ShlexState *self) {
size_t ans = self->buf_pos; self->buf_pos = 0; size_t ans = self->buf_pos; self->buf_pos = 0;
self->buf[ans] = 0; self->buf[ans] = 0;
self->allow_empty = false;
return ans; return ans;
} }
@@ -201,7 +202,7 @@ next_word(ShlexState *self) {
break; break;
case WORD: case WORD:
switch(ch) { 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_WITH_ESCAPES_DELIM: set_state(self, STRING_WITH_ESCAPES); break;
case STRING_WITHOUT_ESCAPES_DELIM: case STRING_WITHOUT_ESCAPES_DELIM:
if (self->support_ansi_c_quoting && prev_word_ch == '$') { self->buf_pos--; set_state(self, ANSI_C_QUOTED); } 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; } break;
case STRING_WITHOUT_ESCAPES: case STRING_WITHOUT_ESCAPES:
switch(ch) { 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; default: write_ch(self, ch); break;
} break; } break;
case STRING_WITH_ESCAPES: case STRING_WITH_ESCAPES:
switch(ch) { 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; case ESCAPE_CHAR: write_escaped_or_fail(); break;
default: write_ch(self, ch); break; default: write_ch(self, ch); break;
} break; } break;
case ANSI_C_QUOTED: case ANSI_C_QUOTED:
switch(ch) { 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; case ESCAPE_CHAR: if (!write_ansi_escape_ch(self)) return -1; break;
default: write_ch(self, ch); break; default: write_ch(self, ch); break;
} break; } break;
@@ -232,7 +233,7 @@ next_word(ShlexState *self) {
switch (self->state) { switch (self->state) {
case WORD: case WORD:
self->state = NORMAL; self->state = NORMAL;
if (self->buf_pos) return get_word(self); if (self->buf_pos || self->allow_empty) return get_word(self);
break; break;
case STRING_WITH_ESCAPES: case STRING_WITHOUT_ESCAPES: case ANSI_C_QUOTED: case STRING_WITH_ESCAPES: case STRING_WITHOUT_ESCAPES: case ANSI_C_QUOTED:
self->err = "Unterminated string at the end of input"; self->err = "Unterminated string at the end of input";

View File

@@ -683,6 +683,12 @@ class TestDataTypes(BaseTest):
tuple(shlex_split(bad)) tuple(shlex_split(bad))
for q, expected in { 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'),), '"ab"': ((0, 'ab'),),
r'x "ab"y \m': ((0, 'x'), (2, 'aby'), (8, 'm')), r'x "ab"y \m': ((0, 'x'), (2, 'aby'), (8, 'm')),
r'''x'y"\z'1''': ((0, 'xy"\\z1'),), r'''x'y"\z'1''': ((0, 'xy"\\z1'),),
@@ -694,11 +700,11 @@ class TestDataTypes(BaseTest):
'😀 a': ((0, '😀'), (2, 'a')), '😀 a': ((0, '😀'), (2, 'a')),
' \t😀a': ((2, '😀a'),), ' \t😀a': ((2, '😀a'),),
}.items(): }.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) ex = tuple(x[1] for x in expected)
actual = tuple(shlex_split(q)) actual = tuple(shlex_split(q))
self.ae(ex, actual, f'Failed for text: {q!r}') 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 { for q, expected in {
"$'ab'": ((0, 'ab'),), "$'ab'": ((0, 'ab'),),