hush: allow nested negation "! ! ! CMD" - bash 5.2.15 allows it

Also, deindent "ch == EOF" code branch in parse_stream()

function                                             old     new   delta
done_word                                            799     797      -2
parse_stream                                        2453    2446      -7
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 0/2 up/down: 0/-9)               Total: -9 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
This commit is contained in:
Denys Vlasenko 2025-08-12 20:16:07 +02:00
parent 2b0b74e8b4
commit f161bc628f
3 changed files with 71 additions and 73 deletions

View file

@ -4190,12 +4190,12 @@ static const struct reserved_combo* reserved_word(struct parse_context *ctx)
# endif
if (r->flag == 0) { /* '!' */
if (ctx->pipe->cmds != ctx->command /* bash disallows: nice | ! cat */
|| ctx->pipe->pi_inverted /* bash disallows: ! ! true */
/* || ctx->pipe->pi_inverted - bash used to disallow "! ! true" bash 5.2.15 allows it */
) {
syntax_error_unexpected_ch('!');
ctx->ctx_res_w = RES_SNTX;
}
ctx->pipe->pi_inverted = 1;
ctx->pipe->pi_inverted = 1 - ctx->pipe->pi_inverted;
return r;
}
if (r->flag & FLAG_START) {
@ -5588,6 +5588,7 @@ static struct pipe *parse_stream(char **pstring,
struct in_str *input,
int end_trigger)
{
struct pipe *pi;
struct parse_context ctx;
int heredoc_cnt;
@ -5622,64 +5623,8 @@ static struct pipe *parse_stream(char **pstring,
ch = i_getch(input);
debug_printf_parse(": ch:%c (%d) globprotect:%d\n",
ch, ch, !!(ctx.word.o_expflags & EXP_FLAG_GLOBPROTECT_CHARS));
if (ch == EOF) {
struct pipe *pi;
if (heredoc_cnt) {
syntax_error_unterm_str("here document");
goto parse_error_exitcode1;
}
if (end_trigger == ')') {
syntax_error_unterm_ch('(');
goto parse_error_exitcode1;
}
if (end_trigger == '}') {
syntax_error_unterm_ch('{');
goto parse_error_exitcode1;
}
if (done_word(&ctx)) {
goto parse_error_exitcode1;
}
o_free_and_set_NULL(&ctx.word);
if (done_pipe(&ctx, PIPE_SEQ)) {
/* Testcase: sh -c 'date |' */
syntax_error_unterm_ch('|');
goto parse_error_exitcode1;
}
// TODO: catch 'date &&<whitespace><EOF>' and 'date ||<whitespace><EOF>' too
/* Do we sit inside of any if's, loops or case's? */
if (HAS_KEYWORDS
IF_HAS_KEYWORDS(&& (ctx.ctx_res_w != RES_NONE || ctx.old_flag != 0))
) {
syntax_error_unterm_str("compound statement");
goto parse_error_exitcode1;
}
pi = ctx.list_head;
/* If we got nothing... */
if (pi->num_cmds == 0
IF_HAS_KEYWORDS(&& pi->res_word == RES_NONE)
) {
free_pipe_list(pi);
pi = NULL;
}
#if !BB_MMU
debug_printf_parse("as_string1 '%s'\n", ctx.as_string.data);
if (pstring)
*pstring = ctx.as_string.data;
else
o_free(&ctx.as_string);
#endif
// heredoc_cnt must be 0 here anyway
//if (heredoc_cnt_ptr)
// *heredoc_cnt_ptr = heredoc_cnt;
debug_leave();
debug_printf_heredoc("parse_stream return heredoc_cnt:%d\n", heredoc_cnt);
debug_printf_parse("parse_stream return %p: EOF\n", pi);
return pi;
}
if (ch == EOF)
break;
/* Handle "'" and "\" first, as they won't play nice with
* i_peek_and_eat_bkslash_nl() anyway:
@ -5705,7 +5650,7 @@ static struct pipe *parse_stream(char **pstring,
/* bash-4.3.43 was removing backslash,
* but 4.4.19 retains it, most other shells too
*/
continue; /* get next char */
break;
}
/* Example: echo Hello \2>file
* we need to know that word 2 is quoted
@ -5719,7 +5664,7 @@ static struct pipe *parse_stream(char **pstring,
if (ch == '\'') {
ctx.word.has_quoted_part = 1;
next = i_getch(input);
if (next == '\'' && !ctx.pending_redirect)
if (next == '\'' && !ctx.pending_redirect/*why?*/)
goto insert_empty_quoted_str_marker;
ch = next;
@ -5835,7 +5780,7 @@ static struct pipe *parse_stream(char **pstring,
* a "cmd1 && <nl> cmd2 &" construct,
* cmd1 may need to run in BG).
*/
struct pipe *pi = ctx.list_head;
pi = ctx.list_head;
if (pi->num_cmds != 0 /* check #1 */
&& pi->followup != PIPE_BG /* check #2 */
) {
@ -6206,12 +6151,66 @@ static struct pipe *parse_stream(char **pstring,
}
} /* while (1) */
/* Reached EOF */
if (heredoc_cnt) {
syntax_error_unterm_str("here document");
goto parse_error_exitcode1;
}
if (end_trigger == ')') {
syntax_error_unterm_ch('(');
goto parse_error_exitcode1;
}
if (end_trigger == '}') {
syntax_error_unterm_ch('{');
goto parse_error_exitcode1;
}
if (done_word(&ctx)) {
goto parse_error_exitcode1;
}
o_free_and_set_NULL(&ctx.word);
if (done_pipe(&ctx, PIPE_SEQ)) {
/* Testcase: sh -c 'date |' */
syntax_error_unterm_ch('|');
goto parse_error_exitcode1;
}
// TODO: catch 'date &&<whitespace><EOF>' and 'date ||<whitespace><EOF>' too
#if HAS_KEYWORDS
/* Do we sit inside of any if's, loops or case's? */
if (ctx.ctx_res_w != RES_NONE || ctx.old_flag != 0) {
syntax_error_unterm_str("compound statement");
goto parse_error_exitcode1;
}
#endif
pi = ctx.list_head;
/* If we got nothing... */
if (pi->num_cmds == 0
IF_HAS_KEYWORDS(&& pi->res_word == RES_NONE)
) {
free_pipe_list(pi);
pi = NULL;
}
#if !BB_MMU
debug_printf_parse("as_string1 '%s'\n", ctx.as_string.data);
if (pstring)
*pstring = ctx.as_string.data;
else
o_free(&ctx.as_string);
#endif
// heredoc_cnt must be 0 here anyway
//if (heredoc_cnt_ptr)
// *heredoc_cnt_ptr = heredoc_cnt;
debug_leave();
debug_printf_heredoc("parse_stream return heredoc_cnt:%d\n", heredoc_cnt);
debug_printf_parse("parse_stream return %p: EOF\n", pi);
return pi;
parse_error_exitcode1:
G.last_exitcode = 1;
parse_error:
{
struct parse_context *pctx;
IF_HAS_KEYWORDS(struct parse_context *p2;)
struct parse_context *pctx IF_HAS_KEYWORDS(, *p2;);
/* Clean up allocated tree.
* Sample for finding leaks on syntax error recovery path.
@ -6226,8 +6225,7 @@ static struct pipe *parse_stream(char **pstring,
/* Update pipe/command counts,
* otherwise freeing may miss some */
done_pipe(pctx, PIPE_SEQ);
debug_printf_clean("freeing list %p from ctx %p\n",
pctx->list_head, pctx);
debug_printf_clean("freeing list %p from ctx %p\n", pctx->list_head, pctx);
debug_print_tree(pctx->list_head, 0);
free_pipe_list(pctx->list_head);
debug_printf_clean("freed list %p\n", pctx->list_head);
@ -6235,9 +6233,8 @@ static struct pipe *parse_stream(char **pstring,
o_free(&pctx->as_string);
#endif
IF_HAS_KEYWORDS(p2 = pctx->stack;)
if (pctx != &ctx) {
if (pctx != &ctx)
free(pctx);
}
IF_HAS_KEYWORDS(pctx = p2;)
} while (HAS_KEYWORDS && pctx);

View file

@ -1,2 +1,2 @@
bash 3.2 fails this
hush: syntax error: unexpected !
0:0
1:1

View file

@ -1,2 +1,3 @@
echo bash 3.2 fails this
! ! true
# bash 3.2 fails this, 5.2.15 allows
! ! true; echo 0:$?
! ! ! true; echo 1:$?