* tilde expansion
* aliases
* builtins mandated by standards we don't support:
- * [un]alias, command, fc, getopts, readonly, times:
+ * [un]alias, command, fc, getopts, times:
* command -v CMD: print "/path/to/CMD"
* prints "CMD" for builtins
* prints "alias ALIAS='EXPANSION'" for aliases
* 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
- * readonly VAR[=VAL]...: make VARs readonly
- * readonly [-p]: list all such VARs (-p has no effect in bash)
+ * 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
* aaa
*/
//config:config HUSH
-//config: bool "hush"
+//config: bool "hush (64 kb)"
//config: default y
//config: help
-//config: hush is a small shell (25k). It handles the normal flow control
-//config: constructs such as if/then/elif/else/fi, for/in/do/done, while loops,
-//config: case/esac. Redirections, here documents, $((arithmetic))
-//config: and functions are supported.
+//config: hush is a small shell. It handles the normal flow control
+//config: constructs such as if/then/elif/else/fi, for/in/do/done, while loops,
+//config: case/esac. Redirections, here documents, $((arithmetic))
+//config: and functions are supported.
//config:
-//config: It will compile and work on no-mmu systems.
+//config: It will compile and work on no-mmu systems.
//config:
-//config: It does not handle select, aliases, tilde expansion,
-//config: &>file and >&file redirection of stdout+stderr.
+//config: It does not handle select, aliases, tilde expansion,
+//config: &>file and >&file redirection of stdout+stderr.
//config:
//config:config HUSH_BASH_COMPAT
//config: bool "bash-compatible extensions"
//config: default y
//config: depends on HUSH_BASH_COMPAT
//config: help
-//config: Enable {abc,def} extension.
+//config: Enable {abc,def} extension.
//config:
//config:config HUSH_INTERACTIVE
//config: bool "Interactive mode"
//config: default y
//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
//config: help
-//config: Enable interactive mode (prompt and command editing).
-//config: Without this, hush simply reads and executes commands
-//config: from stdin just like a shell script from a file.
-//config: No prompt, no PS1/PS2 magic shell variables.
+//config: Enable interactive mode (prompt and command editing).
+//config: Without this, hush simply reads and executes commands
+//config: from stdin just like a shell script from a file.
+//config: No prompt, no PS1/PS2 magic shell variables.
//config:
//config:config HUSH_SAVEHISTORY
//config: bool "Save command history to .hush_history"
//config: default y
//config: depends on HUSH_INTERACTIVE
//config: help
-//config: Enable job control: Ctrl-Z backgrounds, Ctrl-C interrupts current
-//config: command (not entire shell), fg/bg builtins work. Without this option,
-//config: "cmd &" still works by simply spawning a process and immediately
-//config: prompting for next command (or executing next command in a script),
-//config: but no separate process group is formed.
+//config: Enable job control: Ctrl-Z backgrounds, Ctrl-C interrupts current
+//config: command (not entire shell), fg/bg builtins work. Without this option,
+//config: "cmd &" still works by simply spawning a process and immediately
+//config: prompting for next command (or executing next command in a script),
+//config: but no separate process group is formed.
//config:
//config:config HUSH_TICK
//config: bool "Support process substitution"
//config: default y
//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
//config: help
-//config: Enable `command` and $(command).
+//config: Enable `command` and $(command).
//config:
//config:config HUSH_IF
//config: bool "Support if/then/elif/else/fi"
//config: default y
//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
//config: help
-//config: Enable case ... esac statement. +400 bytes.
+//config: Enable case ... esac statement. +400 bytes.
//config:
//config:config HUSH_FUNCTIONS
//config: bool "Support funcname() { commands; } syntax"
//config: default y
//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
//config: help
-//config: Enable support for shell functions. +800 bytes.
+//config: Enable support for shell functions. +800 bytes.
//config:
//config:config HUSH_LOCAL
//config: bool "local builtin"
//config: default y
//config: depends on HUSH_FUNCTIONS
//config: help
-//config: Enable support for local variables in functions.
+//config: Enable support for local variables in functions.
//config:
//config:config HUSH_RANDOM_SUPPORT
//config: bool "Pseudorandom generator and $RANDOM variable"
//config: default y
//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
//config: help
-//config: Enable pseudorandom generator and dynamic variable "$RANDOM".
-//config: Each read of "$RANDOM" will generate a new pseudorandom value.
+//config: Enable pseudorandom generator and dynamic variable "$RANDOM".
+//config: Each read of "$RANDOM" will generate a new pseudorandom value.
//config:
//config:config HUSH_MODE_X
//config: bool "Support 'hush -x' option and 'set -x' command"
//config: default y
//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
//config: help
-//config: This instructs hush to print commands before execution.
-//config: Adds ~300 bytes.
+//config: This instructs hush to print commands before execution.
+//config: Adds ~300 bytes.
//config:
//config:config HUSH_ECHO
//config: bool "echo builtin"
//config: default y
//config: depends on HUSH_EXPORT
//config: help
-//config: export -n unexports variables. It is a bash extension.
+//config: export -n unexports variables. It is a bash extension.
+//config:
+//config:config HUSH_READONLY
+//config: bool "readonly builtin"
+//config: default y
+//config: depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
+//config: help
+//config: Enable support for read-only variables.
//config:
//config:config HUSH_KILL
//config: bool "kill builtin (supports kill %jobspec)"
#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;
BLTIN("export" , builtin_export , "Set environment variables"),
#endif
#if ENABLE_HUSH_JOB
- BLTIN("fg" , builtin_fg_bg , "Bring job into foreground"),
+ BLTIN("fg" , builtin_fg_bg , "Bring job to foreground"),
#endif
#if ENABLE_HUSH_HELP
BLTIN("help" , builtin_help , NULL),
#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
BLTIN("unset" , builtin_unset , "Unset variables"),
#endif
#if ENABLE_HUSH_WAIT
- BLTIN("wait" , builtin_wait , "Wait for process"),
+ BLTIN("wait" , builtin_wait , "Wait for process to finish"),
#endif
};
/* These builtins won't be used if we are on NOMMU and need to re-exec
/* str holds "NAME=VAL" and is expected to be malloced.
* We take ownership of it.
- * flg_export:
- * 0: do not change export flag
- * (if creating new variable, flag will be 0)
- * 1: set export flag and putenv the variable
- * -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)
+#define SETFLAG_EXPORT (1 << 0)
+#define SETFLAG_UNEXPORT (1 << 1)
+#define SETFLAG_MAKE_RO (1 << 2)
+#define SETFLAG_LOCAL_SHIFT 3
+static int set_local_var(char *str, unsigned flags)
{
struct variable **var_pp;
struct variable *cur;
char *free_me = NULL;
char *eq_sign;
int name_len;
+ IF_HUSH_LOCAL(unsigned local_lvl = (flags >> SETFLAG_LOCAL_SHIFT);)
eq_sign = strchr(str, '=');
if (!eq_sign) { /* not expected to ever happen? */
/* 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);
+ bb_error_msg("%s: readonly variable", str);
free(str);
+//NOTE: in bash, assignment in "export READONLY_VAR=Z" fails, and sets $?=1,
+//but export per se succeeds (does put the var in env). We don't mimic that.
return -1;
}
- if (flg_export == -1) { // "&& cur->flg_export" ?
+ if (flags & SETFLAG_UNEXPORT) { // && cur->flg_export ?
debug_printf_env("%s: unsetenv '%s'\n", __func__, str);
*eq_sign = '\0';
unsetenv(str);
* z=z
*/
if (cur->flg_export)
- flg_export = 1;
+ flags |= SETFLAG_EXPORT;
break;
}
#endif
/* Not found - create new variable struct */
cur = xzalloc(sizeof(*cur));
-#if ENABLE_HUSH_LOCAL
- cur->func_nest_level = local_lvl;
-#endif
+ IF_HUSH_LOCAL(cur->func_nest_level = local_lvl;)
cur->next = *var_pp;
*var_pp = cur;
set_str_and_exp:
cur->varstr = str;
-#if !BB_MMU
- cur->flg_read_only = flg_read_only;
-#endif
exp:
- if (flg_export == 1)
+#if !BB_MMU || ENABLE_HUSH_READONLY
+ if (flags & SETFLAG_MAKE_RO) {
+ cur->flg_read_only = 1;
+ }
+#endif
+ if (flags & SETFLAG_EXPORT)
cur->flg_export = 1;
if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S')
cmdedit_update_prompt();
if (cur->flg_export) {
- if (flg_export == -1) {
+ if (flags & SETFLAG_UNEXPORT) {
cur->flg_export = 0;
/* unsetenv was already done */
} else {
}
/* Used at startup and after each cd */
-static void set_pwd_var(int exp)
+static void set_pwd_var(unsigned flag)
{
- set_local_var(xasprintf("PWD=%s", get_cwd(/*force:*/ 1)),
- /*exp:*/ exp, /*lvl:*/ 0, /*ro:*/ 0);
+ set_local_var(xasprintf("PWD=%s", get_cwd(/*force:*/ 1)), flag);
}
static int unset_local_var_len(const char *name, int name_len)
static void FAST_FUNC set_local_var_from_halves(const char *name, const char *val)
{
char *var = xasprintf("%s=%s", name, val);
- set_local_var(var, /*flags:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+ set_local_var(var, /*flag:*/ 0);
}
#endif
if (eq) {
var_pp = get_ptr_to_local_var(*s, eq - *s);
if (var_pp) {
- /* Remove variable from global linked list */
var_p = *var_pp;
+ if (var_p->flg_read_only) {
+ char **p;
+ bb_error_msg("%s: readonly variable", *s);
+ /*
+ * "VAR=V BLTIN" unsets VARs after BLTIN completes.
+ * If VAR is readonly, leaving it in the list
+ * after asssignment error (msg above)
+ * causes doubled error message later, on unset.
+ */
+ debug_printf_env("removing/freeing '%s' element\n", *s);
+ free(*s);
+ p = s;
+ do { *p = p[1]; p++; } while (*p);
+ goto next;
+ }
+ /* Remove variable from global linked list */
debug_printf_env("%s: removing '%s'\n", __func__, var_p->varstr);
*var_pp = var_p->next;
/* Add it to returned list */
var_p->next = old;
old = var_p;
}
- set_local_var(*s, /*exp:*/ 1, /*lvl:*/ 0, /*ro:*/ 0);
+ set_local_var(*s, SETFLAG_EXPORT);
}
+ next:
s++;
}
return old;
static void o_addblock(o_string *o, const char *str, int len)
{
o_grow_by(o, len);
- memcpy(&o->data[o->length], str, len);
+ ((char*)mempcpy(&o->data[o->length], str, len))[0] = '\0';
o->length += len;
- o->data[o->length] = '\0';
}
static void o_addstr(o_string *o, const char *str)
break;
result = xrealloc(result, res_len + (s - val) + repl_len + 1);
- memcpy(result + res_len, val, s - val);
- res_len += s - val;
- strcpy(result + res_len, repl);
- res_len += repl_len;
+ strcpy(mempcpy(result + res_len, val, s - val), repl);
+ res_len += (s - val) + repl_len;
debug_printf_varexp("val:'%s' s:'%s' result:'%s'\n", val, s, result);
val = s + size;
if (exp_op == '/')
break;
}
- if (val[0] && result) {
+ if (*val && result) {
result = xrealloc(result, res_len + strlen(val) + 1);
strcpy(result + res_len, val);
debug_printf_varexp("val:'%s' result:'%s'\n", val, result);
first_char = arg[0] = arg0 & 0x7f;
exp_op = 0;
- if (first_char == '#' /* ${#... */
- && arg[1] && !exp_saveptr /* not ${#} and not ${#<op_char>...} */
+ if (first_char == '#' && arg[1] /* ${#... but not ${#} */
+ && (!exp_saveptr /* and (not ${#<op_char>...} */
+ || (arg[1] == '?' && arg[2] == '\0') /* or ${#?} - "len of $?") */
+ )
) {
/* It must be length operator: ${#var} */
var++;
/* mimic bash message */
die_if_script("%s: %s",
var,
- exp_word[0] ? exp_word : "parameter null or not set"
+ exp_word[0]
+ ? exp_word
+ : "parameter null or not set"
+ /* ash has more specific messages, a-la: */
+ /*: (exp_save == ':' ? "parameter null or not set" : "parameter not set")*/
);
//TODO: how interactive bash aborts expansion mid-command?
} else {
val = NULL;
} else {
char *new_var = xasprintf("%s=%s", var, val);
- set_local_var(new_var, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+ set_local_var(new_var, /*flag:*/ 0);
}
}
}
/* moved_to = -1: fd was opened by redirect; close orig_fd after redir */
};
+static struct squirrel *append_squirrel(struct squirrel *sq, int i, int orig, int moved)
+{
+ sq = xrealloc(sq, (i + 2) * sizeof(sq[0]));
+ sq[i].orig_fd = orig;
+ sq[i].moved_to = moved;
+ sq[i+1].orig_fd = -1; /* end marker */
+ return sq;
+}
+
static struct squirrel *add_squirrel(struct squirrel *sq, int fd, int avoid_fd)
{
+ int moved_to;
int i = 0;
if (sq) while (sq[i].orig_fd >= 0) {
i++;
}
- sq = xrealloc(sq, (i + 2) * sizeof(sq[0]));
- sq[i].orig_fd = fd;
/* If this fd is open, we move and remember it; if it's closed, moved_to = -1 */
- sq[i].moved_to = fcntl_F_DUPFD(fd, avoid_fd);
- debug_printf_redir("redirect_fd %d: previous fd is moved to %d (-1 if it was closed)\n", fd, sq[i].moved_to);
- if (sq[i].moved_to < 0 && errno != EBADF)
+ moved_to = fcntl_F_DUPFD(fd, avoid_fd);
+ debug_printf_redir("redirect_fd %d: previous fd is moved to %d (-1 if it was closed)\n", fd, moved_to);
+ if (moved_to < 0 && errno != EBADF)
xfunc_die();
- sq[i+1].orig_fd = -1; /* end marker */
- return sq;
+ return append_squirrel(sq, i, fd, moved_to);
}
/* fd: redirect wants this fd to be used (e.g. 3>file).
*/
return 1;
}
+ if (openfd == redir->rd_fd && sqp) {
+ /* open() gave us precisely the fd we wanted.
+ * This means that this fd was not busy
+ * (not opened to anywhere).
+ * Remember to close it on restore:
+ */
+ struct squirrel *sq = *sqp;
+ int i = 0;
+ if (sq) while (sq[i].orig_fd >= 0)
+ i++;
+ *sqp = append_squirrel(sq, i, openfd, -1); /* -1 = "it was closed" */
+ debug_printf_redir("redir to previously closed fd %d\n", openfd);
+ }
} else {
/* "rd_fd<*>rd_dup" or "rd_fd<*>-" cases */
openfd = redir->rd_dup;
if (new_env) {
argv = new_env;
while (*argv) {
- set_local_var(*argv, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
- /* Do we need to flag set_local_var() errors?
- * "assignment to readonly var" and "putenv error"
- */
+ if (set_local_var(*argv, /*flag:*/ 0)) {
+ /* assignment to readonly var / putenv error? */
+ rcode = 1;
+ }
argv++;
}
}
fprintf(stderr, " %s", p);
debug_printf_exec("set shell var:'%s'->'%s'\n",
*argv, p);
- set_local_var(p, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
- /* Do we need to flag set_local_var() errors?
- * "assignment to readonly var" and "putenv error"
- */
+ if (set_local_var(p, /*flag:*/ 0)) {
+ /* assignment to readonly var / putenv error? */
+ rcode = 1;
+ }
argv++;
}
if (G_x_mode)
}
/* Insert next value from for_lcur */
/* note: *for_lcur already has quotes removed, $var expanded, etc */
- set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+ set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*flag:*/ 0);
continue;
}
if (rword == RES_IN) {
putenv(shell_ver->varstr);
/* Export PWD */
- set_pwd_var(/*exp:*/ 1);
+ set_pwd_var(SETFLAG_EXPORT);
#if BASH_HOSTNAME_VAR
/* Set (but not export) HOSTNAME unless already set */
}
case 'R':
case 'V':
- set_local_var(xstrdup(optarg), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ opt == 'R');
+ set_local_var(xstrdup(optarg), opt == 'R' ? SETFLAG_MAKE_RO : 0);
break;
# if ENABLE_HUSH_FUNCTIONS
case 'F': {
* Note: do not enforce exporting. If PWD was unset or unexported,
* set it again, but do not export. bash does the same.
*/
- set_pwd_var(/*exp:*/ 0);
+ set_pwd_var(/*flag:*/ 0);
return EXIT_SUCCESS;
}
}
#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, unsigned flags)
{
do {
char *name = *argv;
vpp = get_ptr_to_local_var(name, name_end - name);
var = vpp ? *vpp : NULL;
- if (exp == -1) { /* unexporting? */
+ if (flags & SETFLAG_UNEXPORT) {
/* export -n NAME (without =VALUE) */
if (var) {
var->flg_export = 0;
} /* else: export -n NOT_EXISTING_VAR: no-op */
continue;
}
- if (exp == 1) { /* exporting? */
+ if (flags & SETFLAG_EXPORT) {
/* export NAME (without =VALUE) */
if (var) {
var->flg_export = 1;
continue;
}
}
+ if (flags & SETFLAG_MAKE_RO) {
+ /* readonly NAME (without =VALUE) */
+ if (var) {
+ var->flg_read_only = 1;
+ continue;
+ }
+ }
# if ENABLE_HUSH_LOCAL
- if (exp == 0 /* local? */
- && var && var->func_nest_level == lvl
- ) {
- /* "local x=abc; ...; local x" - ignore second local decl */
- continue;
+ /* Is this "local" bltin? */
+ if (!(flags & (SETFLAG_EXPORT|SETFLAG_UNEXPORT|SETFLAG_MAKE_RO))) {
+ unsigned lvl = flags >> SETFLAG_LOCAL_SHIFT;
+ if (var && var->func_nest_level == lvl) {
+ /* "local x=abc; ...; local x" - ignore second local decl */
+ continue;
+ }
}
# endif
/* Exporting non-existing variable.
* 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);
+ if (set_local_var(name, flags))
+ return EXIT_FAILURE;
} 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, opt_unexport ? SETFLAG_UNEXPORT : SETFLAG_EXPORT);
}
#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, G.func_nest_level << SETFLAG_LOCAL_SHIFT);
+}
+#endif
+
+#if ENABLE_HUSH_READONLY
+static int FAST_FUNC builtin_readonly(char **argv)
+{
+ 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, SETFLAG_MAKE_RO);
}
#endif
sighandler_t handler;
sig = get_signum(*argv++);
- if (sig < 0 || sig >= NSIG) {
+ if (sig < 0) {
ret = EXIT_FAILURE;
/* Mimic bash message exactly */
bb_error_msg("trap: %s: invalid signal specification", argv[-1]);