*
* TODOs:
* grep for "TODO" and fix (some of them are easy)
+ * make complex ${var%...} constructs support optional
+ * make here documents optional
* special variables (done: PWD, PPID, RANDOM)
+ * follow IFS rules more precisely, including update semantics
* tilde expansion
* aliases
- * follow IFS rules more precisely, including update semantics
* builtins mandated by standards we don't support:
- * [un]alias, command, fc, getopts, newgrp, readonly, times
- * make complex ${var%...} constructs support optional
- * make here documents optional
+ * [un]alias, command, fc, getopts, times:
+ * command -v CMD: print "/path/to/CMD"
+ * prints "CMD" for builtins
+ * prints "alias ALIAS='EXPANSION'" for aliases
+ * prints nothing and sets $? to 1 if not found
+ * command -V CMD: print "CMD is /path/CMD|a shell builtin|etc"
+ * command [-p] CMD: run CMD, even if a function CMD also exists
+ * (can use this to override standalone shell as well)
+ * -p: use default $PATH
+ * command BLTIN: disables special-ness (e.g. errors do not abort)
+ * getopts: getopt() for shells
+ * times: print getrusage(SELF/CHILDREN).ru_utime/ru_stime
+ * fc -l[nr] [BEG] [END]: list range of commands in history
+ * fc [-e EDITOR] [BEG] [END]: edit/rerun range of commands
+ * fc -s [PAT=REP] [CMD]: rerun CMD, replacing PAT with REP
*
* Bash compat TODO:
* redirection of stdout+stderr: &> and >&
* The EXPR is evaluated according to ARITHMETIC EVALUATION.
* This is exactly equivalent to let "EXPR".
* $[EXPR]: synonym for $((EXPR))
+ * indirect expansion: ${!VAR}
+ * substring op on @: ${@:n:m}
*
* Won't do:
+ * Some builtins mandated by standards:
+ * newgrp [GRP]: not a builtin in bash but a suid binary
+ * which spawns a new shell with new group ID
* In bash, export builtin is special, its arguments are assignments
* and therefore expansion of them should be "one-word" expansion:
* $ export i=`echo 'a b'` # export has one arg: "i=a b"
//config: help
//config: export -n unexports variables. It is a bash extension.
//config:
+//config:config HUSH_READONLY
+//config: bool "readonly builtin"
+//config: default y
+//config: help
+//config: Enable support for read-only variables.
+//config:
//config:config HUSH_KILL
//config: bool "kill builtin (supports kill %jobspec)"
//config: default y
* therefore we don't show them either.
*/
//usage:#define hush_trivial_usage
-//usage: "[-nxl] [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]"
+//usage: "[-enxl] [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]"
//usage:#define hush_full_usage "\n\n"
//usage: "Unix shell interpreter"
static const char o_opt_strings[] ALIGN1 =
"pipefail\0"
"noexec\0"
+ "errexit\0"
#if ENABLE_HUSH_MODE_X
"xtrace\0"
#endif
enum {
OPT_O_PIPEFAIL,
OPT_O_NOEXEC,
+ OPT_O_ERREXIT,
#if ENABLE_HUSH_MODE_X
OPT_O_XTRACE,
#endif
#else
# define G_saved_tty_pgrp 0
#endif
+ /* How deeply are we in context where "set -e" is ignored */
+ int errexit_depth;
+ /* "set -e" rules (do we follow them correctly?):
+ * Exit if pipe, list, or compound command exits with a non-zero status.
+ * Shell does not exit if failed command is part of condition in
+ * if/while, part of && or || list except the last command, any command
+ * in a pipe but the last, or if the command's return value is being
+ * inverted with !. If a compound command other than a subshell returns a
+ * non-zero status because a command failed while -e was being ignored, the
+ * shell does not exit. A trap on ERR, if set, is executed before the shell
+ * exits [ERR is a bashism].
+ *
+ * If a compound command or function executes in a context where -e is
+ * ignored, none of the commands executed within are affected by the -e
+ * setting. If a compound command or function sets -e while executing in a
+ * context where -e is ignored, that setting does not have any effect until
+ * the compound command or the command containing the function call completes.
+ */
+
char o_opt[NUM_OPT_O];
#if ENABLE_HUSH_MODE_X
# define G_x_mode (G.o_opt[OPT_O_XTRACE])
#if ENABLE_HUSH_EXPORT
static int builtin_export(char **argv) FAST_FUNC;
#endif
+#if ENABLE_HUSH_READONLY
+static int builtin_readonly(char **argv) FAST_FUNC;
+#endif
#if ENABLE_HUSH_JOB
static int builtin_fg_bg(char **argv) FAST_FUNC;
static int builtin_jobs(char **argv) FAST_FUNC;
#if ENABLE_HUSH_READ
BLTIN("read" , builtin_read , "Input into variable"),
#endif
+#if ENABLE_HUSH_READONLY
+ BLTIN("readonly" , builtin_readonly, "Make variables read-only"),
+#endif
#if ENABLE_HUSH_FUNCTIONS
BLTIN("return" , builtin_return , "Return from function"),
#endif
* -1: clear export flag and unsetenv the variable
* flg_read_only is set only when we handle -R var=val
*/
-#if !BB_MMU && ENABLE_HUSH_LOCAL
-/* all params are used */
-#elif BB_MMU && ENABLE_HUSH_LOCAL
-#define set_local_var(str, flg_export, local_lvl, flg_read_only) \
- set_local_var(str, flg_export, local_lvl)
-#elif BB_MMU && !ENABLE_HUSH_LOCAL
-#define set_local_var(str, flg_export, local_lvl, flg_read_only) \
- set_local_var(str, flg_export)
-#elif !BB_MMU && !ENABLE_HUSH_LOCAL
-#define set_local_var(str, flg_export, local_lvl, flg_read_only) \
- set_local_var(str, flg_export, flg_read_only)
-#endif
-static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_only)
+static int set_local_var(char *str,
+ int flg_export UNUSED_PARAM,
+ int local_lvl UNUSED_PARAM,
+ int flg_read_only UNUSED_PARAM)
{
struct variable **var_pp;
struct variable *cur;
/* We found an existing var with this name */
if (cur->flg_read_only) {
-#if !BB_MMU
if (!flg_read_only)
-#endif
bb_error_msg("%s: readonly variable", str);
free(str);
return -1;
set_str_and_exp:
cur->varstr = str;
-#if !BB_MMU
- cur->flg_read_only = flg_read_only;
-#endif
exp:
+#if !BB_MMU || ENABLE_HUSH_READONLY
+ if (flg_read_only != 0) {
+ cur->flg_read_only = flg_read_only;
+ }
+#endif
if (flg_export == 1)
cur->flg_export = 1;
if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S')
debug_printf_parse("done_pipe entered, followup %d\n", type);
/* Close previous command */
not_null = done_command(ctx);
- ctx->pipe->followup = type;
#if HAS_KEYWORDS
ctx->pipe->pi_inverted = ctx->ctx_inverted;
ctx->ctx_inverted = 0;
ctx->pipe->res_word = ctx->ctx_res_w;
#endif
+ if (type == PIPE_BG && ctx->list_head != ctx->pipe) {
+ /* Necessary since && and || have precedence over &:
+ * "cmd1 && cmd2 &" must spawn both cmds, not only cmd2,
+ * in a backgrounded subshell.
+ */
+ struct pipe *pi;
+ struct command *command;
+
+ /* Is this actually this construct, all pipes end with && or ||? */
+ pi = ctx->list_head;
+ while (pi != ctx->pipe) {
+ if (pi->followup != PIPE_AND && pi->followup != PIPE_OR)
+ goto no_conv;
+ pi = pi->next;
+ }
+
+ debug_printf_parse("BG with more than one pipe, converting to { p1 &&...pN; } &\n");
+ pi->followup = PIPE_SEQ; /* close pN _not_ with "&"! */
+ pi = xzalloc(sizeof(*pi));
+ pi->followup = PIPE_BG;
+ pi->num_cmds = 1;
+ pi->cmds = xzalloc(sizeof(pi->cmds[0]));
+ command = &pi->cmds[0];
+ if (CMD_NORMAL != 0) /* "if xzalloc didn't do that already" */
+ command->cmd_type = CMD_NORMAL;
+ command->group = ctx->list_head;
+#if !BB_MMU
+ command->group_as_string = xstrndup(
+ ctx->as_string.data,
+ ctx->as_string.length - 1 /* do not copy last char, "&" */
+ );
+#endif
+ /* Replace all pipes in ctx with one newly created */
+ ctx->list_head = ctx->pipe = pi;
+ } else {
+ no_conv:
+ ctx->pipe->followup = type;
+ }
/* Without this check, even just <enter> on command line generates
* tree of three NOPs (!). Which is harmless but annoying.
if (r->flag & FLAG_START) {
struct parse_context *old;
- old = xmalloc(sizeof(*old));
+ old = xmemdup(ctx, sizeof(*ctx));
debug_printf_parse("push stack %p\n", old);
- *old = *ctx; /* physical copy */
initialize_context(ctx);
ctx->stack = old;
} else if (/*ctx->ctx_res_w == RES_NONE ||*/ !(ctx->old_flag & (1 << r->res))) {
* Really, ask yourself, why
* "cmd && <newline>" doesn't start
* cmd but waits for more input?
- * No reason...)
+ * The only reason is that it might be
+ * a "cmd1 && <nl> cmd2 &" construct,
+ * cmd1 may need to run in BG).
*/
struct pipe *pi = ctx.list_head;
if (pi->num_cmds != 0 /* check #1 */
* and it will match } earlier (not here). */
syntax_error_unexpected_ch(ch);
G.last_exitcode = 2;
- goto parse_error1;
+ goto parse_error2;
default:
if (HUSH_DEBUG)
bb_error_msg_and_die("BUG: unexpected %c\n", ch);
parse_error:
G.last_exitcode = 1;
- parse_error1:
+ parse_error2:
{
struct parse_context *pctx;
IF_HAS_KEYWORDS(struct parse_context *p2;)
if (errmsg)
goto arith_err;
debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
- if (len >= 0) { /* bash compat: len < 0 is illegal */
- if (beg < 0) {
- /* negative beg counts from the end */
- beg = (arith_t)strlen(val) + beg;
- if (beg < 0) /* ${v: -999999} is "" */
- beg = len = 0;
- }
- debug_printf_varexp("from val:'%s'\n", val);
- if (len == 0 || !val || beg >= strlen(val)) {
+ if (beg < 0) {
+ /* negative beg counts from the end */
+ beg = (arith_t)strlen(val) + beg;
+ if (beg < 0) /* ${v: -999999} is "" */
+ beg = len = 0;
+ }
+ debug_printf_varexp("from val:'%s'\n", val);
+ if (len < 0) {
+ /* in bash, len=-n means strlen()-n */
+ len = (arith_t)strlen(val) - beg + len;
+ if (len < 0) /* bash compat */
+ die_if_script("%s: substring expression < 0", var);
+ }
+ if (len <= 0 || !val || beg >= strlen(val)) {
arith_err:
- val = NULL;
- } else {
- /* Paranoia. What if user entered 9999999999999
- * which fits in arith_t but not int? */
- if (len >= INT_MAX)
- len = INT_MAX;
- val = to_be_freed = xstrndup(val + beg, len);
- }
- debug_printf_varexp("val:'%s'\n", val);
- } else
-#endif /* HUSH_SUBSTR_EXPANSION && FEATURE_SH_MATH */
- {
- die_if_script("malformed ${%s:...}", var);
val = NULL;
+ } else {
+ /* Paranoia. What if user entered 9999999999999
+ * which fits in arith_t but not int? */
+ if (len >= INT_MAX)
+ len = INT_MAX;
+ val = to_be_freed = xstrndup(val + beg, len);
}
+ debug_printf_varexp("val:'%s'\n", val);
+#else /* not (HUSH_SUBSTR_EXPANSION && FEATURE_SH_MATH) */
+ die_if_script("malformed ${%s:...}", var);
+ val = NULL;
+#endif
} else { /* one of "-=+?" */
/* Standard-mandated substitution ops:
* ${var?word} - indicate error if unset
return pi->cmdtext;
}
-static void insert_bg_job(struct pipe *pi)
+static void remove_job_from_table(struct pipe *pi)
+{
+ struct pipe *prev_pipe;
+
+ if (pi == G.job_list) {
+ G.job_list = pi->next;
+ } else {
+ prev_pipe = G.job_list;
+ while (prev_pipe->next != pi)
+ prev_pipe = prev_pipe->next;
+ prev_pipe->next = pi->next;
+ }
+ G.last_jobid = 0;
+ if (G.job_list)
+ G.last_jobid = G.job_list->jobid;
+}
+
+static void delete_finished_job(struct pipe *pi)
+{
+ remove_job_from_table(pi);
+ free_pipe(pi);
+}
+
+static void clean_up_last_dead_job(void)
+{
+ if (G.job_list && !G.job_list->alive_cmds)
+ delete_finished_job(G.job_list);
+}
+
+static void insert_job_into_table(struct pipe *pi)
{
struct pipe *job, **jobp;
int i;
- /* Linear search for the ID of the job to use */
- pi->jobid = 1;
- for (job = G.job_list; job; job = job->next)
- if (job->jobid >= pi->jobid)
- pi->jobid = job->jobid + 1;
+ clean_up_last_dead_job();
- /* Add job to the list of running jobs */
+ /* Find the end of the list, and find next job ID to use */
+ i = 0;
jobp = &G.job_list;
- while ((job = *jobp) != NULL)
+ while ((job = *jobp) != NULL) {
+ if (job->jobid > i)
+ i = job->jobid;
jobp = &job->next;
- job = *jobp = xmalloc(sizeof(*job));
+ }
+ pi->jobid = i + 1;
- *job = *pi; /* physical copy */
+ /* Create a new job struct at the end */
+ job = *jobp = xmemdup(pi, sizeof(*pi));
job->next = NULL;
job->cmds = xzalloc(sizeof(pi->cmds[0]) * pi->num_cmds);
/* Cannot copy entire pi->cmds[] vector! This causes double frees */
printf("[%u] %u %s\n", job->jobid, (unsigned)job->cmds[0].pid, job->cmdtext);
G.last_jobid = job->jobid;
}
-
-static void remove_bg_job(struct pipe *pi)
-{
- struct pipe *prev_pipe;
-
- if (pi == G.job_list) {
- G.job_list = pi->next;
- } else {
- prev_pipe = G.job_list;
- while (prev_pipe->next != pi)
- prev_pipe = prev_pipe->next;
- prev_pipe->next = pi->next;
- }
- if (G.job_list)
- G.last_jobid = G.job_list->jobid;
- else
- G.last_jobid = 0;
-}
-
-/* Remove a backgrounded job */
-static void delete_finished_bg_job(struct pipe *pi)
-{
- remove_bg_job(pi);
- free_pipe(pi);
-}
#endif /* JOB */
static int job_exited_or_stopped(struct pipe *pi)
if (G_interactive_fd) {
#if ENABLE_HUSH_JOB
if (fg_pipe->alive_cmds != 0)
- insert_bg_job(fg_pipe);
+ insert_job_into_table(fg_pipe);
#endif
return rcode;
}
pi->cmds[i].pid = 0;
pi->alive_cmds--;
if (!pi->alive_cmds) {
- if (G_interactive_fd)
+ if (G_interactive_fd) {
printf(JOB_STATUS_FORMAT, pi->jobid,
"Done", pi->cmdtext);
- delete_finished_bg_job(pi);
-//bash deletes finished jobs from job table only in interactive mode, after "jobs" cmd,
-//or if pid of a new process matches one of the old ones
-//(see cleanup_dead_jobs(), delete_old_job(), J_NOTIFIED in bash source).
-//Testcase script: "(exit 3) & sleep 1; wait %1; echo $?" prints 3 in bash.
+ delete_finished_job(pi);
+ } else {
+/*
+ * bash deletes finished jobs from job table only in interactive mode,
+ * after "jobs" cmd, or if pid of a new process matches one of the old ones
+ * (see cleanup_dead_jobs(), delete_old_job(), J_NOTIFIED in bash source).
+ * Testcase script: "(exit 3) & sleep 1; wait %1; echo $?" prints 3 in bash.
+ * We only retain one "dead" job, if it's the single job on the list.
+ * This covers most of real-world scenarios where this is useful.
+ */
+ if (pi != G.job_list)
+ delete_finished_job(pi);
+ }
}
} else {
/* child stopped */
/* Go through list of pipes, (maybe) executing them. */
for (; pi; pi = IF_HUSH_LOOPS(rword == RES_DONE ? loop_top : ) pi->next) {
int r;
+ int sv_errexit_depth;
if (G.flag_SIGINT)
break;
IF_HAS_KEYWORDS(rword = pi->res_word;)
debug_printf_exec(": rword=%d cond_code=%d last_rword=%d\n",
rword, cond_code, last_rword);
+
+ sv_errexit_depth = G.errexit_depth;
+ if (IF_HAS_KEYWORDS(rword == RES_IF || rword == RES_ELIF ||)
+ pi->followup != PIPE_SEQ
+ ) {
+ G.errexit_depth++;
+ }
#if ENABLE_HUSH_LOOPS
if ((rword == RES_WHILE || rword == RES_UNTIL || rword == RES_FOR)
&& loop_top == NULL /* avoid bumping G.depth_of_loop twice */
* I'm NOT treating inner &'s as jobs */
#if ENABLE_HUSH_JOB
if (G.run_list_level == 1)
- insert_bg_job(pi);
+ insert_job_into_table(pi);
#endif
/* Last command's pid goes to $! */
G.last_bg_pid = pi->cmds[pi->num_cmds - 1].pid;
check_and_run_traps();
}
+ /* Handle "set -e" */
+ if (rcode != 0 && G.o_opt[OPT_O_ERREXIT]) {
+ debug_printf_exec("ERREXIT:1 errexit_depth:%d\n", G.errexit_depth);
+ if (G.errexit_depth == 0)
+ hush_exit(rcode);
+ }
+ G.errexit_depth = sv_errexit_depth;
+
/* Analyze how result affects subsequent commands */
#if ENABLE_HUSH_IF
if (rword == RES_IF || rword == RES_ELIF)
G.o_opt[idx] = state;
break;
}
+ case 'e':
+ G.o_opt[OPT_O_ERREXIT] = state;
+ break;
default:
return EXIT_FAILURE;
}
flags = (argv[0] && argv[0][0] == '-') ? OPT_login : 0;
builtin_argc = 0;
while (1) {
- opt = getopt(argc, argv, "+c:xinsl"
+ opt = getopt(argc, argv, "+c:exinsl"
#if !BB_MMU
"<:$:R:V:"
# if ENABLE_HUSH_FUNCTIONS
#endif
case 'n':
case 'x':
+ case 'e':
if (set_mode(1, opt, NULL) == 0) /* no error */
break;
default:
}
#endif
-#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_LOCAL
-# if !ENABLE_HUSH_LOCAL
-#define helper_export_local(argv, exp, lvl) \
- helper_export_local(argv, exp)
-# endif
-static void helper_export_local(char **argv, int exp, int lvl)
+#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_LOCAL || ENABLE_HUSH_READONLY
+static int helper_export_local(char **argv,
+ int exp UNUSED_PARAM,
+ int ro UNUSED_PARAM,
+ int lvl UNUSED_PARAM)
{
do {
char *name = *argv;
}
}
# if ENABLE_HUSH_LOCAL
- if (exp == 0 /* local? */
+ if (exp == 0 && ro == 0 /* local? */
&& var && var->func_nest_level == lvl
) {
/* "local x=abc; ...; local x" - ignore second local decl */
* bash does not put it in environment,
* but remembers that it is exported,
* and does put it in env when it is set later.
- * We just set it to "" and export. */
+ * We just set it to "" and export.
+ */
/* Or, it's "local NAME" (without =VALUE).
- * bash sets the value to "". */
+ * bash sets the value to "".
+ */
+ /* Or, it's "readonly NAME" (without =VALUE).
+ * bash remembers NAME and disallows its creation
+ * in the future.
+ */
name = xasprintf("%s=", name);
} else {
/* (Un)exporting/making local NAME=VALUE */
name = xstrdup(name);
}
- set_local_var(name, /*exp:*/ exp, /*lvl:*/ lvl, /*ro:*/ 0);
+ set_local_var(name, /*exp:*/ exp, /*lvl:*/ lvl, /*ro:*/ ro);
} while (*++argv);
+ return EXIT_SUCCESS;
}
#endif
return EXIT_SUCCESS;
}
- helper_export_local(argv, (opt_unexport ? -1 : 1), 0);
-
- return EXIT_SUCCESS;
+ return helper_export_local(argv, /*exp:*/ (opt_unexport ? -1 : 1), /*ro:*/ 0, /*lvl:*/ 0);
}
#endif
bb_error_msg("%s: not in a function", argv[0]);
return EXIT_FAILURE; /* bash compat */
}
- helper_export_local(argv, 0, G.func_nest_level);
- return EXIT_SUCCESS;
+ argv++;
+ return helper_export_local(argv, /*exp:*/ 0, /*ro:*/ 0, /*lvl:*/ G.func_nest_level);
+}
+#endif
+
+#if ENABLE_HUSH_READONLY
+static int FAST_FUNC builtin_readonly(char **argv)
+{
+ if (*++argv == NULL) {
+ /* bash: readonly [-p]: list all readonly VARs
+ * (-p has no effect in bash)
+ */
+ struct variable *e;
+ for (e = G.top_var; e; e = e->next) {
+ if (e->flg_read_only) {
+//TODO: quote value: readonly VAR='VAL'
+ printf("readonly %s\n", e->varstr);
+ }
+ }
+ return EXIT_SUCCESS;
+ }
+ return helper_export_local(argv, /*exp:*/ 0, /*ro:*/ 1, /*lvl:*/ 0);
}
#endif
+
#if ENABLE_HUSH_UNSET
/* http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#unset */
static int FAST_FUNC builtin_unset(char **argv)
printf(JOB_STATUS_FORMAT, job->jobid, status_string, job->cmdtext);
}
+
+ clean_up_last_dead_job();
+
return EXIT_SUCCESS;
}
i = kill(- pi->pgrp, SIGCONT);
if (i < 0) {
if (errno == ESRCH) {
- delete_finished_bg_job(pi);
+ delete_finished_job(pi);
return EXIT_SUCCESS;
}
bb_perror_msg("kill (SIGCONT)");
}
if (argv[0][0] == 'f') {
- remove_bg_job(pi);
+ remove_job_from_table(pi); /* FG job shouldn't be in job table */
return checkjobs_and_fg_shell(pi);
}
return EXIT_SUCCESS;
wait_pipe = parse_jobspec(*argv);
if (wait_pipe) {
ret = job_exited_or_stopped(wait_pipe);
- if (ret < 0)
+ if (ret < 0) {
ret = wait_for_child_or_signal(wait_pipe, 0);
-//bash immediately deletes finished jobs from job table only in interactive mode,
-//we _always_ delete them at once. If we'd start doing that, this (and more)
-//would be necessary to avoid accumulating dead jobs:
-# if 0
- else {
- if (!wait_pipe->alive_cmds)
- delete_finished_bg_job(wait_pipe);
+ } else {
+ /* waiting on "last dead job" removes it */
+ clean_up_last_dead_job();
}
-# endif
}
/* else: parse_jobspec() already emitted error msg */
continue;
/* No */
ret = 127;
if (errno == ECHILD) {
- if (G.last_bg_pid > 0 && pid == G.last_bg_pid) {
+ if (pid == G.last_bg_pid) {
/* "wait $!" but last bg task has already exited. Try:
* (sleep 1; exit 3) & sleep 2; echo $?; wait $!; echo $?
* In bash it prints exitcode 0, then 3.