hush: fix job control with eval /bin/external_prog
hush: fix parsing of unterminated "str with no EOL hush: improved make_string() (smaller, faster, needs less RAM) hush: renamed several functions
This commit is contained in:
		
							
								
								
									
										185
									
								
								shell/hush.c
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								shell/hush.c
									
									
									
									
									
								
							| @@ -117,6 +117,9 @@ extern char **environ; /* This is in <unistd.h>, but protected with __USE_GNU */ | ||||
| #define DEBUG_EXPAND 1 | ||||
| #endif | ||||
|  | ||||
| /* Keep unconditionally on for now */ | ||||
| #define ENABLE_HUSH_DEBUG 1 | ||||
|  | ||||
| #ifndef debug_printf_clean | ||||
| /* broken, of course, but OK for testing */ | ||||
| static const char *indenter(int i) | ||||
| @@ -497,13 +500,12 @@ static int process_command_subs(o_string *dest, struct p_context *ctx, struct in | ||||
| #endif | ||||
| static int parse_group(o_string *dest, struct p_context *ctx, struct in_str *input, int ch); | ||||
| static const char *lookup_param(const char *src); | ||||
| static char *make_string(char **inp); | ||||
| static int handle_dollar(o_string *dest, struct p_context *ctx, struct in_str *input); | ||||
| static int parse_stream(o_string *dest, struct p_context *ctx, struct in_str *input0, const char *end_trigger); | ||||
| /*   setup: */ | ||||
| static int parse_stream_outer(struct in_str *inp, int parse_flag); | ||||
| static int parse_string_outer(const char *s, int parse_flag); | ||||
| static int parse_file_outer(FILE *f); | ||||
| static int parse_and_run_stream(struct in_str *inp, int parse_flag); | ||||
| static int parse_and_run_string(const char *s, int parse_flag); | ||||
| static int parse_and_run_file(FILE *f); | ||||
| /*   job management: */ | ||||
| static int checkjobs(struct pipe* fg_pipe); | ||||
| #if ENABLE_HUSH_JOB | ||||
| @@ -515,9 +517,11 @@ static void delete_finished_bg_job(struct pipe *pi); | ||||
| int checkjobs_and_fg_shell(struct pipe* fg_pipe); /* never called */ | ||||
| #endif | ||||
| /*     local variable support */ | ||||
| static char **expand_variables_to_list(char **argv); | ||||
| static char **expand_strvec_to_strvec(char **argv); | ||||
| /* used for eval */ | ||||
| static char *expand_strvec_to_string(char **argv); | ||||
| /* used for expansion of right hand of assignments */ | ||||
| static char *expand_variables_to_string(const char *str); | ||||
| static char *expand_string_to_string(const char *str); | ||||
| static const char *get_local_var(const char *var); | ||||
| static int set_local_var(const char *s, int flg_export); | ||||
| static void unset_local_var(const char *name); | ||||
| @@ -716,12 +720,11 @@ static const char *set_cwd(void) | ||||
| /* built-in 'eval' handler */ | ||||
| static int builtin_eval(char **argv) | ||||
| { | ||||
| 	char *str = NULL; | ||||
| 	int rcode = EXIT_SUCCESS; | ||||
|  | ||||
| 	if (argv[1]) { | ||||
| 		str = make_string(argv + 1); | ||||
| 		parse_string_outer(str, PARSEFLAG_EXIT_FROM_LOOP | | ||||
| 		char *str = expand_strvec_to_string(argv + 1); | ||||
| 		parse_and_run_string(str, PARSEFLAG_EXIT_FROM_LOOP | | ||||
| 					PARSEFLAG_SEMICOLON); | ||||
| 		free(str); | ||||
| 		rcode = last_return_code; | ||||
| @@ -732,9 +735,9 @@ static int builtin_eval(char **argv) | ||||
| /* built-in 'cd <path>' handler */ | ||||
| static int builtin_cd(char **argv) | ||||
| { | ||||
| 	char *newdir; | ||||
| 	const char *newdir; | ||||
| 	if (argv[1] == NULL) | ||||
| 		newdir = getenv("HOME"); | ||||
| 		newdir = getenv("HOME") ? : "/"; | ||||
| 	else | ||||
| 		newdir = argv[1]; | ||||
| 	if (chdir(newdir)) { | ||||
| @@ -999,7 +1002,7 @@ static int builtin_source(char **argv) | ||||
| 	 * (pointer only is OK!) on this stack frame, | ||||
| 	 * set global_argv=argv+1, recurse, and restore. */ | ||||
| 	mark_open(fileno(input)); | ||||
| 	status = parse_file_outer(input); | ||||
| 	status = parse_and_run_file(input); | ||||
| 	mark_closed(fileno(input)); | ||||
| 	fclose(input); | ||||
| 	return status; | ||||
| @@ -1146,12 +1149,10 @@ static void get_user_input(struct in_str *i) | ||||
|  | ||||
| 	prompt_str = setup_prompt_string(i->promptmode); | ||||
| #if ENABLE_FEATURE_EDITING | ||||
| 	/* | ||||
| 	 ** enable command line editing only while a command line | ||||
| 	 ** is actually being read; otherwise, we'll end up bequeathing | ||||
| 	 ** atexit() handlers and other unwanted stuff to our | ||||
| 	 ** child processes (rob@sysgo.de) | ||||
| 	 */ | ||||
| 	/* Enable command line editing only while a command line | ||||
| 	 * is actually being read; otherwise, we'll end up bequeathing | ||||
| 	 * atexit() handlers and other unwanted stuff to our | ||||
| 	 * child processes (rob@sysgo.de) */ | ||||
| 	r = read_line_input(prompt_str, user_input_buf, BUFSIZ-1, line_input_state); | ||||
| 	i->eof_flag = (r < 0); | ||||
| 	if (i->eof_flag) { /* EOF/error detected */ | ||||
| @@ -1343,8 +1344,8 @@ static void pseudo_exec_argv(char **argv) | ||||
| 		debug_printf_exec("pid %d environment modification: %s\n", | ||||
| 				getpid(), argv[i]); | ||||
| // FIXME: vfork case?? | ||||
| 		p = expand_variables_to_string(argv[i]); | ||||
| 		putenv(p == argv[i] ? xstrdup(p) : p); | ||||
| 		p = expand_string_to_string(argv[i]); | ||||
| 		putenv(p); | ||||
| 	} | ||||
| 	argv += i; | ||||
| 	/* If a variable is assigned in a forest, and nobody listens, | ||||
| @@ -1354,7 +1355,7 @@ static void pseudo_exec_argv(char **argv) | ||||
| 		_exit(EXIT_SUCCESS); | ||||
| 	} | ||||
|  | ||||
| 	argv = expand_variables_to_list(argv); | ||||
| 	argv = expand_strvec_to_strvec(argv); | ||||
|  | ||||
| 	/* | ||||
| 	 * Check if the command matches any of the builtins. | ||||
| @@ -1652,8 +1653,8 @@ static int checkjobs_and_fg_shell(struct pipe* fg_pipe) | ||||
| 	pid_t p; | ||||
| 	int rcode = checkjobs(fg_pipe); | ||||
| 	/* Job finished, move the shell to the foreground */ | ||||
| 	p = getpgid(0); | ||||
| 	debug_printf("fg'ing ourself: getpgid(0)=%d\n", (int)p); | ||||
| 	p = getpgid(0); /* pgid of our process */ | ||||
| 	debug_printf_jobs("fg'ing ourself: getpgid(0)=%d\n", (int)p); | ||||
| 	if (tcsetpgrp(interactive_fd, p) && errno != ENOTTY) | ||||
| 		bb_perror_msg("tcsetpgrp-4a"); | ||||
| 	return rcode; | ||||
| @@ -1675,6 +1676,9 @@ static int checkjobs_and_fg_shell(struct pipe* fg_pipe) | ||||
|  * subshell, when that is in fact necessary.  The subshell process | ||||
|  * now has its stdout directed to the input of the appropriate pipe, | ||||
|  * so this routine is noticeably simpler. | ||||
|  * | ||||
|  * Returns -1 only if started some children. IOW: we have to | ||||
|  * mask out retvals of builtins etc with 0xff! | ||||
|  */ | ||||
| static int run_pipe_real(struct pipe *pi) | ||||
| { | ||||
| @@ -1710,7 +1714,7 @@ static int run_pipe_real(struct pipe *pi) | ||||
| 		rcode = run_list_real(child->group); | ||||
| 		restore_redirects(squirrel); | ||||
| 		debug_printf_exec("run_pipe_real return %d\n", rcode); | ||||
| 		return rcode; | ||||
| 		return rcode; // do we need to add '... & 0xff' ? | ||||
| 	} | ||||
|  | ||||
| 	if (single_fg && child->argv != NULL) { | ||||
| @@ -1739,21 +1743,16 @@ static int run_pipe_real(struct pipe *pi) | ||||
| 					export_me = 1; | ||||
| 				} | ||||
| 				free(name); | ||||
| 				p = expand_variables_to_string(argv[i]); | ||||
| 				p = expand_string_to_string(argv[i]); | ||||
| 				set_local_var(p, export_me); | ||||
| 				if (p != argv[i]) | ||||
| 					free(p); | ||||
| 				free(p); | ||||
| 			} | ||||
| 			return EXIT_SUCCESS;   /* don't worry about errors in set_local_var() yet */ | ||||
| 		} | ||||
| 		for (i = 0; is_assignment(argv[i]); i++) { | ||||
| 			p = expand_variables_to_string(argv[i]); | ||||
| 			if (p != argv[i]) { | ||||
| 				//sp: child->sp--; | ||||
| 				putenv(p); | ||||
| 			} else { | ||||
| 				putenv(xstrdup(p)); | ||||
| 			} | ||||
| 			p = expand_string_to_string(argv[i]); | ||||
| 			//sp: child->sp--; | ||||
| 			putenv(p); | ||||
| 		} | ||||
| 		for (x = bltins; x->cmd; x++) { | ||||
| 			if (strcmp(argv[i], x->cmd) == 0) { | ||||
| @@ -1770,8 +1769,8 @@ static int run_pipe_real(struct pipe *pi) | ||||
| 				setup_redirects(child, squirrel); | ||||
| 				debug_printf_exec(": builtin '%s' '%s'...\n", x->cmd, argv[i+1]); | ||||
| 				//sp: if (child->sp) /* btw we can do it unconditionally... */ | ||||
| 				argv_expanded = expand_variables_to_list(argv + i); | ||||
| 				rcode = x->function(argv_expanded); | ||||
| 				argv_expanded = expand_strvec_to_strvec(argv + i); | ||||
| 				rcode = x->function(argv_expanded) & 0xff; | ||||
| 				free(argv_expanded); | ||||
| 				restore_redirects(squirrel); | ||||
| 				debug_printf_exec("run_pipe_real return %d\n", rcode); | ||||
| @@ -1786,9 +1785,9 @@ static int run_pipe_real(struct pipe *pi) | ||||
| 				save_nofork_data(&nofork_save); | ||||
| 				argv_expanded = argv + i; | ||||
| 				//sp: if (child->sp) | ||||
| 				argv_expanded = expand_variables_to_list(argv + i); | ||||
| 				argv_expanded = expand_strvec_to_strvec(argv + i); | ||||
| 				debug_printf_exec(": run_nofork_applet '%s' '%s'...\n", argv_expanded[0], argv_expanded[1]); | ||||
| 				rcode = run_nofork_applet_prime(&nofork_save, a, argv_expanded); | ||||
| 				rcode = run_nofork_applet_prime(&nofork_save, a, argv_expanded) & 0xff; | ||||
| 				free(argv_expanded); | ||||
| 				restore_redirects(squirrel); | ||||
| 				debug_printf_exec("run_pipe_real return %d\n", rcode); | ||||
| @@ -1832,7 +1831,7 @@ static int run_pipe_real(struct pipe *pi) | ||||
| 			/* Every child adds itself to new process group | ||||
| 			 * with pgid == pid of first child in pipe */ | ||||
| #if ENABLE_HUSH_JOB | ||||
| 			if (interactive_fd) { | ||||
| 			if (run_list_level == 1 && interactive_fd) { | ||||
| 				/* Don't do pgrp restore anymore on fatal signals */ | ||||
| 				set_fatal_sighandler(SIG_DFL); | ||||
| 				if (pi->pgrp < 0) /* true for 1st process only */ | ||||
| @@ -2078,7 +2077,7 @@ static int run_list_real(struct pipe *pi) | ||||
| 				if (!pi->next->progs->argv) | ||||
| 					continue; | ||||
| 				/* create list of variable values */ | ||||
| 				for_list = expand_variables_to_list(pi->next->progs->argv); | ||||
| 				for_list = expand_strvec_to_strvec(pi->next->progs->argv); | ||||
| 				for_lcur = for_list; | ||||
| 				for_varname = pi->progs->argv[0]; | ||||
| 				pi->progs->argv[0] = NULL; | ||||
| @@ -2122,24 +2121,24 @@ static int run_list_real(struct pipe *pi) | ||||
| 			 * of run_pipe_real(), and we don't need to wait for anything. */ | ||||
| 		} else if (pi->followup == PIPE_BG) { | ||||
| 			/* What does bash do with attempts to background builtins? */ | ||||
|  | ||||
| 			/* Even bash 3.2 doesn't do that well with nested bg: | ||||
| 			 * try "{ { sleep 10; echo DEEP; } & echo HERE; } &". | ||||
| 			 * I'm considering NOT treating inner bgs as jobs - | ||||
| 			 * thus maybe "if (run_list_level == 1 && pi->followup == PIPE_BG)" | ||||
| 			 * above? */ | ||||
| 			 * I'm NOT treating inner &'s as jobs */ | ||||
| #if ENABLE_HUSH_JOB | ||||
| 			insert_bg_job(pi); | ||||
| 			if (run_list_level == 1)  | ||||
| 				insert_bg_job(pi); | ||||
| #endif | ||||
| 			rcode = EXIT_SUCCESS; | ||||
| 		} else { | ||||
| #if ENABLE_HUSH_JOB | ||||
| 			/* Paranoia, just "interactive_fd" should be enough */ | ||||
| 			/* Paranoia, just "interactive_fd" should be enough? */ | ||||
| 			if (run_list_level == 1 && interactive_fd) { | ||||
| 				/* waits for completion, then fg's main shell */ | ||||
| 				rcode = checkjobs_and_fg_shell(pi); | ||||
| 			} else | ||||
| #endif | ||||
| 			{ | ||||
| 				/* this one just waits for completion */ | ||||
| 				rcode = checkjobs(pi); | ||||
| 			} | ||||
| 			debug_printf_exec(": checkjobs returned %d\n", rcode); | ||||
| @@ -2343,7 +2342,7 @@ static int xglob(o_string *dest, int flags, glob_t *pglob) | ||||
| 	return gr; | ||||
| } | ||||
|  | ||||
| /* expand_variables_to_list() takes a list of strings, expands | ||||
| /* expand_strvec_to_strvec() takes a list of strings, expands | ||||
|  * all variable references within and returns a pointer to | ||||
|  * a list of expanded strings, possibly with larger number | ||||
|  * of strings. (Think VAR="a b"; echo $VAR). | ||||
| @@ -2629,29 +2628,51 @@ static char **expand_variables(char **argv, char or_mask) | ||||
| 		debug_printf_expand("used_space=%d\n", pos - (char*)list); | ||||
| 	} | ||||
| #endif | ||||
| 	/* To be removed / made conditional later. */ | ||||
| 	if (pos - (char*)list > len) | ||||
| 		bb_error_msg_and_die("BUG in varexp"); | ||||
| 	if (ENABLE_HUSH_DEBUG) | ||||
| 		if (pos - (char*)list > len) | ||||
| 			bb_error_msg_and_die("BUG in varexp"); | ||||
| 	return list; | ||||
| } | ||||
|  | ||||
| static char **expand_variables_to_list(char **argv) | ||||
| static char **expand_strvec_to_strvec(char **argv) | ||||
| { | ||||
| 	return expand_variables(argv, 0); | ||||
| } | ||||
|  | ||||
| static char *expand_variables_to_string(const char *str) | ||||
| static char *expand_string_to_string(const char *str) | ||||
| { | ||||
| 	char *argv[2], **list; | ||||
|  | ||||
| 	argv[0] = (char*)str; | ||||
| 	argv[1] = NULL; | ||||
| 	list = expand_variables(argv, 0x80); /* 0x80: make one-element expansion */ | ||||
| 	/* To be removed / made conditional later. */ | ||||
| 	if (!list[0] || list[1]) | ||||
| 		bb_error_msg_and_die("BUG in varexp"); | ||||
| 	if (ENABLE_HUSH_DEBUG) | ||||
| 		if (!list[0] || list[1]) | ||||
| 			bb_error_msg_and_die("BUG in varexp2"); | ||||
| 	/* actually, just move string 2*sizeof(char*) bytes back */ | ||||
| 	strcpy((char*)list, list[0]); | ||||
| 	debug_printf_expand("string_to_string='%s'\n", (char*)list); | ||||
| 	return (char*)list; | ||||
| } | ||||
|  | ||||
| static char* expand_strvec_to_string(char **argv) | ||||
| { | ||||
| 	char **list; | ||||
|  | ||||
| 	list = expand_variables(argv, 0x80); | ||||
| 	/* Convert all NULs to spaces */ | ||||
| 	if (list[0]) { | ||||
| 		int n = 1; | ||||
| 		while (list[n]) { | ||||
| 			if (ENABLE_HUSH_DEBUG) | ||||
| 				if (list[n-1] + strlen(list[n-1]) + 1 != list[n]) | ||||
| 					bb_error_msg_and_die("BUG in varexp3"); | ||||
| 			list[n][-1] = ' '; /* TODO: or to ifs[0]? */ | ||||
| 			n++; | ||||
| 		} | ||||
| 	} | ||||
| 	strcpy((char*)list, list[0]); | ||||
| 	debug_printf_expand("strvec_to_string='%s'\n", (char*)list); | ||||
| 	return (char*)list; | ||||
| } | ||||
|  | ||||
| @@ -2713,9 +2734,9 @@ static int set_local_var(const char *s, int flg_export) | ||||
| 	} | ||||
|  | ||||
| 	cur = xzalloc(sizeof(*cur)); | ||||
| 	/*cur->next = 0;*/ | ||||
| 	cur->name = xstrdup(name); | ||||
| 	cur->value = xstrdup(value); | ||||
| 	/*cur->next = 0;*/ | ||||
| 	cur->flg_export = flg_export; | ||||
| 	/*cur->flg_read_only = 0;*/ | ||||
| 	{ | ||||
| @@ -3242,31 +3263,6 @@ static const char *lookup_param(const char *src) | ||||
| 	return p; | ||||
| } | ||||
|  | ||||
| /* Make new string for parser */ | ||||
| static char* make_string(char **inp) | ||||
| { | ||||
| 	char *p; | ||||
| 	char *str = NULL; | ||||
| 	int n; | ||||
| 	int val_len; | ||||
| 	int len = 0; | ||||
|  | ||||
| 	for (n = 0; inp[n]; n++) { | ||||
| 		p = expand_variables_to_string(inp[n]); | ||||
| 		val_len = strlen(p); | ||||
| 		str = xrealloc(str, len + val_len + 3); /* +3: space, '\n', <nul>*/ | ||||
| 		str[len++] = ' '; | ||||
| 		strcpy(str + len, p); | ||||
| 		len += val_len; | ||||
| 		if (p != inp[n]) free(p); | ||||
| 	} | ||||
| 	/* We do not check for case where loop had no iterations at all | ||||
| 	 * - cannot happen? */ | ||||
| 	str[len] = '\n'; | ||||
| 	str[len+1] = '\0'; | ||||
| 	return str; | ||||
| } | ||||
|  | ||||
| /* return code: 0 for OK, 1 for syntax error */ | ||||
| static int handle_dollar(o_string *dest, struct p_context *ctx, struct in_str *input) | ||||
| { | ||||
| @@ -3358,9 +3354,9 @@ static int parse_stream(o_string *dest, struct p_context *ctx, | ||||
| 	debug_printf_parse("parse_stream entered, end_trigger='%s'\n", end_trigger); | ||||
|  | ||||
| 	while (1) { | ||||
| 		ch = b_getch(input); | ||||
| 		m = CHAR_IFS; | ||||
| 		next = '\0'; | ||||
| 		ch = b_getch(input); | ||||
| 		if (ch != EOF) { | ||||
| 			m = charmap[ch]; | ||||
| 			if (ch != '\n') | ||||
| @@ -3371,6 +3367,11 @@ static int parse_stream(o_string *dest, struct p_context *ctx, | ||||
| 		if (m == CHAR_ORDINARY | ||||
| 		 || (m != CHAR_SPECIAL && dest->quote) | ||||
| 		) { | ||||
| 			if (ch == EOF) { | ||||
| 				syntax(); | ||||
| 				debug_printf_parse("parse_stream return 1: unterminated \"\n"); | ||||
| 				return 1; | ||||
| 			} | ||||
| 			b_addqchr(dest, ch, dest->quote); | ||||
| 			continue; | ||||
| 		} | ||||
| @@ -3564,8 +3565,8 @@ static void update_charmap(void) | ||||
| } | ||||
|  | ||||
| /* most recursion does not come through here, the exception is | ||||
|  * from builtin_source() */ | ||||
| static int parse_stream_outer(struct in_str *inp, int parse_flag) | ||||
|  * from builtin_source() and builtin_eval() */ | ||||
| static int parse_and_run_stream(struct in_str *inp, int parse_flag) | ||||
| { | ||||
| 	struct p_context ctx; | ||||
| 	o_string temp = NULL_O_STRING; | ||||
| @@ -3607,19 +3608,19 @@ static int parse_stream_outer(struct in_str *inp, int parse_flag) | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| static int parse_string_outer(const char *s, int parse_flag) | ||||
| static int parse_and_run_string(const char *s, int parse_flag) | ||||
| { | ||||
| 	struct in_str input; | ||||
| 	setup_string_in_str(&input, s); | ||||
| 	return parse_stream_outer(&input, parse_flag); | ||||
| 	return parse_and_run_stream(&input, parse_flag); | ||||
| } | ||||
|  | ||||
| static int parse_file_outer(FILE *f) | ||||
| static int parse_and_run_file(FILE *f) | ||||
| { | ||||
| 	int rcode; | ||||
| 	struct in_str input; | ||||
| 	setup_file_in_str(&input, f); | ||||
| 	rcode = parse_stream_outer(&input, PARSEFLAG_SEMICOLON); | ||||
| 	rcode = parse_and_run_stream(&input, PARSEFLAG_SEMICOLON); | ||||
| 	return rcode; | ||||
| } | ||||
|  | ||||
| @@ -3698,7 +3699,7 @@ int hush_main(int argc, char **argv) | ||||
| 		input = fopen("/etc/profile", "r"); | ||||
| 		if (input != NULL) { | ||||
| 			mark_open(fileno(input)); | ||||
| 			parse_file_outer(input); | ||||
| 			parse_and_run_file(input); | ||||
| 			mark_closed(fileno(input)); | ||||
| 			fclose(input); | ||||
| 		} | ||||
| @@ -3710,7 +3711,7 @@ int hush_main(int argc, char **argv) | ||||
| 		case 'c': | ||||
| 			global_argv = argv + optind; | ||||
| 			global_argc = argc - optind; | ||||
| 			opt = parse_string_outer(optarg, PARSEFLAG_SEMICOLON); | ||||
| 			opt = parse_and_run_string(optarg, PARSEFLAG_SEMICOLON); | ||||
| 			goto final_return; | ||||
| 		case 'i': | ||||
| 			/* Well, we cannot just declare interactiveness, | ||||
| @@ -3791,7 +3792,7 @@ int hush_main(int argc, char **argv) | ||||
| #endif | ||||
|  | ||||
| 	if (argv[optind] == NULL) { | ||||
| 		opt = parse_file_outer(stdin); | ||||
| 		opt = parse_and_run_file(stdin); | ||||
| 		goto final_return; | ||||
| 	} | ||||
|  | ||||
| @@ -3799,7 +3800,7 @@ int hush_main(int argc, char **argv) | ||||
| 	global_argv = argv + optind; | ||||
| 	global_argc = argc - optind; | ||||
| 	input = xfopen(argv[optind], "r"); | ||||
| 	opt = parse_file_outer(input); | ||||
| 	opt = parse_and_run_file(input); | ||||
|  | ||||
| #if ENABLE_FEATURE_CLEAN_UP | ||||
| 	fclose(input); | ||||
|   | ||||
							
								
								
									
										1
									
								
								shell/hush_test/hush-bugs/noeol3.right
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								shell/hush_test/hush-bugs/noeol3.right
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| hush: syntax error hush.c:3370 | ||||
							
								
								
									
										2
									
								
								shell/hush_test/hush-bugs/noeol3.tests
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								shell/hush_test/hush-bugs/noeol3.tests
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| # last line has no EOL! | ||||
| echo "unterminated | ||||
		Reference in New Issue
	
	Block a user