*
* 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
#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.
* 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 */
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 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;
+ clean_up_last_dead_job();
+
/* Find the end of the list, and find next job ID to use */
i = 0;
jobp = &G.job_list;
printf("[%u] %u %s\n", job->jobid, (unsigned)job->cmds[0].pid, job->cmdtext);
G.last_jobid = job->jobid;
}
-
-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;
- }
- if (G.job_list)
- G.last_jobid = G.job_list->jobid;
- else
- G.last_jobid = 0;
-}
-
-static void delete_finished_job(struct pipe *pi)
-{
- remove_job_from_table(pi);
- free_pipe(pi);
-}
#endif /* JOB */
static int job_exited_or_stopped(struct pipe *pi)
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_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 */
}
#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;
}
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 keeping some dead jobs, this
-//(and more) would be necessary to avoid accumulating dead jobs:
-# if 0
- else {
- if (!wait_pipe->alive_cmds)
- delete_finished_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;