aboutsummaryrefslogtreecommitdiff
path: root/shell
diff options
context:
space:
mode:
authorDenys Vlasenko <vda.linux@googlemail.com>2025-08-12 17:55:48 +0200
committerDenys Vlasenko <vda.linux@googlemail.com>2025-08-12 17:56:58 +0200
commit5ecbed0e2660f74f3690de52387a7c2f3ea3f2d2 (patch)
treeadc06cd42a26b8f76260ed1ea38c538218d1457e /shell
parentab1de7df999c43f12be26d90e6a143ecfa966630 (diff)
downloadbusybox-w32-5ecbed0e2660f74f3690de52387a7c2f3ea3f2d2.tar.gz
busybox-w32-5ecbed0e2660f74f3690de52387a7c2f3ea3f2d2.tar.bz2
busybox-w32-5ecbed0e2660f74f3690de52387a7c2f3ea3f2d2.zip
hush: do not segfault on "for </dev/null v in..."
This is not accepted by bash, we may also disallow this, but for now, at least do not crash Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
Diffstat (limited to 'shell')
-rw-r--r--shell/hush.c233
-rw-r--r--shell/hush_test/hush-redir/redir_and_constructs1.right2
-rwxr-xr-xshell/hush_test/hush-redir/redir_and_constructs1.tests2
3 files changed, 124 insertions, 113 deletions
diff --git a/shell/hush.c b/shell/hush.c
index 254f39943..fe3d77c65 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -3914,9 +3914,11 @@ static struct pipe *new_pipe(void)
3914 return pi; 3914 return pi;
3915} 3915}
3916 3916
3917/* Command (member of a pipe) is complete, or we start a new pipe 3917/* Parsing of command (member of a pipe) is completed.
3918 * if ctx->command is NULL. 3918 * If it's not null, a new empty command structure is added
3919 * No errors possible here. 3919 * to the current pipe, and ctx->command is set to it.
3920 * Return the current number of already parsed commands in the pipe.
3921 * No errors are possible here.
3920 */ 3922 */
3921static int done_command(struct parse_context *ctx) 3923static int done_command(struct parse_context *ctx)
3922{ 3924{
@@ -3932,17 +3934,16 @@ static int done_command(struct parse_context *ctx)
3932 ctx->pending_redirect = NULL; 3934 ctx->pending_redirect = NULL;
3933 } 3935 }
3934#endif 3936#endif
3935
3936 if (command) { 3937 if (command) {
3937 if (IS_NULL_CMD(command)) { 3938 if (IS_NULL_CMD(command)) {
3938 debug_printf_parse("done_command: skipping null cmd, num_cmds=%d\n", pi->num_cmds); 3939 debug_printf_parse("done_command: skipping null cmd, num_cmds:%d\n", pi->num_cmds);
3939 goto clear_and_ret; 3940 goto clear_and_ret;
3940 } 3941 }
3941 pi->num_cmds++; 3942 pi->num_cmds++;
3942 debug_printf_parse("done_command: ++num_cmds=%d\n", pi->num_cmds); 3943 debug_printf_parse("done_command: ++num_cmds=%d\n", pi->num_cmds);
3943 //debug_print_tree(ctx->list_head, 20); 3944 //debug_print_tree(ctx->list_head, 20);
3944 } else { 3945 } else {
3945 debug_printf_parse("done_command: initializing, num_cmds=%d\n", pi->num_cmds); 3946 debug_printf_parse("done_command: initializing, num_cmds:%d\n", pi->num_cmds);
3946 } 3947 }
3947 3948
3948 /* Only real trickiness here is that the uncommitted 3949 /* Only real trickiness here is that the uncommitted
@@ -3953,28 +3954,33 @@ static int done_command(struct parse_context *ctx)
3953 memset(command, 0, sizeof(*command)); 3954 memset(command, 0, sizeof(*command));
3954#if ENABLE_HUSH_LINENO_VAR 3955#if ENABLE_HUSH_LINENO_VAR
3955 command->lineno = G.parse_lineno; 3956 command->lineno = G.parse_lineno;
3956 debug_printf_parse("command->lineno = G.parse_lineno (%u)\n", G.parse_lineno); 3957 debug_printf_parse("command->lineno=G.parse_lineno (%u)\n", G.parse_lineno);
3957#endif 3958#endif
3958 return pi->num_cmds; /* used only for 0/nonzero check */ 3959 return pi->num_cmds;
3959} 3960}
3960 3961
3962/* Parsing of a pipe is completed.
3963 * Finish prsing current command via done_command().
3964 * (If the pipe is not empty, but done_command() did not change the number
3965 * of commands in pipe, return value is 1. Used for catching syntax errors)
3966 */
3961static int done_pipe(struct parse_context *ctx, pipe_style type) 3967static int done_pipe(struct parse_context *ctx, pipe_style type)
3962{ 3968{
3963 int not_null_pipe; 3969 int num_cmds;
3964 int oldnum; 3970 int oldnum;
3965 int nullcommand; 3971 int last_cmd_is_null;
3966 3972
3967 debug_printf_parse("done_pipe entered, followup %d\n", type); 3973 debug_printf_parse("done_pipe entered, followup %d\n", type);
3968 /* Close previous command */ 3974 /* Close previous command */
3969 oldnum = ctx->pipe->num_cmds; 3975 oldnum = ctx->pipe->num_cmds;
3970 not_null_pipe = done_command(ctx); 3976 num_cmds = done_command(ctx);
3971 3977
3972 /* This is true if this was a non-empty pipe, 3978 /* This is true if this was a non-empty pipe,
3973 * but done_command didn't add a new member to it. 3979 * but done_command didn't add a new member to it.
3974 * Usually it is a syntax error. 3980 * Usually it is a syntax error.
3975 * Examples: "date | | ...", "date | ; ..." 3981 * Examples: "date | | ...", "date | ; ..."
3976 */ 3982 */
3977 nullcommand = (oldnum && not_null_pipe == oldnum); 3983 last_cmd_is_null = (oldnum != 0 && num_cmds == oldnum);
3978 3984
3979#if HAS_KEYWORDS 3985#if HAS_KEYWORDS
3980 ctx->pipe->pi_inverted = ctx->ctx_inverted; 3986 ctx->pipe->pi_inverted = ctx->ctx_inverted;
@@ -4017,7 +4023,7 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
4017 ctx->list_head = ctx->pipe = pi; 4023 ctx->list_head = ctx->pipe = pi;
4018 /* for cases like "cmd && &", do not be tricked by last command 4024 /* for cases like "cmd && &", do not be tricked by last command
4019 * being null - the entire {...} & is NOT null! */ 4025 * being null - the entire {...} & is NOT null! */
4020 not_null_pipe = 1; 4026 num_cmds = 1;
4021 } else { 4027 } else {
4022 no_conv: 4028 no_conv:
4023 ctx->pipe->followup = type; 4029 ctx->pipe->followup = type;
@@ -4026,7 +4032,7 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
4026 /* Without this check, even just <enter> on command line generates 4032 /* Without this check, even just <enter> on command line generates
4027 * tree of three NOPs (!). Which is harmless but annoying. 4033 * tree of three NOPs (!). Which is harmless but annoying.
4028 * IOW: it is safe to do it unconditionally. */ 4034 * IOW: it is safe to do it unconditionally. */
4029 if (not_null_pipe 4035 if (num_cmds != 0
4030#if ENABLE_HUSH_IF 4036#if ENABLE_HUSH_IF
4031 || ctx->ctx_res_w == RES_FI 4037 || ctx->ctx_res_w == RES_FI
4032#endif 4038#endif
@@ -4041,8 +4047,8 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
4041 ) { 4047 ) {
4042 struct pipe *new_p; 4048 struct pipe *new_p;
4043 debug_printf_parse("done_pipe: adding new pipe: " 4049 debug_printf_parse("done_pipe: adding new pipe: "
4044 "not_null_pipe:%d ctx->ctx_res_w:%d\n", 4050 "num_cmds:%d ctx->ctx_res_w:%d\n",
4045 not_null_pipe, ctx->ctx_res_w); 4051 num_cmds, ctx->ctx_res_w);
4046 new_p = new_pipe(); 4052 new_p = new_pipe();
4047 ctx->pipe->next = new_p; 4053 ctx->pipe->next = new_p;
4048 ctx->pipe = new_p; 4054 ctx->pipe = new_p;
@@ -4071,8 +4077,8 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
4071 done_command(ctx); 4077 done_command(ctx);
4072 //debug_print_tree(ctx->list_head, 10); 4078 //debug_print_tree(ctx->list_head, 10);
4073 } 4079 }
4074 debug_printf_parse("done_pipe return:%d\n", nullcommand); 4080 debug_printf_parse("done_pipe return: last_cmd_is_null:%d\n", last_cmd_is_null);
4075 return nullcommand; 4081 return last_cmd_is_null;
4076} 4082}
4077 4083
4078static void initialize_context(struct parse_context *ctx) 4084static void initialize_context(struct parse_context *ctx)
@@ -4255,7 +4261,10 @@ static const struct reserved_combo* reserved_word(struct parse_context *ctx)
4255} 4261}
4256#endif /* HAS_KEYWORDS */ 4262#endif /* HAS_KEYWORDS */
4257 4263
4258/* Word is complete, look at it and update parsing context. 4264/* Parsing of a word is complete.
4265 * Look at it and update current command:
4266 * update current command's argv/cmd_type/etc, fill in redirect name and type,
4267 * check reserved-ness and assignment-ness, etc...
4259 * Normal return is 0. Syntax errors return 1. 4268 * Normal return is 0. Syntax errors return 1.
4260 * Note: on return, word is reset, but not o_free'd! 4269 * Note: on return, word is reset, but not o_free'd!
4261 */ 4270 */
@@ -4292,7 +4301,7 @@ static int done_word(struct parse_context *ctx)
4292// as written: 4301// as written:
4293// <<EOF$t 4302// <<EOF$t
4294// <<EOF$((1)) 4303// <<EOF$((1))
4295// <<EOF`true` [this case also makes heredoc "quoted", a-la <<"EOF". Probably bash-4.3.43 bug] 4304// <<EOF`true` [bash 4.3.43 bug: this case also makes heredoc "quoted", a-la <<"EOF". Fixed by 5.2.15]
4296 4305
4297 ctx->pending_redirect->rd_filename = xstrdup(ctx->word.data); 4306 ctx->pending_redirect->rd_filename = xstrdup(ctx->word.data);
4298 /* Cater for >\file case: 4307 /* Cater for >\file case:
@@ -4309,38 +4318,41 @@ static int done_word(struct parse_context *ctx)
4309 } 4318 }
4310 debug_printf_parse("word stored in rd_filename: '%s'\n", ctx->word.data); 4319 debug_printf_parse("word stored in rd_filename: '%s'\n", ctx->word.data);
4311 ctx->pending_redirect = NULL; 4320 ctx->pending_redirect = NULL;
4312 } else { 4321 goto ret;
4322 }
4323
4313#if HAS_KEYWORDS 4324#if HAS_KEYWORDS
4314# if ENABLE_HUSH_CASE 4325# if ENABLE_HUSH_CASE
4315 if (ctx->ctx_dsemicolon 4326 if (ctx->ctx_dsemicolon
4316 && strcmp(ctx->word.data, "esac") != 0 /* not "... pattern) cmd;; esac" */ 4327 && strcmp(ctx->word.data, "esac") != 0 /* not "... pattern) cmd;; esac" */
4317 ) { 4328 ) {
4318 /* already done when ctx_dsemicolon was set to 1: */ 4329 /* already done when ctx_dsemicolon was set to 1: */
4319 /* ctx->ctx_res_w = RES_MATCH; */ 4330 /* ctx->ctx_res_w = RES_MATCH; */
4320 ctx->ctx_dsemicolon = 0; 4331 ctx->ctx_dsemicolon = 0;
4321 } else 4332 } else
4322# endif 4333# endif
4323# if defined(CMD_TEST2_SINGLEWORD_NOGLOB) 4334# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
4324 if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB 4335 if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB
4325 && strcmp(ctx->word.data, "]]") == 0 4336 && strcmp(ctx->word.data, "]]") == 0
4326 ) { 4337 ) {
4327 /* allow "[[ ]] >file" etc */ 4338 /* allow "[[ ]] >file" etc */
4328 command->cmd_type = CMD_SINGLEWORD_NOGLOB; 4339 command->cmd_type = CMD_SINGLEWORD_NOGLOB;
4329 } else 4340 } else
4330# endif 4341# endif
4331 if (!command->argv /* if it's the first word... */ 4342 if (!command->argv /* if it's the first word... */
4343 && !command->redirects /* and no redirects yet... try: </dev/null ! true; echo $? */
4332# if ENABLE_HUSH_LOOPS 4344# if ENABLE_HUSH_LOOPS
4333 && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */ 4345 && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */
4334 && ctx->ctx_res_w != RES_IN 4346 && ctx->ctx_res_w != RES_IN
4335# endif 4347# endif
4336# if ENABLE_HUSH_CASE 4348# if ENABLE_HUSH_CASE
4337 && ctx->ctx_res_w != RES_CASE 4349 && ctx->ctx_res_w != RES_CASE
4338# endif 4350# endif
4339 ) { 4351 ) {
4340 const struct reserved_combo *reserved; 4352 const struct reserved_combo *reserved;
4341 reserved = reserved_word(ctx); 4353 reserved = reserved_word(ctx);
4342 debug_printf_parse("checking for reserved-ness: %d\n", !!reserved); 4354 debug_printf_parse("checking for reserved-ness: %d\n", !!reserved);
4343 if (reserved) { 4355 if (reserved) {
4344# if ENABLE_HUSH_LINENO_VAR 4356# if ENABLE_HUSH_LINENO_VAR
4345/* Case: 4357/* Case:
4346 * "while ...; do 4358 * "while ...; do
@@ -4348,80 +4360,79 @@ static int done_word(struct parse_context *ctx)
4348 * If we don't close the pipe _now_, immediately after "do", lineno logic 4360 * If we don't close the pipe _now_, immediately after "do", lineno logic
4349 * sees "cmd" as starting at "do" - i.e., at the previous line. 4361 * sees "cmd" as starting at "do" - i.e., at the previous line.
4350 */ 4362 */
4351 if (0 4363 if (0
4352 IF_HUSH_IF(|| reserved->res == RES_THEN) 4364 IF_HUSH_IF(|| reserved->res == RES_THEN)
4353 IF_HUSH_IF(|| reserved->res == RES_ELIF) 4365 IF_HUSH_IF(|| reserved->res == RES_ELIF)
4354 IF_HUSH_IF(|| reserved->res == RES_ELSE) 4366 IF_HUSH_IF(|| reserved->res == RES_ELSE)
4355 IF_HUSH_LOOPS(|| reserved->res == RES_DO) 4367 IF_HUSH_LOOPS(|| reserved->res == RES_DO)
4356 ) { 4368 ) {
4357 done_pipe(ctx, PIPE_SEQ); 4369 done_pipe(ctx, PIPE_SEQ);
4358 }
4359# endif
4360 o_reset_to_empty_unquoted(&ctx->word);
4361 debug_printf_parse("done_word return %d\n",
4362 (ctx->ctx_res_w == RES_SNTX));
4363 return (ctx->ctx_res_w == RES_SNTX);
4364 } 4370 }
4371# endif
4372 o_reset_to_empty_unquoted(&ctx->word);
4373 debug_printf_parse("done_word return %d\n",
4374 (ctx->ctx_res_w == RES_SNTX));
4375 return (ctx->ctx_res_w == RES_SNTX);
4376 }
4365# if defined(CMD_TEST2_SINGLEWORD_NOGLOB) 4377# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
4366 if (strcmp(ctx->word.data, "[[") == 0) { 4378 if (strcmp(ctx->word.data, "[[") == 0) {
4367 command->cmd_type = CMD_TEST2_SINGLEWORD_NOGLOB; 4379 command->cmd_type = CMD_TEST2_SINGLEWORD_NOGLOB;
4368 } else 4380 } else
4369# endif 4381# endif
4370# if defined(CMD_SINGLEWORD_NOGLOB) 4382# if defined(CMD_SINGLEWORD_NOGLOB)
4371 if (0 4383 if (0
4372 /* In bash, local/export/readonly are special, args 4384 /* In bash, local/export/readonly are special, args
4373 * are assignments and therefore expansion of them 4385 * are assignments and therefore expansion of them
4374 * should be "one-word" expansion: 4386 * should be "one-word" expansion:
4375 * $ export i=`echo 'a b'` # one arg: "i=a b" 4387 * $ export i=`echo 'a b'` # one arg: "i=a b"
4376 * compare with: 4388 * compare with:
4377 * $ ls i=`echo 'a b'` # two args: "i=a" and "b" 4389 * $ ls i=`echo 'a b'` # two args: "i=a" and "b"
4378 * ls: cannot access i=a: No such file or directory 4390 * ls: cannot access i=a: No such file or directory
4379 * ls: cannot access b: No such file or directory 4391 * ls: cannot access b: No such file or directory
4380 * Note: bash 3.2.33(1) does this only if export word 4392 * Note: bash 3.2.33(1) does this only if export word
4381 * itself is not quoted: 4393 * itself is not quoted:
4382 * $ export i=`echo 'aaa bbb'`; echo "$i" 4394 * $ export i=`echo 'aaa bbb'`; echo "$i"
4383 * aaa bbb 4395 * aaa bbb
4384 * $ "export" i=`echo 'aaa bbb'`; echo "$i" 4396 * $ "export" i=`echo 'aaa bbb'`; echo "$i"
4385 * aaa 4397 * aaa
4386 */ 4398 */
4387 IF_HUSH_LOCAL( || strcmp(ctx->word.data, "local") == 0) 4399 IF_HUSH_LOCAL( || strcmp(ctx->word.data, "local") == 0)
4388 IF_HUSH_EXPORT( || strcmp(ctx->word.data, "export") == 0) 4400 IF_HUSH_EXPORT( || strcmp(ctx->word.data, "export") == 0)
4389 IF_HUSH_READONLY(|| strcmp(ctx->word.data, "readonly") == 0) 4401 IF_HUSH_READONLY(|| strcmp(ctx->word.data, "readonly") == 0)
4390 ) { 4402 ) {
4391 command->cmd_type = CMD_SINGLEWORD_NOGLOB; 4403 command->cmd_type = CMD_SINGLEWORD_NOGLOB;
4392 } 4404 }
4393# else 4405# else
4394 { /* empty block to pair "if ... else" */ } 4406 { /* empty block to pair "if ... else" */ }
4395# endif 4407# endif
4396 } 4408 }
4397#endif /* HAS_KEYWORDS */ 4409#endif /* HAS_KEYWORDS */
4398 4410
4399 if (command->group) { 4411 if (command->group) {
4400 /* "{ echo foo; } echo bar" - bad */ 4412 /* "{ echo foo; } echo bar" - bad */
4401 syntax_error_at(ctx->word.data); 4413 syntax_error_at(ctx->word.data);
4402 debug_printf_parse("done_word return 1: syntax error, " 4414 debug_printf_parse("done_word return 1: syntax error, "
4403 "groups and arglists don't mix\n"); 4415 "groups and arglists don't mix\n");
4404 return 1; 4416 return 1;
4405 } 4417 }
4406 4418
4407 /* If this word wasn't an assignment, next ones definitely 4419 /* If this word wasn't an assignment, next ones definitely
4408 * can't be assignments. Even if they look like ones. */ 4420 * can't be assignments. Even if they look like ones. */
4409 if (ctx->is_assignment != DEFINITELY_ASSIGNMENT 4421 if (ctx->is_assignment != DEFINITELY_ASSIGNMENT
4410 && ctx->is_assignment != WORD_IS_KEYWORD 4422 && ctx->is_assignment != WORD_IS_KEYWORD
4411 ) { 4423 ) {
4412 ctx->is_assignment = NOT_ASSIGNMENT; 4424 ctx->is_assignment = NOT_ASSIGNMENT;
4413 } else { 4425 } else {
4414 if (ctx->is_assignment == DEFINITELY_ASSIGNMENT) { 4426 if (ctx->is_assignment == DEFINITELY_ASSIGNMENT) {
4415 command->assignment_cnt++; 4427 command->assignment_cnt++;
4416 debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt); 4428 debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt);
4417 }
4418 debug_printf_parse("ctx->is_assignment was:'%s'\n", assignment_flag[ctx->is_assignment]);
4419 ctx->is_assignment = MAYBE_ASSIGNMENT;
4420 } 4429 }
4421 debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]); 4430 debug_printf_parse("ctx->is_assignment was:'%s'\n", assignment_flag[ctx->is_assignment]);
4422 command->argv = add_string_to_strings(command->argv, xstrdup(ctx->word.data)); 4431 ctx->is_assignment = MAYBE_ASSIGNMENT;
4423 debug_print_strings("word appended to argv", command->argv);
4424 } 4432 }
4433 debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]);
4434 command->argv = add_string_to_strings(command->argv, xstrdup(ctx->word.data));
4435 debug_print_strings("word appended to argv", command->argv);
4425 4436
4426#if ENABLE_HUSH_LOOPS 4437#if ENABLE_HUSH_LOOPS
4427 if (ctx->ctx_res_w == RES_FOR) { 4438 if (ctx->ctx_res_w == RES_FOR) {
@@ -4446,8 +4457,8 @@ static int done_word(struct parse_context *ctx)
4446 } 4457 }
4447#endif 4458#endif
4448 4459
4460 ret:
4449 o_reset_to_empty_unquoted(&ctx->word); 4461 o_reset_to_empty_unquoted(&ctx->word);
4450
4451 debug_printf_parse("done_word return 0\n"); 4462 debug_printf_parse("done_word return 0\n");
4452 return 0; 4463 return 0;
4453} 4464}
@@ -4770,8 +4781,7 @@ static struct pipe *parse_stream(char **pstring,
4770 struct in_str *input, 4781 struct in_str *input,
4771 int end_trigger); 4782 int end_trigger);
4772 4783
4773/* Returns number of heredocs not yet consumed, 4784/* Returns number of heredocs not yet consumed, or -1 on error.
4774 * or -1 on error.
4775 */ 4785 */
4776static int parse_group(struct parse_context *ctx, 4786static int parse_group(struct parse_context *ctx,
4777 struct in_str *input, int ch) 4787 struct in_str *input, int ch)
@@ -4832,12 +4842,9 @@ static int parse_group(struct parse_context *ctx,
4832 if (command->argv /* word [word]{... */ 4842 if (command->argv /* word [word]{... */
4833 || ctx->word.length /* word{... */ 4843 || ctx->word.length /* word{... */
4834 || ctx->word.has_quoted_part /* ""{... */ 4844 || ctx->word.has_quoted_part /* ""{... */
4835 ) { 4845 )
4836 syntax_error(NULL);
4837 debug_printf_parse("parse_group return -1: " 4846 debug_printf_parse("parse_group return -1: "
4838 "syntax error, groups and arglists don't mix\n"); 4847 "syntax error, groups and arglists don't mix\n");
4839 return -1;
4840 }
4841#endif 4848#endif
4842 4849
4843 IF_HUSH_FUNCTIONS(skip:) 4850 IF_HUSH_FUNCTIONS(skip:)
diff --git a/shell/hush_test/hush-redir/redir_and_constructs1.right b/shell/hush_test/hush-redir/redir_and_constructs1.right
new file mode 100644
index 000000000..232cd8734
--- /dev/null
+++ b/shell/hush_test/hush-redir/redir_and_constructs1.right
@@ -0,0 +1,2 @@
1hush: can't execute '!': No such file or directory
2127:127
diff --git a/shell/hush_test/hush-redir/redir_and_constructs1.tests b/shell/hush_test/hush-redir/redir_and_constructs1.tests
new file mode 100755
index 000000000..a92731e04
--- /dev/null
+++ b/shell/hush_test/hush-redir/redir_and_constructs1.tests
@@ -0,0 +1,2 @@
1# Reserved words are not recognized after redirects
2</dev/null ! true; echo 127:$?