All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH v1 00/21] Modernize U-Boot shell
@ 2021-12-31 16:13 Francis Laniel
  2021-12-31 16:13 ` [RFC PATCH v1 01/21] test: Add framework to test hush behavior Francis Laniel
                   ` (21 more replies)
  0 siblings, 22 replies; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

Hi.

First I hope you are fine and the same for your relatives.

During 2021 summer, Sean Anderson wrote a contribution to add a new shell, based
on LIL, to U-Boot [1][2].
While one of the goals of this contribution was to address the fact actual
U-Boot shell, which is based on Busybox hush, is old there was a discussion
about adding a new shell versus updating the actual one [3][4].

So, in this series, with Harald Seiler, we updated the actual U-Boot shell to
reflect what is currently in Busybox source code.
Basically, this contribution is about taking a snapshot of Busybox shell/hush.c
file (as it exists in commit 37460f5da) and adapt it to suit U-Boot needs.

This contribution was written to be as backward-compatible as possible to avoid
breaking the existing.
So, the 2021 hush flavor offers the same as the actual, that is to say:
1. Variable expansion.
2. Instruction lists (;, && and ||).
3. If, then and else.
4. Loops (for, while and until).
No new features offered by Busybox hush were implemented (e.g. functions).

In terms of testing, new unit tests were added to ut to ensure the new behavior
is the same as the old one and it does not add regression.
Nonetheless, if old behavior was buggy and fixed upstream, the fix is then added
to U-Boot [5].
In sandbox, all of these tests pass smoothly:
2021> printenv board
board=sandbox
2021> ut hush
Running 20 hush tests
...
Failures: 0
Thanks to the effort of Harald Seiler, I was successful booting a board:
2021> printenv board_rev
board_rev=iMX8MP
2021> boot
...
root@iMX8MPboard:~# fw_printenv board_rev
board_rev=iMX8MP

I marked this contribution as RFC to indeed collect your opinion.
My goal is not to change suddenly actual shell to this one, we clearly need a
transition period to think about it.
I think it is better to see this contribution as a proof of concept which shows
it is possible to update the actual shell.

If you want to review it - your review will really be appreciated - here are
some information regarding the commits:
* commits marked as "test:" deal with unit tests.
* commit "cli: Add Busybox upstream hush.c file." copies Busybox shell/hush.c
into U-Boot tree, this explain why this commit contains around 12000 additions.
* commit "cli: Port Busybox 2021 hush to U-Boot." modifies previously added file
to permit us to use this as new shell.
The really good idea of #include'ing Busybox code into a wrapper file to define
some particular functions while minimizing modifications to upstream code comes
from Harald Seiler.
* Other commits focus on enabling features we need (e.g. if).

Francis Laniel (21):
  test: Add framework to test hush behavior.
  test: hush: Test hush if/else.
  test/py: hush_if_test: Remove the test file.
  test: hush: Test hush variable expansion.
  test: hush: Test hush commands list.
  test: hush: Test hush loops.
  cli: Add Busybox upstream hush.c file.
  cli: Port Busybox 2021 hush to U-Boot.
  cli: Add choice for hush parser.
  cli: Add HUSH_2021_PARSER to hush parser choice .
  cli: Enables using hush 2021 parser as command line parser.
  cli: hush_2021: Enable variables expansion for hush 2021.
  cli: hush_2021: Add functions to be called from run_command().
  cli: Modify run_command() to add hush 2021 as parser.
  test: hush: Fix instructions list tests for hush 2021.
  test: hush: Fix variable expansion tests for hush 2021.
  cli: hush_2021: Enable using \< and \> as string compare operators.
  cli: hush_2021: Enable if keyword.
  test: hush: Fix if tests for hush 2021.
  cli: hush_2021: Enable loops.
  test: hush: Fix loop tests for hush 2021.

 cmd/Kconfig                        |    22 +
 common/Makefile                    |     3 +-
 common/cli.c                       |    27 +-
 common/cli_hush_2021.c             |   309 +
 common/cli_hush_2021_upstream.c    | 12791 +++++++++++++++++++++++++++
 include/cli_hush.h                 |     8 +
 include/test/hush.h                |    15 +
 include/test/suites.h              |     1 +
 test/Makefile                      |     3 +
 test/cmd_ut.c                      |     6 +
 test/hush/Makefile                 |    10 +
 test/hush/cmd_ut_hush.c            |    20 +
 test/hush/dollar.c                 |   242 +
 test/hush/if.c                     |   351 +
 test/hush/list.c                   |   127 +
 test/hush/loop.c                   |    84 +
 test/py/tests/test_hush_if_test.py |   184 -
 17 files changed, 14015 insertions(+), 188 deletions(-)
 create mode 100644 common/cli_hush_2021.c
 create mode 100644 common/cli_hush_2021_upstream.c
 create mode 100644 include/test/hush.h
 create mode 100644 test/hush/Makefile
 create mode 100644 test/hush/cmd_ut_hush.c
 create mode 100644 test/hush/dollar.c
 create mode 100644 test/hush/if.c
 create mode 100644 test/hush/list.c
 create mode 100644 test/hush/loop.c
 delete mode 100644 test/py/tests/test_hush_if_test.py


Best regards, thank you in advance for your reviews and happy new year!
---
[1] https://lists.denx.de/pipermail/u-boot/2021-July/453347.html
[2] https://runtimeterror.com/tech/lil/
[3] https://lists.denx.de/pipermail/u-boot/2021-July/453790.html
[4] https://lists.denx.de/pipermail/u-boot/2021-July/453848.html
[5] https://lists.denx.de/pipermail/u-boot/2021-August/458569.html
--
2.25.1

^ permalink raw reply	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 01/21] test: Add framework to test hush behavior.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-08 14:53   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 02/21] test: hush: Test hush if/else Francis Laniel
                   ` (20 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit introduces a new subcommand to ut: ut hush.
For the moment, this command does nothing, future commits will add tests which
will be run on command call.

Note that CONFIG_HUSH_PARSER must be defined to compile this new subcommand.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 include/test/hush.h     | 15 +++++++++++++++
 include/test/suites.h   |  1 +
 test/Makefile           |  3 +++
 test/cmd_ut.c           |  6 ++++++
 test/hush/Makefile      |  6 ++++++
 test/hush/cmd_ut_hush.c | 20 ++++++++++++++++++++
 6 files changed, 51 insertions(+)
 create mode 100644 include/test/hush.h
 create mode 100644 test/hush/Makefile
 create mode 100644 test/hush/cmd_ut_hush.c

diff --git a/include/test/hush.h b/include/test/hush.h
new file mode 100644
index 0000000000..cca66544a0
--- /dev/null
+++ b/include/test/hush.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * (C) Copyright 2021
+ * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+ */
+
+#ifndef __TEST_HUSH_H__
+#define __TEST_HUSH_H__
+
+#include <test/test.h>
+
+/* Declare a new environment test */
+#define HUSH_TEST(_name, _flags)	UNIT_TEST(_name, _flags, hush_test)
+
+#endif /* __TEST_HUSH_H__ */
diff --git a/include/test/suites.h b/include/test/suites.h
index d35cd83a4e..35a8429e18 100644
--- a/include/test/suites.h
+++ b/include/test/suites.h
@@ -36,6 +36,7 @@ int do_ut_compression(struct cmd_tbl *cmdtp, int flag, int argc,
 		      char *const argv[]);
 int do_ut_dm(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
 int do_ut_env(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
+int do_ut_hush(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
 int do_ut_lib(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
 int do_ut_log(struct cmd_tbl *cmdtp, int flag, int argc, char * const argv[]);
 int do_ut_mem(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]);
diff --git a/test/Makefile b/test/Makefile
index b3b2902e2e..c471125982 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -16,6 +16,9 @@ obj-$(CONFIG_$(SPL_)CMDLINE) += cmd_ut.o
 obj-$(CONFIG_$(SPL_)CMDLINE) += command_ut.o
 obj-$(CONFIG_$(SPL_)UT_COMPRESSION) += compression.o
 obj-y += dm/
+ifneq ($(CONFIG_HUSH_PARSER),)
+obj-$(CONFIG_CMDLINE) += hush/
+endif
 obj-$(CONFIG_$(SPL_)CMDLINE) += print_ut.o
 obj-$(CONFIG_$(SPL_)CMDLINE) += str_ut.o
 obj-$(CONFIG_UT_TIME) += time_ut.o
diff --git a/test/cmd_ut.c b/test/cmd_ut.c
index 90b260f72d..714b7e7417 100644
--- a/test/cmd_ut.c
+++ b/test/cmd_ut.c
@@ -70,6 +70,9 @@ static struct cmd_tbl cmd_ut_sub[] = {
 #ifdef CONFIG_CMD_ADDRMAP
 	U_BOOT_CMD_MKENT(addrmap, CONFIG_SYS_MAXARGS, 1, do_ut_addrmap, "", ""),
 #endif
+#if CONFIG_IS_ENABLED(HUSH_PARSER)
+	U_BOOT_CMD_MKENT(hush, CONFIG_SYS_MAXARGS, 1, do_ut_hush, "", ""),
+#endif
 };
 
 static int do_ut_all(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -148,6 +151,9 @@ static char ut_help_text[] =
 #endif
 #ifdef CONFIG_CMD_ADDRMAP
 	"ut addrmap - Very basic test of addrmap command\n"
+#endif
+#if CONFIG_IS_ENABLED(HUSH_PARSER)
+	"ut hush [test-name] - Test hush behavior\n"
 #endif
 	;
 #endif /* CONFIG_SYS_LONGHELP */
diff --git a/test/hush/Makefile b/test/hush/Makefile
new file mode 100644
index 0000000000..dfa2a92615
--- /dev/null
+++ b/test/hush/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# (C) Copyright 2021
+# Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+
+obj-y += cmd_ut_hush.o
diff --git a/test/hush/cmd_ut_hush.c b/test/hush/cmd_ut_hush.c
new file mode 100644
index 0000000000..48a1adbf28
--- /dev/null
+++ b/test/hush/cmd_ut_hush.c
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2021
+ * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+ */
+
+#include <common.h>
+#include <command.h>
+#include <test/hush.h>
+#include <test/suites.h>
+#include <test/ut.h>
+
+int do_ut_hush(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
+{
+	struct unit_test *tests = UNIT_TEST_SUITE_START(hush_test);
+	const int n_ents = UNIT_TEST_SUITE_COUNT(hush_test);
+
+	return cmd_ut_category("hush", "hush_test_",
+			       tests, n_ents, argc, argv);
+}
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 02/21] test: hush: Test hush if/else.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
  2021-12-31 16:13 ` [RFC PATCH v1 01/21] test: Add framework to test hush behavior Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-08 14:53   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 03/21] test/py: hush_if_test: Remove the test file Francis Laniel
                   ` (19 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

As asked in commit 9c6bf1715f6a ("test/py: hush_if_test: Add tests to cover
octal/hex values"), this commit translates test_hush_if_test.py to a C test.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/Makefile |   1 +
 test/hush/if.c     | 308 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 309 insertions(+)
 create mode 100644 test/hush/if.c

diff --git a/test/hush/Makefile b/test/hush/Makefile
index dfa2a92615..a3c9ae5106 100644
--- a/test/hush/Makefile
+++ b/test/hush/Makefile
@@ -4,3 +4,4 @@
 # Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
 
 obj-y += cmd_ut_hush.o
+obj-y += if.o
diff --git a/test/hush/if.c b/test/hush/if.c
new file mode 100644
index 0000000000..a57d88850f
--- /dev/null
+++ b/test/hush/if.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2021
+ * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+ */
+
+#include <common.h>
+#include <command.h>
+#include <env_attr.h>
+#include <test/hush.h>
+#include <test/ut.h>
+
+/*
+ * All tests will execute the following:
+ * if condition_to_test; then
+ *   true
+ * else
+ *   false
+ * fi
+ * If condition is true, command returns 1, 0 otherwise.
+ */
+const char *if_format = "if %s; then true; else false; fi";
+
+static int hush_test_if_base(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "true");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "false");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_base, 0);
+
+static int hush_test_if_basic_operators(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test aaa = aaa");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa = bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa != bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa != aaa");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa < bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test bbb < aaa");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test bbb > aaa");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa > bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -eq 123");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -eq 456");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -ne 456");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -ne 123");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -lt 456");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -lt 123");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 456 -lt 123");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -le 456");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -le 123");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 456 -le 123");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 456 -gt 123");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -gt 123");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -gt 456");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 456 -ge 123");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -ge 123");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 123 -ge 456");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_basic_operators, 0);
+
+static int hush_test_if_octal(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test 010 -eq 010");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 010 -eq 011");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 010 -ne 011");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 010 -ne 010");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_octal, 0);
+
+static int hush_test_if_hexadecimal(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -gt 0x2000001");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -gt 0x2000000");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -gt 0x1ffffff");
+	ut_assertok(run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_hexadecimal, 0);
+
+static int hush_test_if_mixed(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test 010 -eq 10");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 010 -ne 10");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0xa -eq 10");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0xa -eq 012");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 2000000 -gt 0x1ffffff");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -gt 1ffffff");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -lt 1ffffff");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -eq 2000000");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test 0x2000000 -ne 2000000");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test -z \"\"");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test -z \"aaa\"");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test -n \"aaa\"");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test -n \"\"");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_mixed, 0);
+
+static int hush_test_if_inverted(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test ! aaa = aaa");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! aaa = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! ! aaa = aaa");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! ! aaa = bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_inverted, 0);
+
+static int hush_test_if_binary(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test aaa != aaa -o bbb != bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa != aaa -o bbb = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa = aaa -o bbb != bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa = aaa -o bbb = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa != aaa -a bbb != bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa != aaa -a bbb = bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa = aaa -a bbb != bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test aaa = aaa -a bbb = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_binary, 0);
+
+static int hush_test_if_inverted_binary(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	sprintf(if_formatted, if_format, "test ! aaa != aaa -o ! bbb != bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! aaa != aaa -o ! bbb = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! aaa = aaa -o ! bbb != bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! aaa = aaa -o ! bbb = bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format,
+		"test ! ! aaa != aaa -o ! ! bbb != bbb");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format,
+		"test ! ! aaa != aaa -o ! ! bbb = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format,
+		"test ! ! aaa = aaa -o ! ! bbb != bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test ! ! aaa = aaa -o ! ! bbb = bbb");
+	ut_assertok(run_command(if_formatted, 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_inverted_binary, 0);
+
+static int hush_test_if_z_operator(struct unit_test_state *uts)
+{
+	char if_formatted[128];
+
+	/* Deal with environment variable used during test. */
+	env_set("ut_var_nonexistent", NULL);
+	env_set("ut_var_exists", "1");
+
+	sprintf(if_formatted, if_format, "test -z \"$ut_var_nonexistent\"");
+	ut_assertok(run_command(if_formatted, 0));
+
+	sprintf(if_formatted, if_format, "test -z \"$ut_var_exists\"");
+	ut_asserteq(1, run_command(if_formatted, 0));
+
+	/* Clear the set environment variable. */
+	env_set("ut_var_exists", NULL);
+
+	return 0;
+}
+HUSH_TEST(hush_test_if_z_operator, 0);
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 03/21] test/py: hush_if_test: Remove the test file.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
  2021-12-31 16:13 ` [RFC PATCH v1 01/21] test: Add framework to test hush behavior Francis Laniel
  2021-12-31 16:13 ` [RFC PATCH v1 02/21] test: hush: Test hush if/else Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-08 14:53   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 04/21] test: hush: Test hush variable expansion Francis Laniel
                   ` (18 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

Commit 9087ab2cc4 ("test/py: hush_if_test: Remove the test file.") translated
this test to a C test, so this python file is no more needed.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/py/tests/test_hush_if_test.py | 184 -----------------------------
 1 file changed, 184 deletions(-)
 delete mode 100644 test/py/tests/test_hush_if_test.py

diff --git a/test/py/tests/test_hush_if_test.py b/test/py/tests/test_hush_if_test.py
deleted file mode 100644
index 37c1608bb2..0000000000
--- a/test/py/tests/test_hush_if_test.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0
-# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
-
-# Test operation of the "if" shell command.
-
-import os
-import os.path
-import pytest
-
-# TODO: These tests should be converted to a C test.
-# For more information please take a look at the thread
-# https://lists.denx.de/pipermail/u-boot/2019-October/388732.html
-
-pytestmark = pytest.mark.buildconfigspec('hush_parser')
-
-# The list of "if test" conditions to test.
-subtests = (
-    # Base if functionality.
-
-    ('true', True),
-    ('false', False),
-
-    # Basic operators.
-
-    ('test aaa = aaa', True),
-    ('test aaa = bbb', False),
-
-    ('test aaa != bbb', True),
-    ('test aaa != aaa', False),
-
-    ('test aaa < bbb', True),
-    ('test bbb < aaa', False),
-
-    ('test bbb > aaa', True),
-    ('test aaa > bbb', False),
-
-    ('test 123 -eq 123', True),
-    ('test 123 -eq 456', False),
-
-    ('test 123 -ne 456', True),
-    ('test 123 -ne 123', False),
-
-    ('test 123 -lt 456', True),
-    ('test 123 -lt 123', False),
-    ('test 456 -lt 123', False),
-
-    ('test 123 -le 456', True),
-    ('test 123 -le 123', True),
-    ('test 456 -le 123', False),
-
-    ('test 456 -gt 123', True),
-    ('test 123 -gt 123', False),
-    ('test 123 -gt 456', False),
-
-    ('test 456 -ge 123', True),
-    ('test 123 -ge 123', True),
-    ('test 123 -ge 456', False),
-
-    # Octal tests
-
-    ('test 010 -eq 010', True),
-    ('test 010 -eq 011', False),
-
-    ('test 010 -ne 011', True),
-    ('test 010 -ne 010', False),
-
-    # Hexadecimal tests
-
-    ('test 0x2000000 -gt 0x2000001', False),
-    ('test 0x2000000 -gt 0x2000000', False),
-    ('test 0x2000000 -gt 0x1ffffff', True),
-
-    # Mixed tests
-
-    ('test 010 -eq 10', False),
-    ('test 010 -ne 10', True),
-    ('test 0xa -eq 10', True),
-    ('test 0xa -eq 012', True),
-
-    ('test 2000000 -gt 0x1ffffff', False),
-    ('test 0x2000000 -gt 1ffffff', True),
-    ('test 0x2000000 -lt 1ffffff', False),
-    ('test 0x2000000 -eq 2000000', False),
-    ('test 0x2000000 -ne 2000000', True),
-
-    ('test -z ""', True),
-    ('test -z "aaa"', False),
-
-    ('test -n "aaa"', True),
-    ('test -n ""', False),
-
-    # Inversion of simple tests.
-
-    ('test ! aaa = aaa', False),
-    ('test ! aaa = bbb', True),
-    ('test ! ! aaa = aaa', True),
-    ('test ! ! aaa = bbb', False),
-
-    # Binary operators.
-
-    ('test aaa != aaa -o bbb != bbb', False),
-    ('test aaa != aaa -o bbb = bbb', True),
-    ('test aaa = aaa -o bbb != bbb', True),
-    ('test aaa = aaa -o bbb = bbb', True),
-
-    ('test aaa != aaa -a bbb != bbb', False),
-    ('test aaa != aaa -a bbb = bbb', False),
-    ('test aaa = aaa -a bbb != bbb', False),
-    ('test aaa = aaa -a bbb = bbb', True),
-
-    # Inversion within binary operators.
-
-    ('test ! aaa != aaa -o ! bbb != bbb', True),
-    ('test ! aaa != aaa -o ! bbb = bbb', True),
-    ('test ! aaa = aaa -o ! bbb != bbb', True),
-    ('test ! aaa = aaa -o ! bbb = bbb', False),
-
-    ('test ! ! aaa != aaa -o ! ! bbb != bbb', False),
-    ('test ! ! aaa != aaa -o ! ! bbb = bbb', True),
-    ('test ! ! aaa = aaa -o ! ! bbb != bbb', True),
-    ('test ! ! aaa = aaa -o ! ! bbb = bbb', True),
-)
-
-def exec_hush_if(u_boot_console, expr, result):
-    """Execute a shell "if" command, and validate its result."""
-
-    config = u_boot_console.config.buildconfig
-    maxargs = int(config.get('config_sys_maxargs', '0'))
-    args = len(expr.split(' ')) - 1
-    if args > maxargs:
-        u_boot_console.log.warning('CONFIG_SYS_MAXARGS too low; need ' +
-            str(args))
-        pytest.skip()
-
-    cmd = 'if ' + expr + '; then echo true; else echo false; fi'
-    response = u_boot_console.run_command(cmd)
-    assert response.strip() == str(result).lower()
-
-@pytest.mark.buildconfigspec('cmd_echo')
-@pytest.mark.parametrize('expr,result', subtests)
-def test_hush_if_test(u_boot_console, expr, result):
-    """Test a single "if test" condition."""
-
-    exec_hush_if(u_boot_console, expr, result)
-
-def test_hush_z(u_boot_console):
-    """Test the -z operator"""
-    u_boot_console.run_command('setenv ut_var_nonexistent')
-    u_boot_console.run_command('setenv ut_var_exists 1')
-    exec_hush_if(u_boot_console, 'test -z "$ut_var_nonexistent"', True)
-    exec_hush_if(u_boot_console, 'test -z "$ut_var_exists"', False)
-    u_boot_console.run_command('setenv ut_var_exists')
-
-# We might test this on real filesystems via UMS, DFU, 'save', etc.
-# Of those, only UMS currently allows file removal though.
-@pytest.mark.buildconfigspec('cmd_echo')
-@pytest.mark.boardspec('sandbox')
-def test_hush_if_test_host_file_exists(u_boot_console):
-    """Test the "if test -e" shell command."""
-
-    test_file = u_boot_console.config.result_dir + \
-        '/creating_this_file_breaks_u_boot_tests'
-
-    try:
-        os.unlink(test_file)
-    except:
-        pass
-    assert not os.path.exists(test_file)
-
-    expr = 'test -e hostfs - ' + test_file
-    exec_hush_if(u_boot_console, expr, False)
-
-    try:
-        with open(test_file, 'wb'):
-            pass
-        assert os.path.exists(test_file)
-
-        expr = 'test -e hostfs - ' + test_file
-        exec_hush_if(u_boot_console, expr, True)
-    finally:
-        os.unlink(test_file)
-
-    expr = 'test -e hostfs - ' + test_file
-    exec_hush_if(u_boot_console, expr, False)
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 04/21] test: hush: Test hush variable expansion.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (2 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 03/21] test/py: hush_if_test: Remove the test file Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-08 14:53   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 05/21] test: hush: Test hush commands list Francis Laniel
                   ` (17 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit ensures shell variables are replaced by their values.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/Makefile |   1 +
 test/hush/dollar.c | 191 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 192 insertions(+)
 create mode 100644 test/hush/dollar.c

diff --git a/test/hush/Makefile b/test/hush/Makefile
index a3c9ae5106..feb4f71956 100644
--- a/test/hush/Makefile
+++ b/test/hush/Makefile
@@ -5,3 +5,4 @@
 
 obj-y += cmd_ut_hush.o
 obj-y += if.o
+obj-y += dollar.o
diff --git a/test/hush/dollar.c b/test/hush/dollar.c
new file mode 100644
index 0000000000..9313d145d6
--- /dev/null
+++ b/test/hush/dollar.c
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2021
+ * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+ */
+
+#include <common.h>
+#include <command.h>
+#include <env_attr.h>
+#include <test/hush.h>
+#include <test/ut.h>
+
+static int hush_test_simple_dollar(struct unit_test_state *uts)
+{
+	console_record_reset_enable();
+	ut_assertok(run_command("echo $dollar_foo", 0));
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-zero-length"
+	/*
+	 * Next line is empty as $dollar_foo was not set before, but compiler
+	 * complains about the format being empty, so we disable this warning
+	 * only for this line.
+	 */
+	ut_assert_nextline("");
+#pragma GCC diagnostic pop
+	ut_assert_console_end();
+
+	ut_assertok(run_command("echo ${dollar_foo}", 0));
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-zero-length"
+	/*
+	 * Next line is empty as $dollar_foo was not set before, but compiler
+	 * complains about the format being empty, so we disable this warning
+	 * only for this line.
+	 */
+	ut_assert_nextline("");
+#pragma GCC diagnostic pop
+	ut_assert_console_end();
+
+	ut_assertok(run_command("dollar_foo=bar", 0));
+
+	ut_assertok(run_command("echo $dollar_foo", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command("echo ${dollar_foo}", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	/*
+	 * R is way of giving raw string literals in C++.
+	 * It means a "\n" is a R string literal will not be interpreted as line
+	 * feed but printed as "\n".
+	 * GCC provides it for C as an extension.
+	 */
+	ut_assertok(run_command(R"(dollar_foo=\$bar)", 0));
+
+	ut_assertok(run_command("echo $dollar_foo", 0));
+	ut_assert_nextline("$bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command("dollar_foo='$bar'", 0));
+
+	ut_assertok(run_command("echo $dollar_foo", 0));
+	ut_assert_nextline("$bar");
+	ut_assert_console_end();
+
+	ut_asserteq(1, run_command("dollar_foo=bar quux", 0));
+	/* Next line contains error message. */
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	ut_asserteq(1, run_command("dollar_foo='bar quux", 0));
+	/* Next line contains error message. */
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	ut_asserteq(1, run_command(R"(dollar_foo=bar quux")", 0));
+	/* Two next lines contain error message. */
+	ut_assert_skipline();
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	ut_assertok(run_command(R"(dollar_foo='bar "quux')", 0));
+
+	ut_assertok(run_command("echo $dollar_foo", 0));
+	/*
+	 * This one is buggy.
+	 * ut_assert_nextline(R"(bar "quux)");
+	 * ut_assert_console_end();
+	 *
+	 * So, let's reset output:
+	 */
+	console_record_reset_enable();
+
+	ut_asserteq(1, run_command(R"(dollar_foo="bar 'quux")", 0));
+	/* Next line contains error message. */
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	ut_assertok(run_command("dollar_foo='bar quux'", 0));
+	ut_assertok(run_command("echo $dollar_foo", 0));
+	ut_assert_nextline("bar quux");
+	ut_assert_console_end();
+
+	puts("Beware: this test set local variable dollar_foo and it cannot be unset!");
+
+	return 0;
+}
+HUSH_TEST(hush_test_simple_dollar, 0);
+
+static int hush_test_env_dollar(struct unit_test_state *uts)
+{
+	env_set("env_foo", "bar");
+	console_record_reset_enable();
+
+	ut_assertok(run_command("echo $env_foo", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command("echo ${env_foo}", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	/* Environment variables have precedence over local variable. */
+	ut_assertok(run_command("env_foo=quux", 0));
+	ut_assertok(run_command("echo ${env_foo}", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	/* Clean up setting the variable. */
+	env_set("env_foo", NULL);
+
+	puts("Beware: this test set local variable env_foo and it cannot be unset!");
+
+	return 0;
+}
+HUSH_TEST(hush_test_env_dollar, 0);
+
+static int hush_test_command_dollar(struct unit_test_state *uts)
+{
+	console_record_reset_enable();
+
+	ut_assertok(run_command(R"(dollar_bar="echo bar")", 0));
+
+	ut_assertok(run_command("$dollar_bar", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command("${dollar_bar}", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command(R"(dollar_bar="echo
+	bar")", 0));
+
+	ut_assertok(run_command("$dollar_bar", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command(R"(dollar_bar='echo bar
+	')", 0));
+
+	ut_assertok(run_command("$dollar_bar", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command(R"(dollar_bar='echo bar\n')", 0));
+
+	ut_assertok(run_command("$dollar_bar", 0));
+	ut_assert_nextline("barn");
+	ut_assert_console_end();
+
+	ut_assertok(run_command("dollar_bar='echo $bar'", 0));
+
+	ut_assertok(run_command("$dollar_bar", 0));
+	ut_assert_nextline("$bar");
+	ut_assert_console_end();
+
+	ut_assertok(run_command("dollar_quux=quux", 0));
+	ut_assertok(run_command(R"(dollar_bar="echo $dollar_quux")", 0));
+
+	ut_assertok(run_command("$dollar_bar", 0));
+	ut_assert_nextline("quux");
+	ut_assert_console_end();
+
+	puts("Beware: this test sets local variable dollar_bar and dollar_quux and they cannot be unset!");
+
+	return 0;
+}
+HUSH_TEST(hush_test_command_dollar, 0);
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 05/21] test: hush: Test hush commands list.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (3 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 04/21] test: hush: Test hush variable expansion Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-08 14:54   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 06/21] test: hush: Test hush loops Francis Laniel
                   ` (16 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit ensures behavior of commands separated by ';', '&&' and '||'.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/Makefile |  1 +
 test/hush/list.c   | 79 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 80 insertions(+)
 create mode 100644 test/hush/list.c

diff --git a/test/hush/Makefile b/test/hush/Makefile
index feb4f71956..ff4fe7700b 100644
--- a/test/hush/Makefile
+++ b/test/hush/Makefile
@@ -6,3 +6,4 @@
 obj-y += cmd_ut_hush.o
 obj-y += if.o
 obj-y += dollar.o
+obj-y += list.o
diff --git a/test/hush/list.c b/test/hush/list.c
new file mode 100644
index 0000000000..052cf2783c
--- /dev/null
+++ b/test/hush/list.c
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2021
+ * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+ */
+
+#include <common.h>
+#include <command.h>
+#include <env_attr.h>
+#include <test/hush.h>
+#include <test/ut.h>
+
+static int hush_test_semicolon(struct unit_test_state *uts)
+{
+	/* A; B = B truth table. */
+	ut_asserteq(1, run_command("false; false", 0));
+	ut_assertok(run_command("false; true", 0));
+	ut_assertok(run_command("true; true", 0));
+	ut_asserteq(1, run_command("true; false", 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_semicolon, 0);
+
+static int hush_test_and(struct unit_test_state *uts)
+{
+	/* A && B truth table. */
+	ut_asserteq(1, run_command("false && false", 0));
+	ut_asserteq(1, run_command("false && true", 0));
+	ut_assertok(run_command("true && true", 0));
+	ut_asserteq(1, run_command("true && false", 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_and, 0);
+
+static int hush_test_or(struct unit_test_state *uts)
+{
+	/* A || B truth table. */
+	ut_asserteq(1, run_command("false || false", 0));
+	ut_assertok(run_command("false || true", 0));
+	ut_assertok(run_command("true || true", 0));
+	ut_assertok(run_command("true || false", 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_or, 0);
+
+static int hush_test_and_or(struct unit_test_state *uts)
+{
+	/* A && B || C truth table. */
+	ut_asserteq(1, run_command("false && false || false", 0));
+	ut_asserteq(1, run_command("false && false || true", 0));
+	ut_asserteq(1, run_command("false && true || true", 0));
+	ut_asserteq(1, run_command("false && true || false", 0));
+	ut_assertok(run_command("true && true || false", 0));
+	ut_asserteq(1, run_command("true && false || false", 0));
+	ut_assertok(run_command("true && false || true", 0));
+	ut_assertok(run_command("true && true || true", 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_and_or, 0);
+
+static int hush_test_or_and(struct unit_test_state *uts)
+{
+	/* A || B && C truth table. */
+	ut_asserteq(1, run_command("false || false && false", 0));
+	ut_asserteq(1, run_command("false || false && true", 0));
+	ut_assertok(run_command("false || true && true", 0));
+	ut_asserteq(1, run_command("false || true && false", 0));
+	ut_assertok(run_command("true || true && false", 0));
+	ut_assertok(run_command("true || false && false", 0));
+	ut_assertok(run_command("true || false && true", 0));
+	ut_assertok(run_command("true || true && true", 0));
+
+	return 0;
+}
+HUSH_TEST(hush_test_or_and, 0);
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 06/21] test: hush: Test hush loops.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (4 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 05/21] test: hush: Test hush commands list Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 07/21] cli: Add Busybox upstream hush.c file Francis Laniel
                   ` (15 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

The added tests ensure correct behavior of for, while and until loops.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/Makefile |  1 +
 test/hush/loop.c   | 64 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+)
 create mode 100644 test/hush/loop.c

diff --git a/test/hush/Makefile b/test/hush/Makefile
index ff4fe7700b..a2d98815e5 100644
--- a/test/hush/Makefile
+++ b/test/hush/Makefile
@@ -7,3 +7,4 @@ obj-y += cmd_ut_hush.o
 obj-y += if.o
 obj-y += dollar.o
 obj-y += list.o
+obj-y += loop.o
diff --git a/test/hush/loop.c b/test/hush/loop.c
new file mode 100644
index 0000000000..519c78ef7e
--- /dev/null
+++ b/test/hush/loop.c
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2021
+ * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
+ */
+
+#include <common.h>
+#include <command.h>
+#include <env_attr.h>
+#include <test/hush.h>
+#include <test/ut.h>
+
+static int hush_test_for(struct unit_test_state *uts)
+{
+	console_record_reset_enable();
+
+	ut_assertok(run_command("for loop_i in foo bar quux; do echo $loop_i; done", 0));
+	ut_assert_nextline("foo");
+	ut_assert_nextline("bar");
+	ut_assert_nextline("quux");
+	ut_assert_console_end();
+
+	puts("Beware: this test set local variable loop_i and it cannot be unset!");
+
+	return 0;
+}
+HUSH_TEST(hush_test_for, 0);
+
+static int hush_test_while(struct unit_test_state *uts)
+{
+	console_record_reset_enable();
+
+	/* Exit status is that of test, so 1 since test is false to quit the loop. */
+	ut_asserteq(1, run_command("while test -z \"$loop_foo\"; do echo bar; loop_foo=quux; done", 0));
+	ut_assert_nextline("bar");
+	ut_assert_console_end();
+
+	puts("Beware: this test set local variable loop_foo and it cannot be unset!");
+
+	return 0;
+}
+HUSH_TEST(hush_test_while, 0);
+
+static int hush_test_until(struct unit_test_state *uts)
+{
+	console_record_reset_enable();
+	env_set("loop_bar", "bar");
+
+	/*
+	 * WARNING We have to use environment variable because it is not possible
+	 * resetting local variable.
+	 */
+	ut_assertok(run_command("until test -z \"$loop_bar\"; do echo quux; setenv loop_bar; done", 0));
+	ut_assert_nextline("quux");
+	ut_assert_console_end();
+
+	/*
+	 * Loop normally resets foo environment variable, but we reset it here in
+	 * case the test failed.
+	 */
+	env_set("loop_bar", NULL);
+	return 0;
+}
+HUSH_TEST(hush_test_until, 0);
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 07/21] cli: Add Busybox upstream hush.c file.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (5 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 06/21] test: hush: Test hush loops Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 08/21] cli: Port Busybox 2021 hush to U-Boot Francis Laniel
                   ` (14 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit adds new file cli_hush_2021_upstream.c, it is a copy of Busybox hush
file it was of time to Busybox commit 37460f5da.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli_hush_2021_upstream.c | 12162 ++++++++++++++++++++++++++++++
 1 file changed, 12162 insertions(+)
 create mode 100644 common/cli_hush_2021_upstream.c

diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
new file mode 100644
index 0000000000..c970d9097e
--- /dev/null
+++ b/common/cli_hush_2021_upstream.c
@@ -0,0 +1,12162 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * A prototype Bourne shell grammar parser.
+ * Intended to follow the original Thompson and Ritchie
+ * "small and simple is beautiful" philosophy, which
+ * incidentally is a good match to today's BusyBox.
+ *
+ * Copyright (C) 2000,2001  Larry Doolittle <larry@doolittle.boa.org>
+ * Copyright (C) 2008,2009  Denys Vlasenko <vda.linux@googlemail.com>
+ *
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
+ *
+ * Credits:
+ *      The parser routines proper are all original material, first
+ *      written Dec 2000 and Jan 2001 by Larry Doolittle.  The
+ *      execution engine, the builtins, and much of the underlying
+ *      support has been adapted from busybox-0.49pre's lash, which is
+ *      Copyright (C) 1999-2004 by Erik Andersen <andersen@codepoet.org>
+ *      written by Erik Andersen <andersen@codepoet.org>.  That, in turn,
+ *      is based in part on ladsh.c, by Michael K. Johnson and Erik W.
+ *      Troan, which they placed in the public domain.  I don't know
+ *      how much of the Johnson/Troan code has survived the repeated
+ *      rewrites.
+ *
+ * Other credits:
+ *      o_addchr derived from similar w_addchar function in glibc-2.2.
+ *      parse_redirect, redirect_opt_num, and big chunks of main
+ *      and many builtins derived from contributions by Erik Andersen.
+ *      Miscellaneous bugfixes from Matt Kraai.
+ *
+ * There are two big (and related) architecture differences between
+ * this parser and the lash parser.  One is that this version is
+ * actually designed from the ground up to understand nearly all
+ * of the Bourne grammar.  The second, consequential change is that
+ * the parser and input reader have been turned inside out.  Now,
+ * the parser is in control, and asks for input as needed.  The old
+ * way had the input reader in control, and it asked for parsing to
+ * take place as needed.  The new way makes it much easier to properly
+ * handle the recursion implicit in the various substitutions, especially
+ * across continuation lines.
+ *
+ * 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
+ *      "command" missing features:
+ *          command -p CMD: run CMD using default $PATH
+ *              (can use this to override standalone shell as well?)
+ *          command BLTIN: disables special-ness (e.g. errors do not abort)
+ *          command -V CMD1 CMD2 CMD3 (multiple args) (not in standard)
+ *      builtins mandated by standards we don't support:
+ *          [un]alias, fc:
+ *          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 >&
+ *      reserved words: function select
+ *      advanced test: [[ ]]
+ *      process substitution: <(list) and >(list)
+ *      let EXPR [EXPR...]
+ *          Each EXPR is an arithmetic expression (ARITHMETIC EVALUATION)
+ *          If the last arg evaluates to 0, let returns 1; 0 otherwise.
+ *          NB: let `echo 'a=a + 1'` - error (IOW: multi-word expansion is used)
+ *      ((EXPR))
+ *          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
+ *
+ * Status of [[ support:
+ * [[ args ]] are CMD_SINGLEWORD_NOGLOB:
+ *   v='a b'; [[ $v = 'a b' ]]; echo 0:$?
+ *   [[ /bin/n* ]]; echo 0:$?
+ *   = is glob match operator, not equality operator: STR = GLOB
+ *   == same as =
+ *   =~ is regex match operator: STR =~ REGEX
+ * TODO:
+ * quoting needs to be considered (-f is an operator, "-f" and ""-f are not; etc)
+ * in word = GLOB, quoting should be significant on char-by-char basis: a*cd"*"
+ */
+//config:config HUSH
+//config:	bool "hush (68 kb)"
+//config:	default y
+//config:	select SHELL_HUSH
+//config:	help
+//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:
+//config:	It does not handle select, aliases, tilde expansion,
+//config:	&>file and >&file redirection of stdout+stderr.
+//config:
+// This option is visible (has a description) to make it possible to select
+// a "scripted" applet (such as NOLOGIN) but avoid selecting any shells:
+//config:config SHELL_HUSH
+//config:	bool "Internal shell for embedded script support"
+//config:	default n
+//config:
+//config:# hush options
+//config:# It's only needed to get "nice" menuconfig indenting.
+//config:if SHELL_HUSH || HUSH || SH_IS_HUSH || BASH_IS_HUSH
+//config:
+//config:config HUSH_BASH_COMPAT
+//config:	bool "bash-compatible extensions"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_BRACE_EXPANSION
+//config:	bool "Brace expansion"
+//config:	default y
+//config:	depends on HUSH_BASH_COMPAT
+//config:	help
+//config:	Enable {abc,def} extension.
+//config:
+//config:config HUSH_BASH_SOURCE_CURDIR
+//config:	bool "'source' and '.' builtins search current directory after $PATH"
+//config:	default n   # do not encourage non-standard behavior
+//config:	depends on HUSH_BASH_COMPAT
+//config:	help
+//config:	This is not compliant with standards. Avoid if possible.
+//config:
+//config:config HUSH_LINENO_VAR
+//config:	bool "$LINENO variable (bashism)"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_INTERACTIVE
+//config:	bool "Interactive mode"
+//config:	default y
+//config:	depends on SHELL_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:
+//config:config HUSH_SAVEHISTORY
+//config:	bool "Save command history to .hush_history"
+//config:	default y
+//config:	depends on HUSH_INTERACTIVE && FEATURE_EDITING_SAVEHISTORY
+//config:
+//config:config HUSH_JOB
+//config:	bool "Job control"
+//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:
+//config:config HUSH_TICK
+//config:	bool "Support command substitution"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:	help
+//config:	Enable `command` and $(command).
+//config:
+//config:config HUSH_IF
+//config:	bool "Support if/then/elif/else/fi"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_LOOPS
+//config:	bool "Support for, while and until loops"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_CASE
+//config:	bool "Support case ... esac statement"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:	help
+//config:	Enable case ... esac statement. +400 bytes.
+//config:
+//config:config HUSH_FUNCTIONS
+//config:	bool "Support funcname() { commands; } syntax"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:	help
+//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:
+//config:config HUSH_RANDOM_SUPPORT
+//config:	bool "Pseudorandom generator and $RANDOM variable"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:	help
+//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 SHELL_HUSH
+//config:	help
+//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 SHELL_HUSH
+//config:
+//config:config HUSH_PRINTF
+//config:	bool "printf builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_TEST
+//config:	bool "test builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_HELP
+//config:	bool "help builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_EXPORT
+//config:	bool "export builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_EXPORT_N
+//config:	bool "Support 'export -n' option"
+//config:	default y
+//config:	depends on HUSH_EXPORT
+//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:	depends on SHELL_HUSH
+//config:	help
+//config:	Enable support for read-only variables.
+//config:
+//config:config HUSH_KILL
+//config:	bool "kill builtin (supports kill %jobspec)"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_WAIT
+//config:	bool "wait builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_COMMAND
+//config:	bool "command builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_TRAP
+//config:	bool "trap builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_TYPE
+//config:	bool "type builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_TIMES
+//config:	bool "times builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_READ
+//config:	bool "read builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_SET
+//config:	bool "set builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_UNSET
+//config:	bool "unset builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_ULIMIT
+//config:	bool "ulimit builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_UMASK
+//config:	bool "umask builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_GETOPTS
+//config:	bool "getopts builtin"
+//config:	default y
+//config:	depends on SHELL_HUSH
+//config:
+//config:config HUSH_MEMLEAK
+//config:	bool "memleak builtin (debugging)"
+//config:	default n
+//config:	depends on SHELL_HUSH
+//config:
+//config:endif # hush options
+
+//applet:IF_HUSH(APPLET(hush, BB_DIR_BIN, BB_SUID_DROP))
+//                       APPLET_ODDNAME:name  main  location    suid_type     help
+//applet:IF_SH_IS_HUSH(  APPLET_ODDNAME(sh,   hush, BB_DIR_BIN, BB_SUID_DROP, hush))
+//applet:IF_BASH_IS_HUSH(APPLET_ODDNAME(bash, hush, BB_DIR_BIN, BB_SUID_DROP, hush))
+
+//kbuild:lib-$(CONFIG_SHELL_HUSH) += hush.o match.o shell_common.o
+//kbuild:lib-$(CONFIG_HUSH_RANDOM_SUPPORT) += random.o
+
+/* -i (interactive) is also accepted,
+ * but does nothing, therefore not shown in help.
+ * NOMMU-specific options are not meant to be used by users,
+ * therefore we don't show them either.
+ */
+//usage:#define hush_trivial_usage
+//usage:	"[-enxl] [-c 'SCRIPT' [ARG0 ARGS] | FILE [ARGS] | -s [ARGS]]"
+//usage:#define hush_full_usage "\n\n"
+//usage:	"Unix shell interpreter"
+
+#if !(defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
+	|| defined(__APPLE__) \
+    )
+# include <malloc.h>   /* for malloc_trim */
+#endif
+#include <glob.h>
+/* #include <dmalloc.h> */
+#if ENABLE_HUSH_CASE
+# include <fnmatch.h>
+#endif
+#include <sys/times.h>
+#include <sys/utsname.h> /* for setting $HOSTNAME */
+
+#include "busybox.h"  /* for APPLET_IS_NOFORK/NOEXEC */
+#include "unicode.h"
+#include "shell_common.h"
+#include "math.h"
+#include "match.h"
+#if ENABLE_HUSH_RANDOM_SUPPORT
+# include "random.h"
+#else
+# define CLEAR_RANDOM_T(rnd) ((void)0)
+#endif
+#ifndef O_CLOEXEC
+# define O_CLOEXEC 0
+#endif
+#ifndef F_DUPFD_CLOEXEC
+# define F_DUPFD_CLOEXEC F_DUPFD
+#endif
+
+#if ENABLE_FEATURE_SH_EMBEDDED_SCRIPTS && !(ENABLE_ASH || ENABLE_SH_IS_ASH || ENABLE_BASH_IS_ASH)
+# include "embedded_scripts.h"
+#else
+# define NUM_SCRIPTS 0
+#endif
+
+/* So far, all bash compat is controlled by one config option */
+/* Separate defines document which part of code implements what */
+#define BASH_PATTERN_SUBST ENABLE_HUSH_BASH_COMPAT
+#define BASH_SUBSTR        ENABLE_HUSH_BASH_COMPAT
+#define BASH_SOURCE        ENABLE_HUSH_BASH_COMPAT
+#define BASH_DOLLAR_SQUOTE ENABLE_HUSH_BASH_COMPAT
+#define BASH_HOSTNAME_VAR  ENABLE_HUSH_BASH_COMPAT
+#define BASH_EPOCH_VARS    ENABLE_HUSH_BASH_COMPAT
+#define BASH_TEST2         (ENABLE_HUSH_BASH_COMPAT && ENABLE_HUSH_TEST)
+#define BASH_READ_D        ENABLE_HUSH_BASH_COMPAT
+
+
+/* Build knobs */
+#define LEAK_HUNTING 0
+#define BUILD_AS_NOMMU 0
+/* Enable/disable sanity checks. Ok to enable in production,
+ * only adds a bit of bloat. Set to >1 to get non-production level verbosity.
+ * Keeping 1 for now even in released versions.
+ */
+#define HUSH_DEBUG 1
+/* Slightly bigger (+200 bytes), but faster hush.
+ * So far it only enables a trick with counting SIGCHLDs and forks,
+ * which allows us to do fewer waitpid's.
+ * (we can detect a case where neither forks were done nor SIGCHLDs happened
+ * and therefore waitpid will return the same result as last time)
+ */
+#define ENABLE_HUSH_FAST 0
+/* TODO: implement simplified code for users which do not need ${var%...} ops
+ * So far ${var%...} ops are always enabled:
+ */
+#define ENABLE_HUSH_DOLLAR_OPS 1
+
+
+#if BUILD_AS_NOMMU
+# undef BB_MMU
+# undef USE_FOR_NOMMU
+# undef USE_FOR_MMU
+# define BB_MMU 0
+# define USE_FOR_NOMMU(...) __VA_ARGS__
+# define USE_FOR_MMU(...)
+#endif
+
+#include "NUM_APPLETS.h"
+#if NUM_APPLETS == 1
+/* STANDALONE does not make sense, and won't compile */
+# undef CONFIG_FEATURE_SH_STANDALONE
+# undef ENABLE_FEATURE_SH_STANDALONE
+# undef IF_FEATURE_SH_STANDALONE
+# undef IF_NOT_FEATURE_SH_STANDALONE
+# define ENABLE_FEATURE_SH_STANDALONE 0
+# define IF_FEATURE_SH_STANDALONE(...)
+# define IF_NOT_FEATURE_SH_STANDALONE(...) __VA_ARGS__
+#endif
+
+#if !ENABLE_HUSH_INTERACTIVE
+# undef ENABLE_FEATURE_EDITING
+# define ENABLE_FEATURE_EDITING 0
+# undef ENABLE_FEATURE_EDITING_FANCY_PROMPT
+# define ENABLE_FEATURE_EDITING_FANCY_PROMPT 0
+# undef ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+# define ENABLE_FEATURE_EDITING_SAVE_ON_EXIT 0
+#endif
+
+/* Do we support ANY keywords? */
+#if ENABLE_HUSH_IF || ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
+# define HAS_KEYWORDS 1
+# define IF_HAS_KEYWORDS(...) __VA_ARGS__
+# define IF_HAS_NO_KEYWORDS(...)
+#else
+# define HAS_KEYWORDS 0
+# define IF_HAS_KEYWORDS(...)
+# define IF_HAS_NO_KEYWORDS(...) __VA_ARGS__
+#endif
+
+/* If you comment out one of these below, it will be #defined later
+ * to perform debug printfs to stderr: */
+#define debug_printf(...)         do {} while (0)
+/* Finer-grained debug switches */
+#define debug_printf_parse(...)   do {} while (0)
+#define debug_printf_heredoc(...) do {} while (0)
+#define debug_print_tree(a, b)    do {} while (0)
+#define debug_printf_exec(...)    do {} while (0)
+#define debug_printf_env(...)     do {} while (0)
+#define debug_printf_jobs(...)    do {} while (0)
+#define debug_printf_expand(...)  do {} while (0)
+#define debug_printf_varexp(...)  do {} while (0)
+#define debug_printf_glob(...)    do {} while (0)
+#define debug_printf_redir(...)   do {} while (0)
+#define debug_printf_list(...)    do {} while (0)
+#define debug_printf_subst(...)   do {} while (0)
+#define debug_printf_prompt(...)  do {} while (0)
+#define debug_printf_clean(...)   do {} while (0)
+
+#define ERR_PTR ((void*)(long)1)
+
+#define JOB_STATUS_FORMAT    "[%u] %-22s %.40s\n"
+
+#define _SPECIAL_VARS_STR     "_*@$!?#-"
+#define SPECIAL_VARS_STR     ("_*@$!?#-" + 1)
+#define NUMERIC_SPECVARS_STR ("_*@$!?#-" + 3)
+#if BASH_PATTERN_SUBST
+/* Support / and // replace ops */
+/* Note that // is stored as \ in "encoded" string representation */
+# define VAR_ENCODED_SUBST_OPS      "\\/%#:-=+?"
+# define VAR_SUBST_OPS             ("\\/%#:-=+?" + 1)
+# define MINUS_PLUS_EQUAL_QUESTION ("\\/%#:-=+?" + 5)
+#else
+# define VAR_ENCODED_SUBST_OPS      "%#:-=+?"
+# define VAR_SUBST_OPS              "%#:-=+?"
+# define MINUS_PLUS_EQUAL_QUESTION ("%#:-=+?" + 3)
+#endif
+
+#define SPECIAL_VAR_SYMBOL_STR "\3"
+#define SPECIAL_VAR_SYMBOL       3
+/* The "variable" with name "\1" emits string "\3". Testcase: "echo ^C" */
+#define SPECIAL_VAR_QUOTED_SVS   1
+
+struct variable;
+
+static const char hush_version_str[] ALIGN1 = "HUSH_VERSION="BB_VER;
+
+/* This supports saving pointers malloced in vfork child,
+ * to be freed in the parent.
+ */
+#if !BB_MMU
+typedef struct nommu_save_t {
+	struct variable *old_vars;
+	char **argv;
+	char **argv_from_re_execing;
+} nommu_save_t;
+#endif
+
+enum {
+	RES_NONE  = 0,
+#if ENABLE_HUSH_IF
+	RES_IF    ,
+	RES_THEN  ,
+	RES_ELIF  ,
+	RES_ELSE  ,
+	RES_FI    ,
+#endif
+#if ENABLE_HUSH_LOOPS
+	RES_FOR   ,
+	RES_WHILE ,
+	RES_UNTIL ,
+	RES_DO    ,
+	RES_DONE  ,
+#endif
+#if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
+	RES_IN    ,
+#endif
+#if ENABLE_HUSH_CASE
+	RES_CASE  ,
+	/* three pseudo-keywords support contrived "case" syntax: */
+	RES_CASE_IN,   /* "case ... IN", turns into RES_MATCH when IN is observed */
+	RES_MATCH ,    /* "word)" */
+	RES_CASE_BODY, /* "this command is inside CASE" */
+	RES_ESAC  ,
+#endif
+	RES_XXXX  ,
+	RES_SNTX
+};
+
+typedef struct o_string {
+	char *data;
+	int length; /* position where data is appended */
+	int maxlen;
+	int o_expflags;
+	/* At least some part of the string was inside '' or "",
+	 * possibly empty one: word"", wo''rd etc. */
+	smallint has_quoted_part;
+	smallint has_empty_slot;
+	smallint ended_in_ifs;
+} o_string;
+enum {
+	EXP_FLAG_SINGLEWORD     = 0x80, /* must be 0x80 */
+	EXP_FLAG_GLOB           = 0x2,
+	/* Protect newly added chars against globbing
+	 * by prepending \ to *, ?, [, \ */
+	EXP_FLAG_ESC_GLOB_CHARS = 0x1,
+};
+/* Used for initialization: o_string foo = NULL_O_STRING; */
+#define NULL_O_STRING { NULL }
+
+#ifndef debug_printf_parse
+static const char *const assignment_flag[] = {
+	"MAYBE_ASSIGNMENT",
+	"DEFINITELY_ASSIGNMENT",
+	"NOT_ASSIGNMENT",
+	"WORD_IS_KEYWORD",
+};
+#endif
+
+/* We almost can use standard FILE api, but we need an ability to move
+ * its fd when redirects coincide with it. No api exists for that
+ * (RFE for it at https://sourceware.org/bugzilla/show_bug.cgi?id=21902).
+ * HFILE is our internal alternative. Only supports reading.
+ * Since we now can, we incorporate linked list of all opened HFILEs
+ * into the struct (used to be a separate mini-list).
+ */
+typedef struct HFILE {
+	char *cur;
+	char *end;
+	struct HFILE *next_hfile;
+	int fd;
+	char buf[1024];
+} HFILE;
+
+typedef struct in_str {
+	const char *p;
+	int peek_buf[2];
+	int last_char;
+	HFILE *file;
+} in_str;
+
+/* The descrip member of this structure is only used to make
+ * debugging output pretty */
+static const struct {
+	int32_t mode;
+	signed char default_fd;
+	char descrip[3];
+} redir_table[] ALIGN4 = {
+	{ O_RDONLY,                  0, "<"  },
+	{ O_CREAT|O_TRUNC|O_WRONLY,  1, ">"  },
+	{ O_CREAT|O_APPEND|O_WRONLY, 1, ">>" },
+	{ O_CREAT|O_RDWR,            1, "<>" },
+	{ O_RDONLY,                  0, "<<" },
+/* Should not be needed. Bogus default_fd helps in debugging */
+/*	{ O_RDONLY,                 77, "<<" }, */
+};
+
+struct redir_struct {
+	struct redir_struct *next;
+	char *rd_filename;          /* filename */
+	int rd_fd;                  /* fd to redirect */
+	/* fd to redirect to, or -3 if rd_fd is to be closed (n>&-) */
+	int rd_dup;
+	smallint rd_type;           /* (enum redir_type) */
+	/* note: for heredocs, rd_filename contains heredoc delimiter,
+	 * and subsequently heredoc itself; and rd_dup is a bitmask:
+	 * bit 0: do we need to trim leading tabs?
+	 * bit 1: is heredoc quoted (<<'delim' syntax) ?
+	 */
+};
+typedef enum redir_type {
+	REDIRECT_INPUT     = 0,
+	REDIRECT_OVERWRITE = 1,
+	REDIRECT_APPEND    = 2,
+	REDIRECT_IO        = 3,
+	REDIRECT_HEREDOC   = 4,
+	REDIRECT_HEREDOC2  = 5, /* REDIRECT_HEREDOC after heredoc is loaded */
+
+	REDIRFD_CLOSE      = -3,
+	REDIRFD_SYNTAX_ERR = -2,
+	REDIRFD_TO_FILE    = -1,
+	/* otherwise, rd_fd is redirected to rd_dup */
+
+	HEREDOC_SKIPTABS = 1,
+	HEREDOC_QUOTED   = 2,
+} redir_type;
+
+
+struct command {
+	pid_t pid;                  /* 0 if exited */
+	unsigned assignment_cnt;    /* how many argv[i] are assignments? */
+#if ENABLE_HUSH_LINENO_VAR
+	unsigned lineno;
+#endif
+	smallint cmd_type;          /* CMD_xxx */
+#define CMD_NORMAL   0
+#define CMD_SUBSHELL 1
+#if BASH_TEST2
+/* used for "[[ EXPR ]]" */
+# define CMD_TEST2_SINGLEWORD_NOGLOB 2
+#endif
+#if BASH_TEST2 || ENABLE_HUSH_LOCAL || ENABLE_HUSH_EXPORT || ENABLE_HUSH_READONLY
+/* used to prevent word splitting and globbing in "export v=t*" */
+# define CMD_SINGLEWORD_NOGLOB 3
+#endif
+#if ENABLE_HUSH_FUNCTIONS
+# define CMD_FUNCDEF 4
+#endif
+
+	smalluint cmd_exitcode;
+	/* if non-NULL, this "command" is { list }, ( list ), or a compound statement */
+	struct pipe *group;
+#if !BB_MMU
+	char *group_as_string;
+#endif
+#if ENABLE_HUSH_FUNCTIONS
+	struct function *child_func;
+/* This field is used to prevent a bug here:
+ * while...do f1() {a;}; f1; f1() {b;}; f1; done
+ * When we execute "f1() {a;}" cmd, we create new function and clear
+ * cmd->group, cmd->group_as_string, cmd->argv[0].
+ * When we execute "f1() {b;}", we notice that f1 exists,
+ * and that its "parent cmd" struct is still "alive",
+ * we put those fields back into cmd->xxx
+ * (struct function has ->parent_cmd ptr to facilitate that).
+ * When we loop back, we can execute "f1() {a;}" again and set f1 correctly.
+ * Without this trick, loop would execute a;b;b;b;...
+ * instead of correct sequence a;b;a;b;...
+ * When command is freed, it severs the link
+ * (sets ->child_func->parent_cmd to NULL).
+ */
+#endif
+	char **argv;                /* command name and arguments */
+/* argv vector may contain variable references (^Cvar^C, ^C0^C etc)
+ * and on execution these are substituted with their values.
+ * Substitution can make _several_ words out of one argv[n]!
+ * Example: argv[0]=='.^C*^C.' here: echo .$*.
+ * References of the form ^C`cmd arg^C are `cmd arg` substitutions.
+ */
+	struct redir_struct *redirects; /* I/O redirections */
+};
+/* Is there anything in this command at all? */
+#define IS_NULL_CMD(cmd) \
+	(!(cmd)->group && !(cmd)->argv && !(cmd)->redirects)
+
+struct pipe {
+	struct pipe *next;
+	int num_cmds;               /* total number of commands in pipe */
+	int alive_cmds;             /* number of commands running (not exited) */
+	int stopped_cmds;           /* number of commands alive, but stopped */
+#if ENABLE_HUSH_JOB
+	unsigned jobid;             /* job number */
+	pid_t pgrp;                 /* process group ID for the job */
+	char *cmdtext;              /* name of job */
+#endif
+	struct command *cmds;       /* array of commands in pipe */
+	smallint followup;          /* PIPE_BG, PIPE_SEQ, PIPE_OR, PIPE_AND */
+	IF_HAS_KEYWORDS(smallint pi_inverted;) /* "! cmd | cmd" */
+	IF_HAS_KEYWORDS(smallint res_word;) /* needed for if, for, while, until... */
+};
+typedef enum pipe_style {
+	PIPE_SEQ = 0,
+	PIPE_AND = 1,
+	PIPE_OR  = 2,
+	PIPE_BG  = 3,
+} pipe_style;
+/* Is there anything in this pipe at all? */
+#define IS_NULL_PIPE(pi) \
+	((pi)->num_cmds == 0 IF_HAS_KEYWORDS( && (pi)->res_word == RES_NONE))
+
+/* This holds pointers to the various results of parsing */
+struct parse_context {
+	/* linked list of pipes */
+	struct pipe *list_head;
+	/* last pipe (being constructed right now) */
+	struct pipe *pipe;
+	/* last command in pipe (being constructed right now) */
+	struct command *command;
+	/* last redirect in command->redirects list */
+	struct redir_struct *pending_redirect;
+	o_string word;
+#if !BB_MMU
+	o_string as_string;
+#endif
+	smallint is_assignment; /* 0:maybe, 1:yes, 2:no, 3:keyword */
+#if HAS_KEYWORDS
+	smallint ctx_res_w;
+	smallint ctx_inverted; /* "! cmd | cmd" */
+#if ENABLE_HUSH_CASE
+	smallint ctx_dsemicolon; /* ";;" seen */
+#endif
+	/* bitmask of FLAG_xxx, for figuring out valid reserved words */
+	int old_flag;
+	/* group we are enclosed in:
+	 * example: "if pipe1; pipe2; then pipe3; fi"
+	 * when we see "if" or "then", we malloc and copy current context,
+	 * and make ->stack point to it. then we parse pipeN.
+	 * when closing "then" / fi" / whatever is found,
+	 * we move list_head into ->stack->command->group,
+	 * copy ->stack into current context, and delete ->stack.
+	 * (parsing of { list } and ( list ) doesn't use this method)
+	 */
+	struct parse_context *stack;
+#endif
+};
+enum {
+	MAYBE_ASSIGNMENT      = 0,
+	DEFINITELY_ASSIGNMENT = 1,
+	NOT_ASSIGNMENT        = 2,
+	/* Not an assignment, but next word may be: "if v=xyz cmd;" */
+	WORD_IS_KEYWORD       = 3,
+};
+
+/* On program start, environ points to initial environment.
+ * putenv adds new pointers into it, unsetenv removes them.
+ * Neither of these (de)allocates the strings.
+ * setenv allocates new strings in malloc space and does putenv,
+ * and thus setenv is unusable (leaky) for shell's purposes */
+#define setenv(...) setenv_is_leaky_dont_use()
+struct variable {
+	struct variable *next;
+	char *varstr;        /* points to "name=" portion */
+	int max_len;         /* if > 0, name is part of initial env; else name is malloced */
+	uint16_t var_nest_level;
+	smallint flg_export; /* putenv should be done on this var */
+	smallint flg_read_only;
+};
+
+enum {
+	BC_BREAK = 1,
+	BC_CONTINUE = 2,
+};
+
+#if ENABLE_HUSH_FUNCTIONS
+struct function {
+	struct function *next;
+	char *name;
+	struct command *parent_cmd;
+	struct pipe *body;
+# if !BB_MMU
+	char *body_as_string;
+# endif
+};
+#endif
+
+
+/* set -/+o OPT support. (TODO: make it optional)
+ * bash supports the following opts:
+ * allexport       off
+ * braceexpand     on
+ * emacs           on
+ * errexit         off
+ * errtrace        off
+ * functrace       off
+ * hashall         on
+ * histexpand      off
+ * history         on
+ * ignoreeof       off
+ * interactive-comments    on
+ * keyword         off
+ * monitor         on
+ * noclobber       off
+ * noexec          off
+ * noglob          off
+ * nolog           off
+ * notify          off
+ * nounset         off
+ * onecmd          off
+ * physical        off
+ * pipefail        off
+ * posix           off
+ * privileged      off
+ * verbose         off
+ * vi              off
+ * xtrace          off
+ */
+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
+	NUM_OPT_O
+};
+
+/* "Globals" within this file */
+/* Sorted roughly by size (smaller offsets == smaller code) */
+struct globals {
+	/* interactive_fd != 0 means we are an interactive shell.
+	 * If we are, then saved_tty_pgrp can also be != 0, meaning
+	 * that controlling tty is available. With saved_tty_pgrp == 0,
+	 * job control still works, but terminal signals
+	 * (^C, ^Z, ^Y, ^\) won't work at all, and background
+	 * process groups can only be created with "cmd &".
+	 * With saved_tty_pgrp != 0, hush will use tcsetpgrp()
+	 * to give tty to the foreground process group,
+	 * and will take it back when the group is stopped (^Z)
+	 * or killed (^C).
+	 */
+#if ENABLE_HUSH_INTERACTIVE
+	/* 'interactive_fd' is a fd# open to ctty, if we have one
+	 * _AND_ if we decided to act interactively */
+	int interactive_fd;
+	IF_NOT_FEATURE_EDITING_FANCY_PROMPT(char *PS1;)
+# define G_interactive_fd (G.interactive_fd)
+#else
+# define G_interactive_fd 0
+#endif
+#if ENABLE_FEATURE_EDITING
+	line_input_t *line_input_state;
+#endif
+	pid_t root_pid;
+	pid_t root_ppid;
+	pid_t last_bg_pid;
+#if ENABLE_HUSH_RANDOM_SUPPORT
+	random_t random_gen;
+#endif
+#if ENABLE_HUSH_JOB
+	int run_list_level;
+	unsigned last_jobid;
+	pid_t saved_tty_pgrp;
+	struct pipe *job_list;
+# define G_saved_tty_pgrp (G.saved_tty_pgrp)
+#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])
+#else
+# define G_x_mode 0
+#endif
+	char opt_s;
+	char opt_c;
+#if ENABLE_HUSH_INTERACTIVE
+	smallint promptmode; /* 0: PS1, 1: PS2 */
+#endif
+	smallint flag_SIGINT;
+#if ENABLE_HUSH_LOOPS
+	smallint flag_break_continue;
+#endif
+#if ENABLE_HUSH_FUNCTIONS
+	/* 0: outside of a function (or sourced file)
+	 * -1: inside of a function, ok to use return builtin
+	 * 1: return is invoked, skip all till end of func
+	 */
+	smallint flag_return_in_progress;
+# define G_flag_return_in_progress (G.flag_return_in_progress)
+#else
+# define G_flag_return_in_progress 0
+#endif
+	smallint exiting; /* used to prevent EXIT trap recursion */
+	/* These support $? */
+	smalluint last_exitcode;
+	smalluint expand_exitcode;
+	smalluint last_bg_pid_exitcode;
+#if ENABLE_HUSH_SET
+	/* are global_argv and global_argv[1..n] malloced? (note: not [0]) */
+	smalluint global_args_malloced;
+# define G_global_args_malloced (G.global_args_malloced)
+#else
+# define G_global_args_malloced 0
+#endif
+#if ENABLE_HUSH_BASH_COMPAT
+	int dead_job_exitcode; /* for "wait -n" */
+#endif
+	/* how many non-NULL argv's we have. NB: $# + 1 */
+	int global_argc;
+	char **global_argv;
+#if !BB_MMU
+	char *argv0_for_re_execing;
+#endif
+#if ENABLE_HUSH_LOOPS
+	unsigned depth_break_continue;
+	unsigned depth_of_loop;
+#endif
+#if ENABLE_HUSH_GETOPTS
+	unsigned getopt_count;
+#endif
+	const char *ifs;
+	char *ifs_whitespace; /* = G.ifs or malloced */
+	const char *cwd;
+	struct variable *top_var;
+	char **expanded_assignments;
+	struct variable **shadowed_vars_pp;
+	unsigned var_nest_level;
+#if ENABLE_HUSH_FUNCTIONS
+# if ENABLE_HUSH_LOCAL
+	unsigned func_nest_level; /* solely to prevent "local v" in non-functions */
+# endif
+	struct function *top_func;
+#endif
+	/* Signal and trap handling */
+#if ENABLE_HUSH_FAST
+	unsigned count_SIGCHLD;
+	unsigned handled_SIGCHLD;
+	smallint we_have_children;
+#endif
+#if ENABLE_HUSH_LINENO_VAR
+	unsigned parse_lineno;
+	unsigned execute_lineno;
+#endif
+	HFILE *HFILE_list;
+	HFILE *HFILE_stdin;
+	/* Which signals have non-DFL handler (even with no traps set)?
+	 * Set at the start to:
+	 * (SIGQUIT + maybe SPECIAL_INTERACTIVE_SIGS + maybe SPECIAL_JOBSTOP_SIGS)
+	 * SPECIAL_INTERACTIVE_SIGS are cleared after fork.
+	 * The rest is cleared right before execv syscalls.
+	 * Other than these two times, never modified.
+	 */
+	unsigned special_sig_mask;
+#if ENABLE_HUSH_JOB
+	unsigned fatal_sig_mask;
+# define G_fatal_sig_mask (G.fatal_sig_mask)
+#else
+# define G_fatal_sig_mask 0
+#endif
+#if ENABLE_HUSH_TRAP
+	int pre_trap_exitcode;
+# if ENABLE_HUSH_FUNCTIONS
+	int return_exitcode;
+# endif
+	char **traps; /* char *traps[NSIG] */
+# define G_traps G.traps
+#else
+# define G_traps ((char**)NULL)
+#endif
+	sigset_t pending_set;
+#if ENABLE_HUSH_MEMLEAK
+	unsigned long memleak_value;
+#endif
+#if ENABLE_HUSH_MODE_X
+	unsigned x_mode_depth;
+	/* "set -x" output should not be redirectable with subsequent 2>FILE.
+	 * We dup fd#2 to x_mode_fd when "set -x" is executed, and use it
+	 * for all subsequent output.
+	 */
+	int x_mode_fd;
+	o_string x_mode_buf;
+#endif
+#if HUSH_DEBUG >= 2
+	int debug_indent;
+#endif
+	struct sigaction sa;
+	char optstring_buf[sizeof("eixcs")];
+#if BASH_EPOCH_VARS
+	char epoch_buf[sizeof("%llu.nnnnnn") + sizeof(long long)*3];
+#endif
+#if ENABLE_FEATURE_EDITING
+	char user_input_buf[CONFIG_FEATURE_EDITING_MAX_LEN];
+#endif
+};
+#define G (*ptr_to_globals)
+/* Not #defining name to G.name - this quickly gets unwieldy
+ * (too many defines). Also, I actually prefer to see when a variable
+ * is global, thus "G." prefix is a useful hint */
+#define INIT_G() do { \
+	SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
+	/* memset(&G.sa, 0, sizeof(G.sa)); */  \
+	sigfillset(&G.sa.sa_mask); \
+	G.sa.sa_flags = SA_RESTART; \
+} while (0)
+
+
+/* Function prototypes for builtins */
+static int builtin_cd(char **argv) FAST_FUNC;
+#if ENABLE_HUSH_ECHO
+static int builtin_echo(char **argv) FAST_FUNC;
+#endif
+static int builtin_eval(char **argv) FAST_FUNC;
+static int builtin_exec(char **argv) FAST_FUNC;
+static int builtin_exit(char **argv) FAST_FUNC;
+#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;
+#endif
+#if ENABLE_HUSH_GETOPTS
+static int builtin_getopts(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_HELP
+static int builtin_help(char **argv) FAST_FUNC;
+#endif
+#if MAX_HISTORY && ENABLE_FEATURE_EDITING
+static int builtin_history(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_LOCAL
+static int builtin_local(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_MEMLEAK
+static int builtin_memleak(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_PRINTF
+static int builtin_printf(char **argv) FAST_FUNC;
+#endif
+static int builtin_pwd(char **argv) FAST_FUNC;
+#if ENABLE_HUSH_READ
+static int builtin_read(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_SET
+static int builtin_set(char **argv) FAST_FUNC;
+#endif
+static int builtin_shift(char **argv) FAST_FUNC;
+static int builtin_source(char **argv) FAST_FUNC;
+#if ENABLE_HUSH_TEST || BASH_TEST2
+static int builtin_test(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_TRAP
+static int builtin_trap(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_TYPE
+static int builtin_type(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_TIMES
+static int builtin_times(char **argv) FAST_FUNC;
+#endif
+static int builtin_true(char **argv) FAST_FUNC;
+#if ENABLE_HUSH_UMASK
+static int builtin_umask(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_UNSET
+static int builtin_unset(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_KILL
+static int builtin_kill(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_WAIT
+static int builtin_wait(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_LOOPS
+static int builtin_break(char **argv) FAST_FUNC;
+static int builtin_continue(char **argv) FAST_FUNC;
+#endif
+#if ENABLE_HUSH_FUNCTIONS
+static int builtin_return(char **argv) FAST_FUNC;
+#endif
+
+/* Table of built-in functions.  They can be forked or not, depending on
+ * context: within pipes, they fork.  As simple commands, they do not.
+ * When used in non-forking context, they can change global variables
+ * in the parent shell process.  If forked, of course they cannot.
+ * For example, 'unset foo | whatever' will parse and run, but foo will
+ * still be set at the end. */
+struct built_in_command {
+	const char *b_cmd;
+	int (*b_function)(char **argv) FAST_FUNC;
+#if ENABLE_HUSH_HELP
+	const char *b_descr;
+# define BLTIN(cmd, func, help) { cmd, func, help }
+#else
+# define BLTIN(cmd, func, help) { cmd, func }
+#endif
+};
+
+static const struct built_in_command bltins1[] ALIGN_PTR = {
+	BLTIN("."        , builtin_source  , "Run commands in file"),
+	BLTIN(":"        , builtin_true    , NULL),
+#if ENABLE_HUSH_JOB
+	BLTIN("bg"       , builtin_fg_bg   , "Resume job in background"),
+#endif
+#if ENABLE_HUSH_LOOPS
+	BLTIN("break"    , builtin_break   , "Exit loop"),
+#endif
+	BLTIN("cd"       , builtin_cd      , "Change directory"),
+#if ENABLE_HUSH_LOOPS
+	BLTIN("continue" , builtin_continue, "Start new loop iteration"),
+#endif
+	BLTIN("eval"     , builtin_eval    , "Construct and run shell command"),
+	BLTIN("exec"     , builtin_exec    , "Execute command, don't return to shell"),
+	BLTIN("exit"     , builtin_exit    , NULL),
+#if ENABLE_HUSH_EXPORT
+	BLTIN("export"   , builtin_export  , "Set environment variables"),
+#endif
+#if ENABLE_HUSH_JOB
+	BLTIN("fg"       , builtin_fg_bg   , "Bring job to foreground"),
+#endif
+#if ENABLE_HUSH_GETOPTS
+	BLTIN("getopts"  , builtin_getopts , NULL),
+#endif
+#if ENABLE_HUSH_HELP
+	BLTIN("help"     , builtin_help    , NULL),
+#endif
+#if MAX_HISTORY && ENABLE_FEATURE_EDITING
+	BLTIN("history"  , builtin_history , "Show history"),
+#endif
+#if ENABLE_HUSH_JOB
+	BLTIN("jobs"     , builtin_jobs    , "List jobs"),
+#endif
+#if ENABLE_HUSH_KILL
+	BLTIN("kill"     , builtin_kill    , "Send signals to processes"),
+#endif
+#if ENABLE_HUSH_LOCAL
+	BLTIN("local"    , builtin_local   , "Set local variables"),
+#endif
+#if ENABLE_HUSH_MEMLEAK
+	BLTIN("memleak"  , builtin_memleak , NULL),
+#endif
+#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
+#if ENABLE_HUSH_SET
+	BLTIN("set"      , builtin_set     , "Set positional parameters"),
+#endif
+	BLTIN("shift"    , builtin_shift   , "Shift positional parameters"),
+#if BASH_SOURCE
+	BLTIN("source"   , builtin_source  , NULL),
+#endif
+#if ENABLE_HUSH_TIMES
+	BLTIN("times"    , builtin_times   , NULL),
+#endif
+#if ENABLE_HUSH_TRAP
+	BLTIN("trap"     , builtin_trap    , "Trap signals"),
+#endif
+	BLTIN("true"     , builtin_true    , NULL),
+#if ENABLE_HUSH_TYPE
+	BLTIN("type"     , builtin_type    , "Show command type"),
+#endif
+#if ENABLE_HUSH_ULIMIT
+	BLTIN("ulimit"   , shell_builtin_ulimit, "Control resource limits"),
+#endif
+#if ENABLE_HUSH_UMASK
+	BLTIN("umask"    , builtin_umask   , "Set file creation mask"),
+#endif
+#if ENABLE_HUSH_UNSET
+	BLTIN("unset"    , builtin_unset   , "Unset variables"),
+#endif
+#if ENABLE_HUSH_WAIT
+	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
+ * (it's cheaper to run an external program in this case):
+ */
+static const struct built_in_command bltins2[] ALIGN_PTR = {
+#if ENABLE_HUSH_TEST
+	BLTIN("["        , builtin_test    , NULL),
+#endif
+#if BASH_TEST2
+	BLTIN("[["       , builtin_test    , NULL),
+#endif
+#if ENABLE_HUSH_ECHO
+	BLTIN("echo"     , builtin_echo    , NULL),
+#endif
+#if ENABLE_HUSH_PRINTF
+	BLTIN("printf"   , builtin_printf  , NULL),
+#endif
+	BLTIN("pwd"      , builtin_pwd     , NULL),
+#if ENABLE_HUSH_TEST
+	BLTIN("test"     , builtin_test    , NULL),
+#endif
+};
+
+
+/* Debug printouts.
+ */
+#if HUSH_DEBUG >= 2
+/* prevent disasters with G.debug_indent < 0 */
+# define indent() fdprintf(2, "%*s", (G.debug_indent * 2) & 0xff, "")
+# define debug_enter() (G.debug_indent++)
+# define debug_leave() (G.debug_indent--)
+#else
+# define indent()      ((void)0)
+# define debug_enter() ((void)0)
+# define debug_leave() ((void)0)
+#endif
+
+#ifndef debug_printf
+# define debug_printf(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_parse
+# define debug_printf_parse(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_heredoc
+# define debug_printf_heredoc(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_exec
+#define debug_printf_exec(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_env
+# define debug_printf_env(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_jobs
+# define debug_printf_jobs(...) (indent(), fdprintf(2, __VA_ARGS__))
+# define DEBUG_JOBS 1
+#else
+# define DEBUG_JOBS 0
+#endif
+
+#ifndef debug_printf_expand
+# define debug_printf_expand(...) (indent(), fdprintf(2, __VA_ARGS__))
+# define DEBUG_EXPAND 1
+#else
+# define DEBUG_EXPAND 0
+#endif
+
+#ifndef debug_printf_varexp
+# define debug_printf_varexp(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_glob
+# define debug_printf_glob(...) (indent(), fdprintf(2, __VA_ARGS__))
+# define DEBUG_GLOB 1
+#else
+# define DEBUG_GLOB 0
+#endif
+
+#ifndef debug_printf_redir
+# define debug_printf_redir(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_list
+# define debug_printf_list(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_subst
+# define debug_printf_subst(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_prompt
+# define debug_printf_prompt(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
+#ifndef debug_printf_clean
+# define debug_printf_clean(...) (indent(), fdprintf(2, __VA_ARGS__))
+# define DEBUG_CLEAN 1
+#else
+# define DEBUG_CLEAN 0
+#endif
+
+#if DEBUG_EXPAND
+static void debug_print_strings(const char *prefix, char **vv)
+{
+	indent();
+	fdprintf(2, "%s:\n", prefix);
+	while (*vv)
+		fdprintf(2, " '%s'\n", *vv++);
+}
+#else
+# define debug_print_strings(prefix, vv) ((void)0)
+#endif
+
+
+/* Leak hunting. Use hush_leaktool.sh for post-processing.
+ */
+#if LEAK_HUNTING
+static void *xxmalloc(int lineno, size_t size)
+{
+	void *ptr = xmalloc((size + 0xff) & ~0xff);
+	fdprintf(2, "line %d: malloc %p\n", lineno, ptr);
+	return ptr;
+}
+static void *xxrealloc(int lineno, void *ptr, size_t size)
+{
+	ptr = xrealloc(ptr, (size + 0xff) & ~0xff);
+	fdprintf(2, "line %d: realloc %p\n", lineno, ptr);
+	return ptr;
+}
+static char *xxstrdup(int lineno, const char *str)
+{
+	char *ptr = xstrdup(str);
+	fdprintf(2, "line %d: strdup %p\n", lineno, ptr);
+	return ptr;
+}
+static void xxfree(void *ptr)
+{
+	fdprintf(2, "free %p\n", ptr);
+	free(ptr);
+}
+# define xmalloc(s)     xxmalloc(__LINE__, s)
+# define xrealloc(p, s) xxrealloc(__LINE__, p, s)
+# define xstrdup(s)     xxstrdup(__LINE__, s)
+# define free(p)        xxfree(p)
+#endif
+
+
+/* Syntax and runtime errors. They always abort scripts.
+ * In interactive use they usually discard unparsed and/or unexecuted commands
+ * and return to the prompt.
+ * HUSH_DEBUG >= 2 prints line number in this file where it was detected.
+ */
+#if HUSH_DEBUG < 2
+# define msg_and_die_if_script(lineno, ...)     msg_and_die_if_script(__VA_ARGS__)
+# define syntax_error(lineno, msg)              syntax_error(msg)
+# define syntax_error_at(lineno, msg)           syntax_error_at(msg)
+# define syntax_error_unterm_ch(lineno, ch)     syntax_error_unterm_ch(ch)
+# define syntax_error_unterm_str(lineno, s)     syntax_error_unterm_str(s)
+# define syntax_error_unexpected_ch(lineno, ch) syntax_error_unexpected_ch(ch)
+#endif
+
+static void die_if_script(void)
+{
+	if (!G_interactive_fd) {
+		if (G.last_exitcode) /* sometines it's 2, not 1 (bash compat) */
+			xfunc_error_retval = G.last_exitcode;
+		xfunc_die();
+	}
+}
+
+static void msg_and_die_if_script(unsigned lineno, const char *fmt, ...)
+{
+	va_list p;
+
+#if HUSH_DEBUG >= 2
+	bb_error_msg("hush.c:%u", lineno);
+#endif
+	va_start(p, fmt);
+	bb_verror_msg(fmt, p, NULL);
+	va_end(p);
+	die_if_script();
+}
+
+static void syntax_error(unsigned lineno UNUSED_PARAM, const char *msg)
+{
+	if (msg)
+		bb_error_msg("syntax error: %s", msg);
+	else
+		bb_simple_error_msg("syntax error");
+	die_if_script();
+}
+
+static void syntax_error_at(unsigned lineno UNUSED_PARAM, const char *msg)
+{
+	bb_error_msg("syntax error at '%s'", msg);
+	die_if_script();
+}
+
+static void syntax_error_unterm_str(unsigned lineno UNUSED_PARAM, const char *s)
+{
+	bb_error_msg("syntax error: unterminated %s", s);
+//? source4.tests fails: in bash, echo ${^} in script does not terminate the script
+//	die_if_script();
+}
+
+static void syntax_error_unterm_ch(unsigned lineno, char ch)
+{
+	char msg[2] = { ch, '\0' };
+	syntax_error_unterm_str(lineno, msg);
+}
+
+static void syntax_error_unexpected_ch(unsigned lineno UNUSED_PARAM, int ch)
+{
+	char msg[2];
+	msg[0] = ch;
+	msg[1] = '\0';
+#if HUSH_DEBUG >= 2
+	bb_error_msg("hush.c:%u", lineno);
+#endif
+	bb_error_msg("syntax error: unexpected %s", ch == EOF ? "EOF" : msg);
+	die_if_script();
+}
+
+#if HUSH_DEBUG < 2
+# undef msg_and_die_if_script
+# undef syntax_error
+# undef syntax_error_at
+# undef syntax_error_unterm_ch
+# undef syntax_error_unterm_str
+# undef syntax_error_unexpected_ch
+#else
+# define msg_and_die_if_script(...)     msg_and_die_if_script(__LINE__, __VA_ARGS__)
+# define syntax_error(msg)              syntax_error(__LINE__, msg)
+# define syntax_error_at(msg)           syntax_error_at(__LINE__, msg)
+# define syntax_error_unterm_ch(ch)     syntax_error_unterm_ch(__LINE__, ch)
+# define syntax_error_unterm_str(s)     syntax_error_unterm_str(__LINE__, s)
+# define syntax_error_unexpected_ch(ch) syntax_error_unexpected_ch(__LINE__, ch)
+#endif
+
+
+/* Utility functions
+ */
+/* Replace each \x with x in place, return ptr past NUL. */
+static char *unbackslash(char *src)
+{
+	char *dst = src = strchrnul(src, '\\');
+	while (1) {
+		if (*src == '\\') {
+			src++;
+			if (*src != '\0') {
+				/* \x -> x */
+				*dst++ = *src++;
+				continue;
+			}
+			/* else: "\<nul>". Do not delete this backslash.
+			 * Testcase: eval 'echo ok\'
+			 */
+			*dst++ = '\\';
+			/* fallthrough */
+		}
+		if ((*dst++ = *src++) == '\0')
+			break;
+	}
+	return dst;
+}
+
+static char **add_strings_to_strings(char **strings, char **add, int need_to_dup)
+{
+	int i;
+	unsigned count1;
+	unsigned count2;
+	char **v;
+
+	v = strings;
+	count1 = 0;
+	if (v) {
+		while (*v) {
+			count1++;
+			v++;
+		}
+	}
+	count2 = 0;
+	v = add;
+	while (*v) {
+		count2++;
+		v++;
+	}
+	v = xrealloc(strings, (count1 + count2 + 1) * sizeof(char*));
+	v[count1 + count2] = NULL;
+	i = count2;
+	while (--i >= 0)
+		v[count1 + i] = (need_to_dup ? xstrdup(add[i]) : add[i]);
+	return v;
+}
+#if LEAK_HUNTING
+static char **xx_add_strings_to_strings(int lineno, char **strings, char **add, int need_to_dup)
+{
+	char **ptr = add_strings_to_strings(strings, add, need_to_dup);
+	fdprintf(2, "line %d: add_strings_to_strings %p\n", lineno, ptr);
+	return ptr;
+}
+#define add_strings_to_strings(strings, add, need_to_dup) \
+	xx_add_strings_to_strings(__LINE__, strings, add, need_to_dup)
+#endif
+
+/* Note: takes ownership of "add" ptr (it is not strdup'ed) */
+static char **add_string_to_strings(char **strings, char *add)
+{
+	char *v[2];
+	v[0] = add;
+	v[1] = NULL;
+	return add_strings_to_strings(strings, v, /*dup:*/ 0);
+}
+#if LEAK_HUNTING
+static char **xx_add_string_to_strings(int lineno, char **strings, char *add)
+{
+	char **ptr = add_string_to_strings(strings, add);
+	fdprintf(2, "line %d: add_string_to_strings %p\n", lineno, ptr);
+	return ptr;
+}
+#define add_string_to_strings(strings, add) \
+	xx_add_string_to_strings(__LINE__, strings, add)
+#endif
+
+static void free_strings(char **strings)
+{
+	char **v;
+
+	if (!strings)
+		return;
+	v = strings;
+	while (*v) {
+		free(*v);
+		v++;
+	}
+	free(strings);
+}
+
+static int dup_CLOEXEC(int fd, int avoid_fd)
+{
+	int newfd;
+ repeat:
+	newfd = fcntl(fd, F_DUPFD_CLOEXEC, avoid_fd + 1);
+	if (newfd >= 0) {
+		if (F_DUPFD_CLOEXEC == F_DUPFD) /* if old libc (w/o F_DUPFD_CLOEXEC) */
+			fcntl(newfd, F_SETFD, FD_CLOEXEC);
+	} else { /* newfd < 0 */
+		if (errno == EBUSY)
+			goto repeat;
+		if (errno == EINTR)
+			goto repeat;
+	}
+	return newfd;
+}
+
+static int xdup_CLOEXEC_and_close(int fd, int avoid_fd)
+{
+	int newfd;
+ repeat:
+	newfd = fcntl(fd, F_DUPFD_CLOEXEC, avoid_fd + 1);
+	if (newfd < 0) {
+		if (errno == EBUSY)
+			goto repeat;
+		if (errno == EINTR)
+			goto repeat;
+		/* fd was not open? */
+		if (errno == EBADF)
+			return fd;
+		xfunc_die();
+	}
+	if (F_DUPFD_CLOEXEC == F_DUPFD) /* if old libc (w/o F_DUPFD_CLOEXEC) */
+		fcntl(newfd, F_SETFD, FD_CLOEXEC);
+	close(fd);
+	return newfd;
+}
+
+
+/* Manipulating HFILEs */
+static HFILE *hfopen(const char *name)
+{
+	HFILE *fp;
+	int fd;
+
+	fd = STDIN_FILENO;
+	if (name) {
+		fd = open(name, O_RDONLY | O_CLOEXEC);
+		if (fd < 0)
+			return NULL;
+		if (O_CLOEXEC == 0) /* ancient libc */
+			close_on_exec_on(fd);
+	}
+
+	fp = xmalloc(sizeof(*fp));
+	if (name == NULL)
+		G.HFILE_stdin = fp;
+	fp->fd = fd;
+	fp->cur = fp->end = fp->buf;
+	fp->next_hfile = G.HFILE_list;
+	G.HFILE_list = fp;
+	return fp;
+}
+static void hfclose(HFILE *fp)
+{
+	HFILE **pp = &G.HFILE_list;
+	while (*pp) {
+		HFILE *cur = *pp;
+		if (cur == fp) {
+			*pp = cur->next_hfile;
+			break;
+		}
+		pp = &cur->next_hfile;
+	}
+	if (fp->fd >= 0)
+		close(fp->fd);
+	free(fp);
+}
+static int refill_HFILE_and_getc(HFILE *fp)
+{
+	int n;
+
+	if (fp->fd < 0) {
+		/* Already saw EOF */
+		return EOF;
+	}
+#if ENABLE_HUSH_INTERACTIVE && !ENABLE_FEATURE_EDITING
+	/* If user presses ^C, read() restarts after SIGINT (we use SA_RESTART).
+	 * IOW: ^C will not immediately stop line input.
+	 * But poll() is different: it does NOT restart after signals.
+	 */
+	if (fp == G.HFILE_stdin) {
+		struct pollfd pfd[1];
+		pfd[0].fd = fp->fd;
+		pfd[0].events = POLLIN;
+		n = poll(pfd, 1, -1);
+		if (n < 0
+		 /*&& errno == EINTR - assumed true */
+		 && sigismember(&G.pending_set, SIGINT)
+		) {
+			return '\0';
+		}
+	}
+#else
+/* if FEATURE_EDITING=y, we do not use this routine for interactive input */
+#endif
+	/* Try to buffer more input */
+	n = safe_read(fp->fd, fp->buf, sizeof(fp->buf));
+	if (n < 0) {
+		bb_simple_perror_msg("read error");
+		n = 0;
+	}
+	fp->cur = fp->buf;
+	fp->end = fp->buf + n;
+	if (n == 0) {
+		/* EOF/error */
+		close(fp->fd);
+		fp->fd = -1;
+		return EOF;
+	}
+	return (unsigned char)(*fp->cur++);
+}
+/* Inlined for common case of non-empty buffer.
+ */
+static ALWAYS_INLINE int hfgetc(HFILE *fp)
+{
+	if (fp->cur < fp->end)
+		return (unsigned char)(*fp->cur++);
+	/* Buffer empty */
+	return refill_HFILE_and_getc(fp);
+}
+static int move_HFILEs_on_redirect(int fd, int avoid_fd)
+{
+	HFILE *fl = G.HFILE_list;
+	while (fl) {
+		if (fd == fl->fd) {
+			/* We use it only on script files, they are all CLOEXEC */
+			fl->fd = xdup_CLOEXEC_and_close(fd, avoid_fd);
+			debug_printf_redir("redirect_fd %d: matches a script fd, moving it to %d\n", fd, fl->fd);
+			return 1; /* "found and moved" */
+		}
+		fl = fl->next_hfile;
+	}
+#if ENABLE_HUSH_MODE_X
+	if (G.x_mode_fd > 0 && fd == G.x_mode_fd) {
+		G.x_mode_fd = xdup_CLOEXEC_and_close(fd, avoid_fd);
+		return 1; /* "found and moved" */
+	}
+#endif
+	return 0; /* "not in the list" */
+}
+#if ENABLE_FEATURE_SH_STANDALONE && BB_MMU
+static void close_all_HFILE_list(void)
+{
+	HFILE *fl = G.HFILE_list;
+	while (fl) {
+		/* hfclose would also free HFILE object.
+		 * It is disastrous if we share memory with a vforked parent.
+		 * I'm not sure we never come here after vfork.
+		 * Therefore just close fd, nothing more.
+		 *
+		 * ">" instead of ">=": we don't close fd#0,
+		 * interactive shell uses hfopen(NULL) as stdin input
+		 * which has fl->fd == 0, but fd#0 gets redirected in pipes.
+		 * If we'd close it here, then e.g. interactive "set | sort"
+		 * with NOFORKed sort, would have sort's input fd closed.
+		 */
+		if (fl->fd > 0)
+			/*hfclose(fl); - unsafe */
+			close(fl->fd);
+		fl = fl->next_hfile;
+	}
+}
+#endif
+static int fd_in_HFILEs(int fd)
+{
+	HFILE *fl = G.HFILE_list;
+	while (fl) {
+		if (fl->fd == fd)
+			return 1;
+		fl = fl->next_hfile;
+	}
+	return 0;
+}
+
+
+/* Helpers for setting new $n and restoring them back
+ */
+typedef struct save_arg_t {
+	char *sv_argv0;
+	char **sv_g_argv;
+	int sv_g_argc;
+	IF_HUSH_SET(smallint sv_g_malloced;)
+} save_arg_t;
+
+static void save_and_replace_G_args(save_arg_t *sv, char **argv)
+{
+	sv->sv_argv0 = argv[0];
+	sv->sv_g_argv = G.global_argv;
+	sv->sv_g_argc = G.global_argc;
+	IF_HUSH_SET(sv->sv_g_malloced = G.global_args_malloced;)
+
+	argv[0] = G.global_argv[0]; /* retain $0 */
+	G.global_argv = argv;
+	IF_HUSH_SET(G.global_args_malloced = 0;)
+
+	G.global_argc = 1 + string_array_len(argv + 1);
+}
+
+static void restore_G_args(save_arg_t *sv, char **argv)
+{
+#if ENABLE_HUSH_SET
+	if (G.global_args_malloced) {
+		/* someone ran "set -- arg1 arg2 ...", undo */
+		char **pp = G.global_argv;
+		while (*++pp) /* note: does not free $0 */
+			free(*pp);
+		free(G.global_argv);
+	}
+#endif
+	argv[0] = sv->sv_argv0;
+	G.global_argv = sv->sv_g_argv;
+	G.global_argc = sv->sv_g_argc;
+	IF_HUSH_SET(G.global_args_malloced = sv->sv_g_malloced;)
+}
+
+
+/* Basic theory of signal handling in shell
+ * ========================================
+ * This does not describe what hush does, rather, it is current understanding
+ * what it _should_ do. If it doesn't, it's a bug.
+ * http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#trap
+ *
+ * Signals are handled only after each pipe ("cmd | cmd | cmd" thing)
+ * is finished or backgrounded. It is the same in interactive and
+ * non-interactive shells, and is the same regardless of whether
+ * a user trap handler is installed or a shell special one is in effect.
+ * ^C or ^Z from keyboard seems to execute "at once" because it usually
+ * backgrounds (i.e. stops) or kills all members of currently running
+ * pipe.
+ *
+ * Wait builtin is interruptible by signals for which user trap is set
+ * or by SIGINT in interactive shell.
+ *
+ * Trap handlers will execute even within trap handlers. (right?)
+ *
+ * User trap handlers are forgotten when subshell ("(cmd)") is entered,
+ * except for handlers set to '' (empty string).
+ *
+ * If job control is off, backgrounded commands ("cmd &")
+ * have SIGINT, SIGQUIT set to SIG_IGN.
+ *
+ * Commands which are run in command substitution ("`cmd`")
+ * have SIGTTIN, SIGTTOU, SIGTSTP set to SIG_IGN.
+ *
+ * Ordinary commands have signals set to SIG_IGN/DFL as inherited
+ * by the shell from its parent.
+ *
+ * Signals which differ from SIG_DFL action
+ * (note: child (i.e., [v]forked) shell is not an interactive shell):
+ *
+ * SIGQUIT: ignore
+ * SIGTERM (interactive): ignore
+ * SIGHUP (interactive):
+ *    send SIGCONT to stopped jobs, send SIGHUP to all jobs and exit
+ * SIGTTIN, SIGTTOU, SIGTSTP (if job control is on): ignore
+ *    Note that ^Z is handled not by trapping SIGTSTP, but by seeing
+ *    that all pipe members are stopped. Try this in bash:
+ *    while :; do :; done - ^Z does not background it
+ *    (while :; do :; done) - ^Z backgrounds it
+ * SIGINT (interactive): wait for last pipe, ignore the rest
+ *    of the command line, show prompt. NB: ^C does not send SIGINT
+ *    to interactive shell while shell is waiting for a pipe,
+ *    since shell is bg'ed (is not in foreground process group).
+ *    Example 1: this waits 5 sec, but does not execute ls:
+ *    "echo $$; sleep 5; ls -l" + "kill -INT <pid>"
+ *    Example 2: this does not wait and does not execute ls:
+ *    "echo $$; sleep 5 & wait; ls -l" + "kill -INT <pid>"
+ *    Example 3: this does not wait 5 sec, but executes ls:
+ *    "sleep 5; ls -l" + press ^C
+ *    Example 4: this does not wait and does not execute ls:
+ *    "sleep 5 & wait; ls -l" + press ^C
+ *
+ * (What happens to signals which are IGN on shell start?)
+ * (What happens with signal mask on shell start?)
+ *
+ * Old implementation
+ * ==================
+ * We use in-kernel pending signal mask to determine which signals were sent.
+ * We block all signals which we don't want to take action immediately,
+ * i.e. we block all signals which need to have special handling as described
+ * above, and all signals which have traps set.
+ * After each pipe execution, we extract any pending signals via sigtimedwait()
+ * and act on them.
+ *
+ * unsigned special_sig_mask: a mask of such "special" signals
+ * sigset_t blocked_set:  current blocked signal set
+ *
+ * "trap - SIGxxx":
+ *    clear bit in blocked_set unless it is also in special_sig_mask
+ * "trap 'cmd' SIGxxx":
+ *    set bit in blocked_set (even if 'cmd' is '')
+ * after [v]fork, if we plan to be a shell:
+ *    unblock signals with special interactive handling
+ *    (child shell is not interactive),
+ *    unset all traps except '' (note: regardless of child shell's type - {}, (), etc)
+ * after [v]fork, if we plan to exec:
+ *    POSIX says fork clears pending signal mask in child - no need to clear it.
+ *    Restore blocked signal set to one inherited by shell just prior to exec.
+ *
+ * Note: as a result, we do not use signal handlers much. The only uses
+ * are to count SIGCHLDs
+ * and to restore tty pgrp on signal-induced exit.
+ *
+ * Note 2 (compat):
+ * Standard says "When a subshell is entered, traps that are not being ignored
+ * are set to the default actions". bash interprets it so that traps which
+ * are set to '' (ignore) are NOT reset to defaults. We do the same.
+ *
+ * Problem: the above approach makes it unwieldy to catch signals while
+ * we are in read builtin, or while we read commands from stdin:
+ * masked signals are not visible!
+ *
+ * New implementation
+ * ==================
+ * We record each signal we are interested in by installing signal handler
+ * for them - a bit like emulating kernel pending signal mask in userspace.
+ * We are interested in: signals which need to have special handling
+ * as described above, and all signals which have traps set.
+ * Signals are recorded in pending_set.
+ * After each pipe execution, we extract any pending signals
+ * and act on them.
+ *
+ * unsigned special_sig_mask: a mask of shell-special signals.
+ * unsigned fatal_sig_mask: a mask of signals on which we restore tty pgrp.
+ * char *traps[sig] if trap for sig is set (even if it's '').
+ * sigset_t pending_set: set of sigs we received.
+ *
+ * "trap - SIGxxx":
+ *    if sig is in special_sig_mask, set handler back to:
+ *        record_pending_signo, or to IGN if it's a tty stop signal
+ *    if sig is in fatal_sig_mask, set handler back to sigexit.
+ *    else: set handler back to SIG_DFL
+ * "trap 'cmd' SIGxxx":
+ *    set handler to record_pending_signo.
+ * "trap '' SIGxxx":
+ *    set handler to SIG_IGN.
+ * after [v]fork, if we plan to be a shell:
+ *    set signals with special interactive handling to SIG_DFL
+ *    (because child shell is not interactive),
+ *    unset all traps except '' (note: regardless of child shell's type - {}, (), etc)
+ * after [v]fork, if we plan to exec:
+ *    POSIX says fork clears pending signal mask in child - no need to clear it.
+ *
+ * To make wait builtin interruptible, we handle SIGCHLD as special signal,
+ * otherwise (if we leave it SIG_DFL) sigsuspend in wait builtin will not wake up on it.
+ *
+ * Note (compat):
+ * Standard says "When a subshell is entered, traps that are not being ignored
+ * are set to the default actions". bash interprets it so that traps which
+ * are set to '' (ignore) are NOT reset to defaults. We do the same.
+ */
+enum {
+	SPECIAL_INTERACTIVE_SIGS = 0
+		| (1 << SIGTERM)
+		| (1 << SIGINT)
+		| (1 << SIGHUP)
+		,
+	SPECIAL_JOBSTOP_SIGS = 0
+#if ENABLE_HUSH_JOB
+		| (1 << SIGTTIN)
+		| (1 << SIGTTOU)
+		| (1 << SIGTSTP)
+#endif
+		,
+};
+
+static void record_pending_signo(int sig)
+{
+	sigaddset(&G.pending_set, sig);
+#if ENABLE_HUSH_FAST
+	if (sig == SIGCHLD) {
+		G.count_SIGCHLD++;
+//bb_error_msg("[%d] SIGCHLD_handler: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+	}
+#endif
+}
+
+static sighandler_t install_sighandler(int sig, sighandler_t handler)
+{
+	struct sigaction old_sa;
+
+	/* We could use signal() to install handlers... almost:
+	 * except that we need to mask ALL signals while handlers run.
+	 * I saw signal nesting in strace, race window isn't small.
+	 * SA_RESTART is also needed, but in Linux, signal()
+	 * sets SA_RESTART too.
+	 */
+	/* memset(&G.sa, 0, sizeof(G.sa)); - already done */
+	/* sigfillset(&G.sa.sa_mask);      - already done */
+	/* G.sa.sa_flags = SA_RESTART;     - already done */
+	G.sa.sa_handler = handler;
+	sigaction(sig, &G.sa, &old_sa);
+	return old_sa.sa_handler;
+}
+
+static void hush_exit(int exitcode) NORETURN;
+
+static void restore_ttypgrp_and__exit(void) NORETURN;
+static void restore_ttypgrp_and__exit(void)
+{
+	/* xfunc has failed! die die die */
+	/* no EXIT traps, this is an escape hatch! */
+	G.exiting = 1;
+	hush_exit(xfunc_error_retval);
+}
+
+#if ENABLE_HUSH_JOB
+
+/* Needed only on some libc:
+ * It was observed that on exit(), fgetc'ed buffered data
+ * gets "unwound" via lseek(fd, -NUM, SEEK_CUR).
+ * With the net effect that even after fork(), not vfork(),
+ * exit() in NOEXECed applet in "sh SCRIPT":
+ *	noexec_applet_here
+ *	echo END_OF_SCRIPT
+ * lseeks fd in input FILE object from EOF to "e" in "echo END_OF_SCRIPT".
+ * This makes "echo END_OF_SCRIPT" executed twice.
+ * Similar problems can be seen with msg_and_die_if_script() -> xfunc_die()
+ * and in `cmd` handling.
+ * If set as die_func(), this makes xfunc_die() exit via _exit(), not exit():
+ */
+static void fflush_and__exit(void) NORETURN;
+static void fflush_and__exit(void)
+{
+	fflush_all();
+	_exit(xfunc_error_retval);
+}
+
+/* After [v]fork, in child: do not restore tty pgrp on xfunc death */
+# define disable_restore_tty_pgrp_on_exit() (die_func = fflush_and__exit)
+/* After [v]fork, in parent: restore tty pgrp on xfunc death */
+# define enable_restore_tty_pgrp_on_exit()  (die_func = restore_ttypgrp_and__exit)
+
+/* Restores tty foreground process group, and exits.
+ * May be called as signal handler for fatal signal
+ * (will resend signal to itself, producing correct exit state)
+ * or called directly with -EXITCODE.
+ * We also call it if xfunc is exiting.
+ */
+static void sigexit(int sig) NORETURN;
+static void sigexit(int sig)
+{
+	/* Careful: we can end up here after [v]fork. Do not restore
+	 * tty pgrp then, only top-level shell process does that */
+	if (G_saved_tty_pgrp && getpid() == G.root_pid) {
+		/* Disable all signals: job control, SIGPIPE, etc.
+		 * Mostly paranoid measure, to prevent infinite SIGTTOU.
+		 */
+		sigprocmask_allsigs(SIG_BLOCK);
+		tcsetpgrp(G_interactive_fd, G_saved_tty_pgrp);
+	}
+
+	/* Not a signal, just exit */
+	if (sig <= 0)
+		_exit(- sig);
+
+	kill_myself_with_sig(sig); /* does not return */
+}
+#else
+
+# define disable_restore_tty_pgrp_on_exit() ((void)0)
+# define enable_restore_tty_pgrp_on_exit()  ((void)0)
+
+#endif
+
+static sighandler_t pick_sighandler(unsigned sig)
+{
+	sighandler_t handler = SIG_DFL;
+	if (sig < sizeof(unsigned)*8) {
+		unsigned sigmask = (1 << sig);
+
+#if ENABLE_HUSH_JOB
+		/* is sig fatal? */
+		if (G_fatal_sig_mask & sigmask)
+			handler = sigexit;
+		else
+#endif
+		/* sig has special handling? */
+		if (G.special_sig_mask & sigmask) {
+			handler = record_pending_signo;
+			/* TTIN/TTOU/TSTP can't be set to record_pending_signo
+			 * in order to ignore them: they will be raised
+			 * in an endless loop when we try to do some
+			 * terminal ioctls! We do have to _ignore_ these.
+			 */
+			if (SPECIAL_JOBSTOP_SIGS & sigmask)
+				handler = SIG_IGN;
+		}
+	}
+	return handler;
+}
+
+/* Restores tty foreground process group, and exits. */
+static void hush_exit(int exitcode)
+{
+#if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+	save_history(G.line_input_state); /* may be NULL */
+#endif
+
+	fflush_all();
+	if (G.exiting <= 0 && G_traps && G_traps[0] && G_traps[0][0]) {
+		char *argv[3];
+		/* argv[0] is unused */
+		argv[1] = xstrdup(G_traps[0]); /* copy, since EXIT trap handler may modify G_traps[0] */
+		argv[2] = NULL;
+		G.exiting = 1; /* prevent EXIT trap recursion */
+		/* Note: G_traps[0] is not cleared!
+		 * "trap" will still show it, if executed
+		 * in the handler */
+		builtin_eval(argv);
+	}
+
+#if ENABLE_FEATURE_CLEAN_UP
+	{
+		struct variable *cur_var;
+		if (G.cwd != bb_msg_unknown)
+			free((char*)G.cwd);
+		cur_var = G.top_var;
+		while (cur_var) {
+			struct variable *tmp = cur_var;
+			if (!cur_var->max_len)
+				free(cur_var->varstr);
+			cur_var = cur_var->next;
+			free(tmp);
+		}
+	}
+#endif
+
+	fflush_all();
+#if ENABLE_HUSH_JOB
+	sigexit(- (exitcode & 0xff));
+#else
+	_exit(exitcode);
+#endif
+}
+
+//TODO: return a mask of ALL handled sigs?
+static int check_and_run_traps(void)
+{
+	int last_sig = 0;
+
+	while (1) {
+		int sig;
+
+		if (sigisemptyset(&G.pending_set))
+			break;
+		sig = 0;
+		do {
+			sig++;
+			if (sigismember(&G.pending_set, sig)) {
+				sigdelset(&G.pending_set, sig);
+				goto got_sig;
+			}
+		} while (sig < NSIG);
+		break;
+ got_sig:
+#if ENABLE_HUSH_TRAP
+		if (G_traps && G_traps[sig]) {
+			debug_printf_exec("%s: sig:%d handler:'%s'\n", __func__, sig, G.traps[sig]);
+			if (G_traps[sig][0]) {
+				/* We have user-defined handler */
+				smalluint save_rcode;
+				int save_pre;
+				char *argv[3];
+				/* argv[0] is unused */
+				argv[1] = xstrdup(G_traps[sig]);
+				/* why strdup? trap can modify itself: trap 'trap "echo oops" INT' INT */
+				argv[2] = NULL;
+				save_pre = G.pre_trap_exitcode;
+				G.pre_trap_exitcode = save_rcode = G.last_exitcode;
+				builtin_eval(argv);
+				free(argv[1]);
+				G.pre_trap_exitcode = save_pre;
+				G.last_exitcode = save_rcode;
+# if ENABLE_HUSH_FUNCTIONS
+				if (G.return_exitcode >= 0) {
+					debug_printf_exec("trap exitcode:%d\n", G.return_exitcode);
+					G.last_exitcode = G.return_exitcode;
+				}
+# endif
+				last_sig = sig;
+			} /* else: "" trap, ignoring signal */
+			continue;
+		}
+#endif
+		/* not a trap: special action */
+		switch (sig) {
+		case SIGINT:
+			debug_printf_exec("%s: sig:%d default SIGINT handler\n", __func__, sig);
+			G.flag_SIGINT = 1;
+			last_sig = sig;
+			break;
+#if ENABLE_HUSH_JOB
+		case SIGHUP: {
+//TODO: why are we doing this? ash and dash don't do this,
+//they have no handler for SIGHUP at all,
+//they rely on kernel to send SIGHUP+SIGCONT to orphaned process groups
+			struct pipe *job;
+			debug_printf_exec("%s: sig:%d default SIGHUP handler\n", __func__, sig);
+			/* bash is observed to signal whole process groups,
+			 * not individual processes */
+			for (job = G.job_list; job; job = job->next) {
+				if (job->pgrp <= 0)
+					continue;
+				debug_printf_exec("HUPing pgrp %d\n", job->pgrp);
+				if (kill(- job->pgrp, SIGHUP) == 0)
+					kill(- job->pgrp, SIGCONT);
+			}
+			sigexit(SIGHUP);
+		}
+#endif
+#if ENABLE_HUSH_FAST
+		case SIGCHLD:
+			debug_printf_exec("%s: sig:%d default SIGCHLD handler\n", __func__, sig);
+			G.count_SIGCHLD++;
+//bb_error_msg("[%d] check_and_run_traps: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+			/* Note:
+			 * We don't do 'last_sig = sig' here -> NOT returning this sig.
+			 * This simplifies wait builtin a bit.
+			 */
+			break;
+#endif
+		default: /* ignored: */
+			debug_printf_exec("%s: sig:%d default handling is to ignore\n", __func__, sig);
+			/* SIGTERM, SIGQUIT, SIGTTIN, SIGTTOU, SIGTSTP */
+			/* Note:
+			 * We don't do 'last_sig = sig' here -> NOT returning this sig.
+			 * Example: wait is not interrupted by TERM
+			 * in interactive shell, because TERM is ignored.
+			 */
+			break;
+		}
+	}
+	return last_sig;
+}
+
+
+static const char *get_cwd(int force)
+{
+	if (force || G.cwd == NULL) {
+		/* xrealloc_getcwd_or_warn(arg) calls free(arg),
+		 * we must not try to free(bb_msg_unknown) */
+		if (G.cwd == bb_msg_unknown)
+			G.cwd = NULL;
+		G.cwd = xrealloc_getcwd_or_warn((char *)G.cwd);
+		if (!G.cwd)
+			G.cwd = bb_msg_unknown;
+	}
+	return G.cwd;
+}
+
+
+/*
+ * Shell and environment variable support
+ */
+static struct variable **get_ptr_to_local_var(const char *name, unsigned len)
+{
+	struct variable **pp;
+	struct variable *cur;
+
+	pp = &G.top_var;
+	while ((cur = *pp) != NULL) {
+		if (strncmp(cur->varstr, name, len) == 0 && cur->varstr[len] == '=')
+			return pp;
+		pp = &cur->next;
+	}
+	return NULL;
+}
+
+static const char* FAST_FUNC get_local_var_value(const char *name)
+{
+	struct variable **vpp;
+	unsigned len = strlen(name);
+
+	if (G.expanded_assignments) {
+		char **cpp = G.expanded_assignments;
+		while (*cpp) {
+			char *cp = *cpp;
+			if (strncmp(cp, name, len) == 0 && cp[len] == '=')
+				return cp + len + 1;
+			cpp++;
+		}
+	}
+
+	vpp = get_ptr_to_local_var(name, len);
+	if (vpp)
+		return (*vpp)->varstr + len + 1;
+
+	if (strcmp(name, "PPID") == 0)
+		return utoa(G.root_ppid);
+	// bash compat: UID? EUID?
+#if ENABLE_HUSH_RANDOM_SUPPORT
+	if (strcmp(name, "RANDOM") == 0)
+		return utoa(next_random(&G.random_gen));
+#endif
+#if ENABLE_HUSH_LINENO_VAR
+	if (strcmp(name, "LINENO") == 0)
+		return utoa(G.execute_lineno);
+#endif
+#if BASH_EPOCH_VARS
+	{
+		const char *fmt = NULL;
+		if (strcmp(name, "EPOCHSECONDS") == 0)
+			fmt = "%llu";
+		else if (strcmp(name, "EPOCHREALTIME") == 0)
+			fmt = "%llu.%06u";
+		if (fmt) {
+			struct timeval tv;
+			xgettimeofday(&tv);
+			sprintf(G.epoch_buf, fmt, (unsigned long long)tv.tv_sec,
+					(unsigned)tv.tv_usec);
+			return G.epoch_buf;
+		}
+	}
+#endif
+	return NULL;
+}
+
+#if ENABLE_HUSH_GETOPTS
+static void handle_changed_special_names(const char *name, unsigned name_len)
+{
+	if (name_len == 6) {
+		if (strncmp(name, "OPTIND", 6) == 0) {
+			G.getopt_count = 0;
+			return;
+		}
+	}
+}
+#else
+/* Do not even bother evaluating arguments */
+# define handle_changed_special_names(...) ((void)0)
+#endif
+
+/* str holds "NAME=VAL" and is expected to be malloced.
+ * We take ownership of it.
+ */
+#define SETFLAG_EXPORT   (1 << 0)
+#define SETFLAG_UNEXPORT (1 << 1)
+#define SETFLAG_MAKE_RO  (1 << 2)
+#define SETFLAG_VARLVL_SHIFT   3
+static int set_local_var(char *str, unsigned flags)
+{
+	struct variable **cur_pp;
+	struct variable *cur;
+	char *free_me = NULL;
+	char *eq_sign;
+	int name_len;
+	int retval;
+	unsigned local_lvl = (flags >> SETFLAG_VARLVL_SHIFT);
+
+	eq_sign = strchr(str, '=');
+	if (HUSH_DEBUG && !eq_sign)
+		bb_simple_error_msg_and_die("BUG in setvar");
+
+	name_len = eq_sign - str + 1; /* including '=' */
+	cur_pp = &G.top_var;
+	while ((cur = *cur_pp) != NULL) {
+		if (strncmp(cur->varstr, str, name_len) != 0) {
+			cur_pp = &cur->next;
+			continue;
+		}
+
+		/* We found an existing var with this name */
+		if (cur->flg_read_only) {
+			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 (flags & SETFLAG_UNEXPORT) { // && cur->flg_export ?
+			debug_printf_env("%s: unsetenv '%s'\n", __func__, str);
+			*eq_sign = '\0';
+			unsetenv(str);
+			*eq_sign = '=';
+		}
+		if (cur->var_nest_level < local_lvl) {
+			/* bash 3.2.33(1) and exported vars:
+			 * # export z=z
+			 * # f() { local z=a; env | grep ^z; }
+			 * # f
+			 * z=a
+			 * # env | grep ^z
+			 * z=z
+			 */
+			if (cur->flg_export)
+				flags |= SETFLAG_EXPORT;
+			/* New variable is local ("local VAR=VAL" or
+			 * "VAR=VAL cmd")
+			 * and existing one is global, or local
+			 * on a lower level that new one.
+			 * Remove it from global variable list:
+			 */
+			*cur_pp = cur->next;
+			if (G.shadowed_vars_pp) {
+				/* Save in "shadowed" list */
+				debug_printf_env("shadowing %s'%s'/%u by '%s'/%u\n",
+					cur->flg_export ? "exported " : "",
+					cur->varstr, cur->var_nest_level, str, local_lvl
+				);
+				cur->next = *G.shadowed_vars_pp;
+				*G.shadowed_vars_pp = cur;
+			} else {
+				/* Came from pseudo_exec_argv(), no need to save: delete it */
+				debug_printf_env("shadow-deleting %s'%s'/%u by '%s'/%u\n",
+					cur->flg_export ? "exported " : "",
+					cur->varstr, cur->var_nest_level, str, local_lvl
+				);
+				if (cur->max_len == 0) /* allocated "VAR=VAL"? */
+					free_me = cur->varstr; /* then free it later */
+				free(cur);
+			}
+			break;
+		}
+
+		if (strcmp(cur->varstr + name_len, eq_sign + 1) == 0) {
+			debug_printf_env("assignement '%s' does not change anything\n", str);
+ free_and_exp:
+			free(str);
+			goto exp;
+		}
+
+		/* Replace the value in the found "struct variable" */
+		if (cur->max_len != 0) {
+			if (cur->max_len >= strnlen(str, cur->max_len + 1)) {
+				/* This one is from startup env, reuse space */
+				debug_printf_env("reusing startup env for '%s'\n", str);
+				strcpy(cur->varstr, str);
+				goto free_and_exp;
+			}
+			/* Can't reuse */
+			cur->max_len = 0;
+			goto set_str_and_exp;
+		}
+		/* max_len == 0 signifies "malloced" var, which we can
+		 * (and have to) free. But we can't free(cur->varstr) here:
+		 * if cur->flg_export is 1, it is in the environment.
+		 * We should either unsetenv+free, or wait until putenv,
+		 * then putenv(new)+free(old).
+		 */
+		free_me = cur->varstr;
+		goto set_str_and_exp;
+	}
+
+	/* Not found or shadowed - create new variable struct */
+	debug_printf_env("%s: alloc new var '%s'/%u\n", __func__, str, local_lvl);
+	cur = xzalloc(sizeof(*cur));
+	cur->var_nest_level = local_lvl;
+	cur->next = *cur_pp;
+	*cur_pp = cur;
+
+ set_str_and_exp:
+	cur->varstr = str;
+ exp:
+#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;
+	retval = 0;
+	if (cur->flg_export) {
+		if (flags & SETFLAG_UNEXPORT) {
+			cur->flg_export = 0;
+			/* unsetenv was already done */
+		} else {
+			debug_printf_env("%s: putenv '%s'/%u\n", __func__, cur->varstr, cur->var_nest_level);
+			retval = putenv(cur->varstr);
+			/* fall through to "free(free_me)" -
+			 * only now we can free old exported malloced string
+			 */
+		}
+	}
+	free(free_me);
+
+	handle_changed_special_names(cur->varstr, name_len - 1);
+
+	return retval;
+}
+
+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, /*flag:*/ 0);
+}
+
+/* Used at startup and after each cd */
+static void set_pwd_var(unsigned flag)
+{
+	set_local_var(xasprintf("PWD=%s", get_cwd(/*force:*/ 1)), flag);
+}
+
+#if ENABLE_HUSH_UNSET || ENABLE_HUSH_GETOPTS
+static int unset_local_var_len(const char *name, int name_len)
+{
+	struct variable *cur;
+	struct variable **cur_pp;
+
+	cur_pp = &G.top_var;
+	while ((cur = *cur_pp) != NULL) {
+		if (strncmp(cur->varstr, name, name_len) == 0
+		 && cur->varstr[name_len] == '='
+		) {
+			if (cur->flg_read_only) {
+				bb_error_msg("%s: readonly variable", name);
+				return EXIT_FAILURE;
+			}
+
+			*cur_pp = cur->next;
+			debug_printf_env("%s: unsetenv '%s'\n", __func__, cur->varstr);
+			bb_unsetenv(cur->varstr);
+			if (!cur->max_len)
+				free(cur->varstr);
+			free(cur);
+
+			break;
+		}
+		cur_pp = &cur->next;
+	}
+
+	/* Handle "unset LINENO" et al even if did not find the variable to unset */
+	handle_changed_special_names(name, name_len);
+
+	return EXIT_SUCCESS;
+}
+
+static int unset_local_var(const char *name)
+{
+	return unset_local_var_len(name, strlen(name));
+}
+#endif
+
+
+/*
+ * Helpers for "var1=val1 var2=val2 cmd" feature
+ */
+static void add_vars(struct variable *var)
+{
+	struct variable *next;
+
+	while (var) {
+		next = var->next;
+		var->next = G.top_var;
+		G.top_var = var;
+		if (var->flg_export) {
+			debug_printf_env("%s: restoring exported '%s'/%u\n", __func__, var->varstr, var->var_nest_level);
+			putenv(var->varstr);
+		} else {
+			debug_printf_env("%s: restoring variable '%s'/%u\n", __func__, var->varstr, var->var_nest_level);
+		}
+		var = next;
+	}
+}
+
+/* We put strings[i] into variable table and possibly putenv them.
+ * If variable is read only, we can free the strings[i]
+ * which attempts to overwrite it.
+ * The strings[] vector itself is freed.
+ */
+static void set_vars_and_save_old(char **strings)
+{
+	char **s;
+
+	if (!strings)
+		return;
+
+	s = strings;
+	while (*s) {
+		struct variable *var_p;
+		struct variable **var_pp;
+		char *eq;
+
+		eq = strchr(*s, '=');
+		if (HUSH_DEBUG && !eq)
+			bb_simple_error_msg_and_die("BUG in varexp4");
+		var_pp = get_ptr_to_local_var(*s, eq - *s);
+		if (var_pp) {
+			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;
+			}
+			/* below, set_local_var() with nest level will
+			 * "shadow" (remove) this variable from
+			 * global linked list.
+			 */
+		}
+		debug_printf_env("%s: env override '%s'/%u\n", __func__, *s, G.var_nest_level);
+		set_local_var(*s, (G.var_nest_level << SETFLAG_VARLVL_SHIFT) | SETFLAG_EXPORT);
+		s++;
+ next: ;
+	}
+	free(strings);
+}
+
+
+/*
+ * Unicode helper
+ */
+static void reinit_unicode_for_hush(void)
+{
+	/* Unicode support should be activated even if LANG is set
+	 * _during_ shell execution, not only if it was set when
+	 * shell was started. Therefore, re-check LANG every time:
+	 */
+	if (ENABLE_FEATURE_CHECK_UNICODE_IN_ENV
+	 || ENABLE_UNICODE_USING_LOCALE
+	) {
+		const char *s = get_local_var_value("LC_ALL");
+		if (!s) s = get_local_var_value("LC_CTYPE");
+		if (!s) s = get_local_var_value("LANG");
+		reinit_unicode(s);
+	}
+}
+
+/*
+ * in_str support (strings, and "strings" read from files).
+ */
+
+#if ENABLE_HUSH_INTERACTIVE
+/* To test correct lineedit/interactive behavior, type from command line:
+ *	echo $P\
+ *	\
+ *	AT\
+ *	H\
+ *	\
+ * It exercises a lot of corner cases.
+ */
+static const char *setup_prompt_string(void)
+{
+	const char *prompt_str;
+
+	debug_printf_prompt("%s promptmode:%d\n", __func__, G.promptmode);
+
+# if ENABLE_FEATURE_EDITING_FANCY_PROMPT
+	prompt_str = get_local_var_value(G.promptmode == 0 ? "PS1" : "PS2");
+	if (!prompt_str)
+		prompt_str = "";
+# else
+	prompt_str = "> "; /* if PS2, else... */
+	if (G.promptmode == 0) { /* PS1 */
+		/* No fancy prompts supported, (re)generate "CURDIR $ " by hand */
+		free(G.PS1);
+		/* bash uses $PWD value, even if it is set by user.
+		 * It uses current dir only if PWD is unset.
+		 * We always use current dir. */
+		prompt_str = G.PS1 = xasprintf("%s %c ", get_cwd(0), (geteuid() != 0) ? '$' : '#');
+	}
+# endif
+	debug_printf("prompt_str '%s'\n", prompt_str);
+	return prompt_str;
+}
+static int get_user_input(struct in_str *i)
+{
+# if ENABLE_FEATURE_EDITING
+	/* In EDITING case, this function reads next input line,
+	 * saves it in i->p, then returns 1st char of it.
+	 */
+	int r;
+	const char *prompt_str;
+
+	prompt_str = setup_prompt_string();
+	for (;;) {
+		reinit_unicode_for_hush();
+		G.flag_SIGINT = 0;
+		/* buglet: SIGINT will not make new prompt to appear _at once_,
+		 * only after <Enter>. (^C works immediately) */
+		r = read_line_input(G.line_input_state, prompt_str,
+				G.user_input_buf, CONFIG_FEATURE_EDITING_MAX_LEN-1
+		);
+		/* read_line_input intercepts ^C, "convert" it to SIGINT */
+		if (r == 0) {
+			raise(SIGINT);
+		}
+		check_and_run_traps();
+		if (r != 0 && !G.flag_SIGINT)
+			break;
+		/* ^C or SIGINT: repeat */
+		/* bash prints ^C even on real SIGINT (non-kbd generated) */
+		write(STDOUT_FILENO, "^C\n", 3);
+		G.last_exitcode = 128 | SIGINT;
+	}
+	if (r < 0) {
+		/* EOF/error detected */
+		/* ^D on interactive input goes to next line before exiting: */
+		write(STDOUT_FILENO, "\n", 1);
+		i->p = NULL;
+		i->peek_buf[0] = r = EOF;
+		return r;
+	}
+	i->p = G.user_input_buf;
+	return (unsigned char)*i->p++;
+# else
+	/* In !EDITING case, this function gets called for every char.
+	 * Buffering happens deeper in the call chain, in hfgetc(i->file).
+	 */
+	int r;
+
+	for (;;) {
+		G.flag_SIGINT = 0;
+		if (i->last_char == '\0' || i->last_char == '\n') {
+			const char *prompt_str = setup_prompt_string();
+			/* Why check_and_run_traps here? Try this interactively:
+			 * $ trap 'echo INT' INT; (sleep 2; kill -INT $$) &
+			 * $ <[enter], repeatedly...>
+			 * Without check_and_run_traps, handler never runs.
+			 */
+			check_and_run_traps();
+			fputs_stdout(prompt_str);
+			fflush_all();
+		}
+		r = hfgetc(i->file);
+		/* In !ENABLE_FEATURE_EDITING we don't use read_line_input,
+		 * no ^C masking happens during fgetc, no special code for ^C:
+		 * it generates SIGINT as usual.
+		 */
+		check_and_run_traps();
+		if (r != '\0' && !G.flag_SIGINT)
+			break;
+		if (G.flag_SIGINT) {
+			/* ^C or SIGINT: repeat */
+			/* bash prints ^C even on real SIGINT (non-kbd generated) */
+			/* kernel prints "^C" itself, just print newline: */
+			write(STDOUT_FILENO, "\n", 1);
+			G.last_exitcode = 128 | SIGINT;
+		}
+	}
+	return r;
+# endif
+}
+/* This is the magic location that prints prompts
+ * and gets data back from the user */
+static int fgetc_interactive(struct in_str *i)
+{
+	int ch;
+	/* If it's interactive stdin, get new line. */
+	if (G_interactive_fd && i->file == G.HFILE_stdin) {
+		/* Returns first char (or EOF), the rest is in i->p[] */
+		ch = get_user_input(i);
+		G.promptmode = 1; /* PS2 */
+		debug_printf_prompt("%s promptmode=%d\n", __func__, G.promptmode);
+	} else {
+		/* Not stdin: script file, sourced file, etc */
+		do ch = hfgetc(i->file); while (ch == '\0');
+	}
+	return ch;
+}
+#else  /* !INTERACTIVE */
+static ALWAYS_INLINE int fgetc_interactive(struct in_str *i)
+{
+	int ch;
+	do ch = hfgetc(i->file); while (ch == '\0');
+	return ch;
+}
+#endif  /* !INTERACTIVE */
+
+static int i_getch(struct in_str *i)
+{
+	int ch;
+
+	if (!i->file) {
+		/* string-based in_str */
+		ch = (unsigned char)*i->p;
+		if (ch != '\0') {
+			i->p++;
+			i->last_char = ch;
+			return ch;
+		}
+		return EOF;
+	}
+
+	/* FILE-based in_str */
+
+#if ENABLE_FEATURE_EDITING
+	/* This can be stdin, check line editing char[] buffer */
+	if (i->p && *i->p != '\0') {
+		ch = (unsigned char)*i->p++;
+		goto out;
+	}
+#endif
+	/* peek_buf[] is an int array, not char. Can contain EOF. */
+	ch = i->peek_buf[0];
+	if (ch != 0) {
+		int ch2 = i->peek_buf[1];
+		i->peek_buf[0] = ch2;
+		if (ch2 == 0) /* very likely, avoid redundant write */
+			goto out;
+		i->peek_buf[1] = 0;
+		goto out;
+	}
+
+	ch = fgetc_interactive(i);
+ out:
+	debug_printf("file_get: got '%c' %d\n", ch, ch);
+	i->last_char = ch;
+#if ENABLE_HUSH_LINENO_VAR
+	if (ch == '\n') {
+		G.parse_lineno++;
+		debug_printf_parse("G.parse_lineno++ = %u\n", G.parse_lineno);
+	}
+#endif
+	return ch;
+}
+
+static int i_peek(struct in_str *i)
+{
+	int ch;
+
+	if (!i->file) {
+		/* string-based in_str */
+		/* Doesn't report EOF on NUL. None of the callers care. */
+		return (unsigned char)*i->p;
+	}
+
+	/* FILE-based in_str */
+
+#if ENABLE_FEATURE_EDITING && ENABLE_HUSH_INTERACTIVE
+	/* This can be stdin, check line editing char[] buffer */
+	if (i->p && *i->p != '\0')
+		return (unsigned char)*i->p;
+#endif
+	/* peek_buf[] is an int array, not char. Can contain EOF. */
+	ch = i->peek_buf[0];
+	if (ch != 0)
+		return ch;
+
+	/* Need to get a new char */
+	ch = fgetc_interactive(i);
+	debug_printf("file_peek: got '%c' %d\n", ch, ch);
+
+	/* Save it by either rolling back line editing buffer, or in i->peek_buf[0] */
+#if ENABLE_FEATURE_EDITING && ENABLE_HUSH_INTERACTIVE
+	if (i->p) {
+		i->p -= 1;
+		return ch;
+	}
+#endif
+	i->peek_buf[0] = ch;
+	/*i->peek_buf[1] = 0; - already is */
+	return ch;
+}
+
+/* Only ever called if i_peek() was called, and did not return EOF.
+ * IOW: we know the previous peek saw an ordinary char, not EOF, not NUL,
+ * not end-of-line. Therefore we never need to read a new editing line here.
+ */
+static int i_peek2(struct in_str *i)
+{
+	int ch;
+
+	/* There are two cases when i->p[] buffer exists.
+	 * (1) it's a string in_str.
+	 * (2) It's a file, and we have a saved line editing buffer.
+	 * In both cases, we know that i->p[0] exists and not NUL, and
+	 * the peek2 result is in i->p[1].
+	 */
+	if (i->p)
+		return (unsigned char)i->p[1];
+
+	/* Now we know it is a file-based in_str. */
+
+	/* peek_buf[] is an int array, not char. Can contain EOF. */
+	/* Is there 2nd char? */
+	ch = i->peek_buf[1];
+	if (ch == 0) {
+		/* We did not read it yet, get it now */
+		do ch = hfgetc(i->file); while (ch == '\0');
+		i->peek_buf[1] = ch;
+	}
+
+	debug_printf("file_peek2: got '%c' %d\n", ch, ch);
+	return ch;
+}
+
+static int i_getch_and_eat_bkslash_nl(struct in_str *input)
+{
+	for (;;) {
+		int ch, ch2;
+
+		ch = i_getch(input);
+		if (ch != '\\')
+			return ch;
+		ch2 = i_peek(input);
+		if (ch2 != '\n')
+			return ch;
+		/* backslash+newline, skip it */
+		i_getch(input);
+	}
+}
+
+/* Note: this function _eats_ \<newline> pairs, safe to use plain
+ * i_getch() after it instead of i_getch_and_eat_bkslash_nl().
+ */
+static int i_peek_and_eat_bkslash_nl(struct in_str *input)
+{
+	for (;;) {
+		int ch, ch2;
+
+		ch = i_peek(input);
+		if (ch != '\\')
+			return ch;
+		ch2 = i_peek2(input);
+		if (ch2 != '\n')
+			return ch;
+		/* backslash+newline, skip it */
+		i_getch(input);
+		i_getch(input);
+	}
+}
+
+static void setup_file_in_str(struct in_str *i, HFILE *fp)
+{
+	memset(i, 0, sizeof(*i));
+	i->file = fp;
+	/* i->p = NULL; */
+}
+
+static void setup_string_in_str(struct in_str *i, const char *s)
+{
+	memset(i, 0, sizeof(*i));
+	/*i->file = NULL */;
+	i->p = s;
+}
+
+
+/*
+ * o_string support
+ */
+#define B_CHUNK  (32 * sizeof(char*))
+
+static void o_reset_to_empty_unquoted(o_string *o)
+{
+	o->length = 0;
+	o->has_quoted_part = 0;
+	if (o->data)
+		o->data[0] = '\0';
+}
+
+static void o_free_and_set_NULL(o_string *o)
+{
+	free(o->data);
+	memset(o, 0, sizeof(*o));
+}
+
+static ALWAYS_INLINE void o_free(o_string *o)
+{
+	free(o->data);
+}
+
+static void o_grow_by(o_string *o, int len)
+{
+	if (o->length + len > o->maxlen) {
+		o->maxlen += (2 * len) | (B_CHUNK-1);
+		o->data = xrealloc(o->data, 1 + o->maxlen);
+	}
+}
+
+static void o_addchr(o_string *o, int ch)
+{
+	debug_printf("o_addchr: '%c' o->length=%d o=%p\n", ch, o->length, o);
+	if (o->length < o->maxlen) {
+		/* likely. avoid o_grow_by() call */
+ add:
+		o->data[o->length] = ch;
+		o->length++;
+		o->data[o->length] = '\0';
+		return;
+	}
+	o_grow_by(o, 1);
+	goto add;
+}
+
+#if 0
+/* Valid only if we know o_string is not empty */
+static void o_delchr(o_string *o)
+{
+	o->length--;
+	o->data[o->length] = '\0';
+}
+#endif
+
+static void o_addblock(o_string *o, const char *str, int len)
+{
+	o_grow_by(o, len);
+	((char*)mempcpy(&o->data[o->length], str, len))[0] = '\0';
+	o->length += len;
+}
+
+static void o_addstr(o_string *o, const char *str)
+{
+	o_addblock(o, str, strlen(str));
+}
+
+static void o_addstr_with_NUL(o_string *o, const char *str)
+{
+	o_addblock(o, str, strlen(str) + 1);
+}
+
+#if !BB_MMU
+static void nommu_addchr(o_string *o, int ch)
+{
+	if (o)
+		o_addchr(o, ch);
+}
+#else
+# define nommu_addchr(o, str) ((void)0)
+#endif
+
+#if ENABLE_HUSH_MODE_X
+static void x_mode_addchr(int ch)
+{
+	o_addchr(&G.x_mode_buf, ch);
+}
+static void x_mode_addstr(const char *str)
+{
+	o_addstr(&G.x_mode_buf, str);
+}
+static void x_mode_addblock(const char *str, int len)
+{
+	o_addblock(&G.x_mode_buf, str, len);
+}
+static void x_mode_prefix(void)
+{
+	int n = G.x_mode_depth;
+	do x_mode_addchr('+'); while (--n >= 0);
+}
+static void x_mode_flush(void)
+{
+	int len = G.x_mode_buf.length;
+	if (len <= 0)
+		return;
+	if (G.x_mode_fd > 0) {
+		G.x_mode_buf.data[len] = '\n';
+		full_write(G.x_mode_fd, G.x_mode_buf.data, len + 1);
+	}
+	G.x_mode_buf.length = 0;
+}
+#endif
+
+/*
+ * HUSH_BRACE_EXPANSION code needs corresponding quoting on variable expansion side.
+ * Currently, "v='{q,w}'; echo $v" erroneously expands braces in $v.
+ * Apparently, on unquoted $v bash still does globbing
+ * ("v='*.txt'; echo $v" prints all .txt files),
+ * but NOT brace expansion! Thus, there should be TWO independent
+ * quoting mechanisms on $v expansion side: one protects
+ * $v from brace expansion, and other additionally protects "$v" against globbing.
+ * We have only second one.
+ */
+
+#if ENABLE_HUSH_BRACE_EXPANSION
+# define MAYBE_BRACES "{}"
+#else
+# define MAYBE_BRACES ""
+#endif
+
+/* My analysis of quoting semantics tells me that state information
+ * is associated with a destination, not a source.
+ */
+static void o_addqchr(o_string *o, int ch)
+{
+	int sz = 1;
+	/* '-' is included because of this case:
+	 * >filename0 >filename1 >filename9; v='-'; echo filename[0"$v"9]
+	 */
+	char *found = strchr("*?[-\\" MAYBE_BRACES, ch);
+	if (found)
+		sz++;
+	o_grow_by(o, sz);
+	if (found) {
+		o->data[o->length] = '\\';
+		o->length++;
+	}
+	o->data[o->length] = ch;
+	o->length++;
+	o->data[o->length] = '\0';
+}
+
+static void o_addQchr(o_string *o, int ch)
+{
+	int sz = 1;
+	if ((o->o_expflags & EXP_FLAG_ESC_GLOB_CHARS)
+	 && strchr("*?[-\\" MAYBE_BRACES, ch)
+	) {
+		sz++;
+		o->data[o->length] = '\\';
+		o->length++;
+	}
+	o_grow_by(o, sz);
+	o->data[o->length] = ch;
+	o->length++;
+	o->data[o->length] = '\0';
+}
+
+static void o_addqblock(o_string *o, const char *str, int len)
+{
+	while (len) {
+		char ch;
+		int sz;
+		int ordinary_cnt = strcspn(str, "*?[-\\" MAYBE_BRACES);
+		if (ordinary_cnt > len) /* paranoia */
+			ordinary_cnt = len;
+		o_addblock(o, str, ordinary_cnt);
+		if (ordinary_cnt == len)
+			return; /* NUL is already added by o_addblock */
+		str += ordinary_cnt;
+		len -= ordinary_cnt + 1; /* we are processing + 1 char below */
+
+		ch = *str++;
+		sz = 1;
+		if (ch) { /* it is necessarily one of "*?[-\\" MAYBE_BRACES */
+			sz++;
+			o->data[o->length] = '\\';
+			o->length++;
+		}
+		o_grow_by(o, sz);
+		o->data[o->length] = ch;
+		o->length++;
+	}
+	o->data[o->length] = '\0';
+}
+
+static void o_addQblock(o_string *o, const char *str, int len)
+{
+	if (!(o->o_expflags & EXP_FLAG_ESC_GLOB_CHARS)) {
+		o_addblock(o, str, len);
+		return;
+	}
+	o_addqblock(o, str, len);
+}
+
+static void o_addQstr(o_string *o, const char *str)
+{
+	o_addQblock(o, str, strlen(str));
+}
+
+/* A special kind of o_string for $VAR and `cmd` expansion.
+ * It contains char* list[] at the beginning, which is grown in 16 element
+ * increments. Actual string data starts at the next multiple of 16 * (char*).
+ * list[i] contains an INDEX (int!) into this string data.
+ * It means that if list[] needs to grow, data needs to be moved higher up
+ * but list[i]'s need not be modified.
+ * NB: remembering how many list[i]'s you have there is crucial.
+ * o_finalize_list() operation post-processes this structure - calculates
+ * and stores actual char* ptrs in list[]. Oh, it NULL terminates it as well.
+ */
+#if DEBUG_EXPAND || DEBUG_GLOB
+static void debug_print_list(const char *prefix, o_string *o, int n)
+{
+	char **list = (char**)o->data;
+	int string_start = ((n + 0xf) & ~0xf) * sizeof(list[0]);
+	int i = 0;
+
+	indent();
+	fdprintf(2, "%s: list:%p n:%d string_start:%d length:%d maxlen:%d glob:%d quoted:%d escape:%d\n",
+			prefix, list, n, string_start, o->length, o->maxlen,
+			!!(o->o_expflags & EXP_FLAG_GLOB),
+			o->has_quoted_part,
+			!!(o->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+	while (i < n) {
+		indent();
+		fdprintf(2, " list[%d]=%d '%s' %p\n", i, (int)(uintptr_t)list[i],
+				o->data + (int)(uintptr_t)list[i] + string_start,
+				o->data + (int)(uintptr_t)list[i] + string_start);
+		i++;
+	}
+	if (n) {
+		const char *p = o->data + (int)(uintptr_t)list[n - 1] + string_start;
+		indent();
+		fdprintf(2, " total_sz:%ld\n", (long)((p + strlen(p) + 1) - o->data));
+	}
+}
+#else
+# define debug_print_list(prefix, o, n) ((void)0)
+#endif
+
+/* n = o_save_ptr_helper(str, n) "starts new string" by storing an index value
+ * in list[n] so that it points past last stored byte so far.
+ * It returns n+1. */
+static int o_save_ptr_helper(o_string *o, int n)
+{
+	char **list = (char**)o->data;
+	int string_start;
+	int string_len;
+
+	if (!o->has_empty_slot) {
+		string_start = ((n + 0xf) & ~0xf) * sizeof(list[0]);
+		string_len = o->length - string_start;
+		if (!(n & 0xf)) { /* 0, 0x10, 0x20...? */
+			debug_printf_list("list[%d]=%d string_start=%d (growing)\n", n, string_len, string_start);
+			/* list[n] points to string_start, make space for 16 more pointers */
+			o->maxlen += 0x10 * sizeof(list[0]);
+			o->data = xrealloc(o->data, o->maxlen + 1);
+			list = (char**)o->data;
+			memmove(list + n + 0x10, list + n, string_len);
+			/*
+			 * expand_on_ifs() has a "previous argv[] ends in IFS?"
+			 * check. (grep for -prev-ifs-check-).
+			 * Ensure that argv[-1][last] is not garbage
+			 * but zero bytes, to save index check there.
+			 */
+			list[n + 0x10 - 1] = 0;
+			o->length += 0x10 * sizeof(list[0]);
+		} else {
+			debug_printf_list("list[%d]=%d string_start=%d\n",
+					n, string_len, string_start);
+		}
+	} else {
+		/* We have empty slot at list[n], reuse without growth */
+		string_start = ((n+1 + 0xf) & ~0xf) * sizeof(list[0]); /* NB: n+1! */
+		string_len = o->length - string_start;
+		debug_printf_list("list[%d]=%d string_start=%d (empty slot)\n",
+				n, string_len, string_start);
+		o->has_empty_slot = 0;
+	}
+	o->has_quoted_part = 0;
+	list[n] = (char*)(uintptr_t)string_len;
+	return n + 1;
+}
+
+/* "What was our last o_save_ptr'ed position (byte offset relative o->data)?" */
+static int o_get_last_ptr(o_string *o, int n)
+{
+	char **list = (char**)o->data;
+	int string_start = ((n + 0xf) & ~0xf) * sizeof(list[0]);
+
+	return ((int)(uintptr_t)list[n-1]) + string_start;
+}
+
+/*
+ * Globbing routines.
+ *
+ * Most words in commands need to be globbed, even ones which are
+ * (single or double) quoted. This stems from the possiblity of
+ * constructs like "abc"* and 'abc'* - these should be globbed.
+ * Having a different code path for fully-quoted strings ("abc",
+ * 'abc') would only help performance-wise, but we still need
+ * code for partially-quoted strings.
+ *
+ * Unfortunately, if we want to match bash and ash behavior in all cases,
+ * the logic can't be "shell-syntax argument is first transformed
+ * to a string, then globbed, and if globbing does not match anything,
+ * it is used verbatim". Here are two examples where it fails:
+ *
+ * 	echo 'b\*'?
+ *
+ * The globbing can't be avoided (because of '?' at the end).
+ * The glob pattern is: b\\\*? - IOW, both \ and * are literals
+ * and are glob-escaped. If this does not match, bash/ash print b\*?
+ * - IOW: they "unbackslash" the glob pattern.
+ * Now, look at this:
+ *
+ * 	v='\\\*'; echo b$v?
+ *
+ * The glob pattern is the same here: b\\\*? - the unquoted $v expansion
+ * should be used as glob pattern with no changes. However, if glob
+ * does not match, bash/ash print b\\\*? - NOT THE SAME as first example!
+ *
+ * ash implements this by having an encoded representation of the word
+ * to glob, which IS NOT THE SAME as the glob pattern - it has more data.
+ * Glob pattern is derived from it. If glob fails, the decision what result
+ * should be is made using that encoded representation. Not glob pattern.
+ */
+
+#if ENABLE_HUSH_BRACE_EXPANSION
+/* There in a GNU extension, GLOB_BRACE, but it is not usable:
+ * first, it processes even {a} (no commas), second,
+ * I didn't manage to make it return strings when they don't match
+ * existing files. Need to re-implement it.
+ */
+
+/* Helper */
+static int glob_needed(const char *s)
+{
+	while (*s) {
+		if (*s == '\\') {
+			if (!s[1])
+				return 0;
+			s += 2;
+			continue;
+		}
+		if (*s == '*' || *s == '[' || *s == '?' || *s == '{')
+			return 1;
+		s++;
+	}
+	return 0;
+}
+/* Return pointer to next closing brace or to comma */
+static const char *next_brace_sub(const char *cp)
+{
+	unsigned depth = 0;
+	cp++;
+	while (*cp != '\0') {
+		if (*cp == '\\') {
+			if (*++cp == '\0')
+				break;
+			cp++;
+			continue;
+		}
+		if ((*cp == '}' && depth-- == 0) || (*cp == ',' && depth == 0))
+			break;
+		if (*cp++ == '{')
+			depth++;
+	}
+
+	return *cp != '\0' ? cp : NULL;
+}
+/* Recursive brace globber. Note: may garble pattern[]. */
+static int glob_brace(char *pattern, o_string *o, int n)
+{
+	char *new_pattern_buf;
+	const char *begin;
+	const char *next;
+	const char *rest;
+	const char *p;
+	size_t rest_len;
+
+	debug_printf_glob("glob_brace('%s')\n", pattern);
+
+	begin = pattern;
+	while (1) {
+		if (*begin == '\0')
+			goto simple_glob;
+		if (*begin == '{') {
+			/* Find the first sub-pattern and at the same time
+			 * find the rest after the closing brace */
+			next = next_brace_sub(begin);
+			if (next == NULL) {
+				/* An illegal expression */
+				goto simple_glob;
+			}
+			if (*next == '}') {
+				/* "{abc}" with no commas - illegal
+				 * brace expr, disregard and skip it */
+				begin = next + 1;
+				continue;
+			}
+			break;
+		}
+		if (*begin == '\\' && begin[1] != '\0')
+			begin++;
+		begin++;
+	}
+	debug_printf_glob("begin:%s\n", begin);
+	debug_printf_glob("next:%s\n", next);
+
+	/* Now find the end of the whole brace expression */
+	rest = next;
+	while (*rest != '}') {
+		rest = next_brace_sub(rest);
+		if (rest == NULL) {
+			/* An illegal expression */
+			goto simple_glob;
+		}
+		debug_printf_glob("rest:%s\n", rest);
+	}
+	rest_len = strlen(++rest) + 1;
+
+	/* We are sure the brace expression is well-formed */
+
+	/* Allocate working buffer large enough for our work */
+	new_pattern_buf = xmalloc(strlen(pattern));
+
+	/* We have a brace expression.  BEGIN points to the opening {,
+	 * NEXT points past the terminator of the first element, and REST
+	 * points past the final }.  We will accumulate result names from
+	 * recursive runs for each brace alternative in the buffer using
+	 * GLOB_APPEND.  */
+
+	p = begin + 1;
+	while (1) {
+		/* Construct the new glob expression */
+		memcpy(
+			mempcpy(
+				mempcpy(new_pattern_buf,
+					/* We know the prefix for all sub-patterns */
+					pattern, begin - pattern),
+				p, next - p),
+			rest, rest_len);
+
+		/* Note: glob_brace() may garble new_pattern_buf[].
+		 * That's why we re-copy prefix every time (1st memcpy above).
+		 */
+		n = glob_brace(new_pattern_buf, o, n);
+		if (*next == '}') {
+			/* We saw the last entry */
+			break;
+		}
+		p = next + 1;
+		next = next_brace_sub(next);
+	}
+	free(new_pattern_buf);
+	return n;
+
+ simple_glob:
+	{
+		int gr;
+		glob_t globdata;
+
+		memset(&globdata, 0, sizeof(globdata));
+		gr = glob(pattern, 0, NULL, &globdata);
+		debug_printf_glob("glob('%s'):%d\n", pattern, gr);
+		if (gr != 0) {
+			if (gr == GLOB_NOMATCH) {
+				globfree(&globdata);
+				/* NB: garbles parameter */
+				unbackslash(pattern);
+				o_addstr_with_NUL(o, pattern);
+				debug_printf_glob("glob pattern '%s' is literal\n", pattern);
+				return o_save_ptr_helper(o, n);
+			}
+			if (gr == GLOB_NOSPACE)
+				bb_die_memory_exhausted();
+			/* GLOB_ABORTED? Only happens with GLOB_ERR flag,
+			 * but we didn't specify it. Paranoia again. */
+			bb_error_msg_and_die("glob error %d on '%s'", gr, pattern);
+		}
+		if (globdata.gl_pathv && globdata.gl_pathv[0]) {
+			char **argv = globdata.gl_pathv;
+			while (1) {
+				o_addstr_with_NUL(o, *argv);
+				n = o_save_ptr_helper(o, n);
+				argv++;
+				if (!*argv)
+					break;
+			}
+		}
+		globfree(&globdata);
+	}
+	return n;
+}
+/* Performs globbing on last list[],
+ * saving each result as a new list[].
+ */
+static int perform_glob(o_string *o, int n)
+{
+	char *pattern, *copy;
+
+	debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
+	if (!o->data)
+		return o_save_ptr_helper(o, n);
+	pattern = o->data + o_get_last_ptr(o, n);
+	debug_printf_glob("glob pattern '%s'\n", pattern);
+	if (!glob_needed(pattern)) {
+		/* unbackslash last string in o in place, fix length */
+		o->length = unbackslash(pattern) - o->data;
+		debug_printf_glob("glob pattern '%s' is literal\n", pattern);
+		return o_save_ptr_helper(o, n);
+	}
+
+	copy = xstrdup(pattern);
+	/* "forget" pattern in o */
+	o->length = pattern - o->data;
+	n = glob_brace(copy, o, n);
+	free(copy);
+	if (DEBUG_GLOB)
+		debug_print_list("perform_glob returning", o, n);
+	return n;
+}
+
+#else /* !HUSH_BRACE_EXPANSION */
+
+/* Helper */
+static int glob_needed(const char *s)
+{
+	while (*s) {
+		if (*s == '\\') {
+			if (!s[1])
+				return 0;
+			s += 2;
+			continue;
+		}
+		if (*s == '*' || *s == '[' || *s == '?')
+			return 1;
+		s++;
+	}
+	return 0;
+}
+/* Performs globbing on last list[],
+ * saving each result as a new list[].
+ */
+static int perform_glob(o_string *o, int n)
+{
+	glob_t globdata;
+	int gr;
+	char *pattern;
+
+	debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
+	if (!o->data)
+		return o_save_ptr_helper(o, n);
+	pattern = o->data + o_get_last_ptr(o, n);
+	debug_printf_glob("glob pattern '%s'\n", pattern);
+	if (!glob_needed(pattern)) {
+ literal:
+		/* unbackslash last string in o in place, fix length */
+		o->length = unbackslash(pattern) - o->data;
+		debug_printf_glob("glob pattern '%s' is literal\n", pattern);
+		return o_save_ptr_helper(o, n);
+	}
+
+	memset(&globdata, 0, sizeof(globdata));
+	/* Can't use GLOB_NOCHECK: it does not unescape the string.
+	 * If we glob "*.\*" and don't find anything, we need
+	 * to fall back to using literal "*.*", but GLOB_NOCHECK
+	 * will return "*.\*"!
+	 */
+	gr = glob(pattern, 0, NULL, &globdata);
+	debug_printf_glob("glob('%s'):%d\n", pattern, gr);
+	if (gr != 0) {
+		if (gr == GLOB_NOMATCH) {
+			globfree(&globdata);
+			goto literal;
+		}
+		if (gr == GLOB_NOSPACE)
+			bb_die_memory_exhausted();
+		/* GLOB_ABORTED? Only happens with GLOB_ERR flag,
+		 * but we didn't specify it. Paranoia again. */
+		bb_error_msg_and_die("glob error %d on '%s'", gr, pattern);
+	}
+	if (globdata.gl_pathv && globdata.gl_pathv[0]) {
+		char **argv = globdata.gl_pathv;
+		/* "forget" pattern in o */
+		o->length = pattern - o->data;
+		while (1) {
+			o_addstr_with_NUL(o, *argv);
+			n = o_save_ptr_helper(o, n);
+			argv++;
+			if (!*argv)
+				break;
+		}
+	}
+	globfree(&globdata);
+	if (DEBUG_GLOB)
+		debug_print_list("perform_glob returning", o, n);
+	return n;
+}
+
+#endif /* !HUSH_BRACE_EXPANSION */
+
+/* If o->o_expflags & EXP_FLAG_GLOB, glob the string so far remembered.
+ * Otherwise, just finish current list[] and start new */
+static int o_save_ptr(o_string *o, int n)
+{
+	if (o->o_expflags & EXP_FLAG_GLOB) {
+		/* If o->has_empty_slot, list[n] was already globbed
+		 * (if it was requested back then when it was filled)
+		 * so don't do that again! */
+		if (!o->has_empty_slot)
+			return perform_glob(o, n); /* o_save_ptr_helper is inside */
+	}
+	return o_save_ptr_helper(o, n);
+}
+
+/* "Please convert list[n] to real char* ptrs, and NULL terminate it." */
+static char **o_finalize_list(o_string *o, int n)
+{
+	char **list;
+	int string_start;
+
+	if (DEBUG_EXPAND)
+		debug_print_list("finalized", o, n);
+	debug_printf_expand("finalized n:%d\n", n);
+	list = (char**)o->data;
+	string_start = ((n + 0xf) & ~0xf) * sizeof(list[0]);
+	list[--n] = NULL;
+	while (n) {
+		n--;
+		list[n] = o->data + (int)(uintptr_t)list[n] + string_start;
+	}
+	return list;
+}
+
+static void free_pipe_list(struct pipe *pi);
+
+/* Returns pi->next - next pipe in the list */
+static struct pipe *free_pipe(struct pipe *pi)
+{
+	struct pipe *next;
+	int i;
+
+	debug_printf_clean("free_pipe (pid %d)\n", getpid());
+	for (i = 0; i < pi->num_cmds; i++) {
+		struct command *command;
+		struct redir_struct *r, *rnext;
+
+		command = &pi->cmds[i];
+		debug_printf_clean("  command %d:\n", i);
+		if (command->argv) {
+			if (DEBUG_CLEAN) {
+				int a;
+				char **p;
+				for (a = 0, p = command->argv; *p; a++, p++) {
+					debug_printf_clean("   argv[%d] = %s\n", a, *p);
+				}
+			}
+			free_strings(command->argv);
+			//command->argv = NULL;
+		}
+		/* not "else if": on syntax error, we may have both! */
+		if (command->group) {
+			debug_printf_clean("   begin group (cmd_type:%d)\n",
+					command->cmd_type);
+			free_pipe_list(command->group);
+			debug_printf_clean("   end group\n");
+			//command->group = NULL;
+		}
+		/* else is crucial here.
+		 * If group != NULL, child_func is meaningless */
+#if ENABLE_HUSH_FUNCTIONS
+		else if (command->child_func) {
+			debug_printf_exec("cmd %p releases child func at %p\n", command, command->child_func);
+			command->child_func->parent_cmd = NULL;
+		}
+#endif
+#if !BB_MMU
+		free(command->group_as_string);
+		//command->group_as_string = NULL;
+#endif
+		for (r = command->redirects; r; r = rnext) {
+			debug_printf_clean("   redirect %d%s",
+					r->rd_fd, redir_table[r->rd_type].descrip);
+			/* guard against the case >$FOO, where foo is unset or blank */
+			if (r->rd_filename) {
+				debug_printf_clean(" fname:'%s'\n", r->rd_filename);
+				free(r->rd_filename);
+				//r->rd_filename = NULL;
+			}
+			debug_printf_clean(" rd_dup:%d\n", r->rd_dup);
+			rnext = r->next;
+			free(r);
+		}
+		//command->redirects = NULL;
+	}
+	free(pi->cmds);   /* children are an array, they get freed all at once */
+	//pi->cmds = NULL;
+#if ENABLE_HUSH_JOB
+	free(pi->cmdtext);
+	//pi->cmdtext = NULL;
+#endif
+
+	next = pi->next;
+	free(pi);
+	return next;
+}
+
+static void free_pipe_list(struct pipe *pi)
+{
+	while (pi) {
+#if HAS_KEYWORDS
+		debug_printf_clean("pipe reserved word %d\n", pi->res_word);
+#endif
+		debug_printf_clean("pipe followup code %d\n", pi->followup);
+		pi = free_pipe(pi);
+	}
+}
+
+
+/*** Parsing routines ***/
+
+#ifndef debug_print_tree
+static void debug_print_tree(struct pipe *pi, int lvl)
+{
+	static const char *const PIPE[] = {
+		[PIPE_SEQ] = "SEQ",
+		[PIPE_AND] = "AND",
+		[PIPE_OR ] = "OR" ,
+		[PIPE_BG ] = "BG" ,
+	};
+	static const char *RES[] = {
+		[RES_NONE ] = "NONE" ,
+# if ENABLE_HUSH_IF
+		[RES_IF   ] = "IF"   ,
+		[RES_THEN ] = "THEN" ,
+		[RES_ELIF ] = "ELIF" ,
+		[RES_ELSE ] = "ELSE" ,
+		[RES_FI   ] = "FI"   ,
+# endif
+# if ENABLE_HUSH_LOOPS
+		[RES_FOR  ] = "FOR"  ,
+		[RES_WHILE] = "WHILE",
+		[RES_UNTIL] = "UNTIL",
+		[RES_DO   ] = "DO"   ,
+		[RES_DONE ] = "DONE" ,
+# endif
+# if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
+		[RES_IN   ] = "IN"   ,
+# endif
+# if ENABLE_HUSH_CASE
+		[RES_CASE ] = "CASE" ,
+		[RES_CASE_IN ] = "CASE_IN" ,
+		[RES_MATCH] = "MATCH",
+		[RES_CASE_BODY] = "CASE_BODY",
+		[RES_ESAC ] = "ESAC" ,
+# endif
+		[RES_XXXX ] = "XXXX" ,
+		[RES_SNTX ] = "SNTX" ,
+	};
+	static const char *const CMDTYPE[] = {
+		"{}",
+		"()",
+		"[noglob]",
+# if ENABLE_HUSH_FUNCTIONS
+		"func()",
+# endif
+	};
+
+	int pin, prn;
+
+	pin = 0;
+	while (pi) {
+		fdprintf(2, "%*spipe %d #cmds:%d %sres_word=%s followup=%d %s\n",
+				lvl*2, "",
+				pin,
+				pi->num_cmds,
+				(IF_HAS_KEYWORDS(pi->pi_inverted ? "! " :) ""),
+				RES[pi->res_word],
+				pi->followup, PIPE[pi->followup]
+		);
+		prn = 0;
+		while (prn < pi->num_cmds) {
+			struct command *command = &pi->cmds[prn];
+			char **argv = command->argv;
+
+			fdprintf(2, "%*s cmd %d assignment_cnt:%d",
+					lvl*2, "", prn,
+					command->assignment_cnt);
+# if ENABLE_HUSH_LINENO_VAR
+			fdprintf(2, " LINENO:%u", command->lineno);
+# endif
+			if (command->group) {
+				fdprintf(2, " group %s: (argv=%p)%s%s\n",
+						CMDTYPE[command->cmd_type],
+						argv
+# if !BB_MMU
+						, " group_as_string:", command->group_as_string
+# else
+						, "", ""
+# endif
+				);
+				debug_print_tree(command->group, lvl+1);
+				prn++;
+				continue;
+			}
+			if (argv) while (*argv) {
+				fdprintf(2, " '%s'", *argv);
+				argv++;
+			}
+			if (command->redirects)
+				fdprintf(2, " {redir}");
+			fdprintf(2, "\n");
+			prn++;
+		}
+		pi = pi->next;
+		pin++;
+	}
+}
+#endif /* debug_print_tree */
+
+static struct pipe *new_pipe(void)
+{
+	struct pipe *pi;
+	pi = xzalloc(sizeof(struct pipe));
+	/*pi->res_word = RES_NONE; - RES_NONE is 0 anyway */
+	return pi;
+}
+
+/* Command (member of a pipe) is complete, or we start a new pipe
+ * if ctx->command is NULL.
+ * No errors possible here.
+ */
+static int done_command(struct parse_context *ctx)
+{
+	/* The command is really already in the pipe structure, so
+	 * advance the pipe counter and make a new, null command. */
+	struct pipe *pi = ctx->pipe;
+	struct command *command = ctx->command;
+
+#if 0	/* Instead we emit error message at run time */
+	if (ctx->pending_redirect) {
+		/* For example, "cmd >" (no filename to redirect to) */
+		syntax_error("invalid redirect");
+		ctx->pending_redirect = NULL;
+	}
+#endif
+
+	if (command) {
+		if (IS_NULL_CMD(command)) {
+			debug_printf_parse("done_command: skipping null cmd, num_cmds=%d\n", pi->num_cmds);
+			goto clear_and_ret;
+		}
+		pi->num_cmds++;
+		debug_printf_parse("done_command: ++num_cmds=%d\n", pi->num_cmds);
+		//debug_print_tree(ctx->list_head, 20);
+	} else {
+		debug_printf_parse("done_command: initializing, num_cmds=%d\n", pi->num_cmds);
+	}
+
+	/* Only real trickiness here is that the uncommitted
+	 * command structure is not counted in pi->num_cmds. */
+	pi->cmds = xrealloc(pi->cmds, sizeof(*pi->cmds) * (pi->num_cmds+1));
+	ctx->command = command = &pi->cmds[pi->num_cmds];
+ clear_and_ret:
+	memset(command, 0, sizeof(*command));
+#if ENABLE_HUSH_LINENO_VAR
+	command->lineno = G.parse_lineno;
+	debug_printf_parse("command->lineno = G.parse_lineno (%u)\n", G.parse_lineno);
+#endif
+	return pi->num_cmds; /* used only for 0/nonzero check */
+}
+
+static void done_pipe(struct parse_context *ctx, pipe_style type)
+{
+	int not_null;
+
+	debug_printf_parse("done_pipe entered, followup %d\n", type);
+	/* Close previous command */
+	not_null = done_command(ctx);
+#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;
+		/* for cases like "cmd && &", do not be tricked by last command
+		 * being null - the entire {...} & is NOT null! */
+		not_null = 1;
+	} 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.
+	 * IOW: it is safe to do it unconditionally. */
+	if (not_null
+#if ENABLE_HUSH_IF
+	 || ctx->ctx_res_w == RES_FI
+#endif
+#if ENABLE_HUSH_LOOPS
+	 || ctx->ctx_res_w == RES_DONE
+	 || ctx->ctx_res_w == RES_FOR
+	 || ctx->ctx_res_w == RES_IN
+#endif
+#if ENABLE_HUSH_CASE
+	 || ctx->ctx_res_w == RES_ESAC
+#endif
+	) {
+		struct pipe *new_p;
+		debug_printf_parse("done_pipe: adding new pipe: "
+				"not_null:%d ctx->ctx_res_w:%d\n",
+				not_null, ctx->ctx_res_w);
+		new_p = new_pipe();
+		ctx->pipe->next = new_p;
+		ctx->pipe = new_p;
+		/* RES_THEN, RES_DO etc are "sticky" -
+		 * they remain set for pipes inside if/while.
+		 * This is used to control execution.
+		 * RES_FOR and RES_IN are NOT sticky (needed to support
+		 * cases where variable or value happens to match a keyword):
+		 */
+#if ENABLE_HUSH_LOOPS
+		if (ctx->ctx_res_w == RES_FOR
+		 || ctx->ctx_res_w == RES_IN)
+			ctx->ctx_res_w = RES_NONE;
+#endif
+#if ENABLE_HUSH_CASE
+		if (ctx->ctx_res_w == RES_MATCH)
+			ctx->ctx_res_w = RES_CASE_BODY;
+		if (ctx->ctx_res_w == RES_CASE)
+			ctx->ctx_res_w = RES_CASE_IN;
+#endif
+		ctx->command = NULL; /* trick done_command below */
+		/* Create the memory for command, roughly:
+		 * ctx->pipe->cmds = new struct command;
+		 * ctx->command = &ctx->pipe->cmds[0];
+		 */
+		done_command(ctx);
+		//debug_print_tree(ctx->list_head, 10);
+	}
+	debug_printf_parse("done_pipe return\n");
+}
+
+static void initialize_context(struct parse_context *ctx)
+{
+	memset(ctx, 0, sizeof(*ctx));
+	if (MAYBE_ASSIGNMENT != 0)
+		ctx->is_assignment = MAYBE_ASSIGNMENT;
+	ctx->pipe = ctx->list_head = new_pipe();
+	/* Create the memory for command, roughly:
+	 * ctx->pipe->cmds = new struct command;
+	 * ctx->command = &ctx->pipe->cmds[0];
+	 */
+	done_command(ctx);
+}
+
+/* If a reserved word is found and processed, parse context is modified
+ * and 1 is returned.
+ */
+#if HAS_KEYWORDS
+struct reserved_combo {
+	char literal[6];
+	unsigned char res;
+	unsigned char assignment_flag;
+	uint32_t flag;
+};
+enum {
+	FLAG_END   = (1 << RES_NONE ),
+# if ENABLE_HUSH_IF
+	FLAG_IF    = (1 << RES_IF   ),
+	FLAG_THEN  = (1 << RES_THEN ),
+	FLAG_ELIF  = (1 << RES_ELIF ),
+	FLAG_ELSE  = (1 << RES_ELSE ),
+	FLAG_FI    = (1 << RES_FI   ),
+# endif
+# if ENABLE_HUSH_LOOPS
+	FLAG_FOR   = (1 << RES_FOR  ),
+	FLAG_WHILE = (1 << RES_WHILE),
+	FLAG_UNTIL = (1 << RES_UNTIL),
+	FLAG_DO    = (1 << RES_DO   ),
+	FLAG_DONE  = (1 << RES_DONE ),
+	FLAG_IN    = (1 << RES_IN   ),
+# endif
+# if ENABLE_HUSH_CASE
+	FLAG_MATCH = (1 << RES_MATCH),
+	FLAG_ESAC  = (1 << RES_ESAC ),
+# endif
+	FLAG_START = (1 << RES_XXXX ),
+};
+
+static const struct reserved_combo* match_reserved_word(o_string *word)
+{
+	/* Mostly a list of accepted follow-up reserved words.
+	 * FLAG_END means we are done with the sequence, and are ready
+	 * to turn the compound list into a command.
+	 * FLAG_START means the word must start a new compound list.
+	 */
+	static const struct reserved_combo reserved_list[] ALIGN4 = {
+# if ENABLE_HUSH_IF
+		{ "!",     RES_NONE,  NOT_ASSIGNMENT  , 0 },
+		{ "if",    RES_IF,    MAYBE_ASSIGNMENT, FLAG_THEN | FLAG_START },
+		{ "then",  RES_THEN,  MAYBE_ASSIGNMENT, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
+		{ "elif",  RES_ELIF,  MAYBE_ASSIGNMENT, FLAG_THEN },
+		{ "else",  RES_ELSE,  MAYBE_ASSIGNMENT, FLAG_FI   },
+		{ "fi",    RES_FI,    NOT_ASSIGNMENT  , FLAG_END  },
+# endif
+# if ENABLE_HUSH_LOOPS
+		{ "for",   RES_FOR,   NOT_ASSIGNMENT  , FLAG_IN | FLAG_DO | FLAG_START },
+		{ "while", RES_WHILE, MAYBE_ASSIGNMENT, FLAG_DO | FLAG_START },
+		{ "until", RES_UNTIL, MAYBE_ASSIGNMENT, FLAG_DO | FLAG_START },
+		{ "in",    RES_IN,    NOT_ASSIGNMENT  , FLAG_DO   },
+		{ "do",    RES_DO,    MAYBE_ASSIGNMENT, FLAG_DONE },
+		{ "done",  RES_DONE,  NOT_ASSIGNMENT  , FLAG_END  },
+# endif
+# if ENABLE_HUSH_CASE
+		{ "case",  RES_CASE,  NOT_ASSIGNMENT  , FLAG_MATCH | FLAG_START },
+		{ "esac",  RES_ESAC,  NOT_ASSIGNMENT  , FLAG_END  },
+# endif
+	};
+	const struct reserved_combo *r;
+
+	for (r = reserved_list; r < reserved_list + ARRAY_SIZE(reserved_list); r++) {
+		if (strcmp(word->data, r->literal) == 0)
+			return r;
+	}
+	return NULL;
+}
+/* Return NULL: not a keyword, else: keyword
+ */
+static const struct reserved_combo* reserved_word(struct parse_context *ctx)
+{
+# if ENABLE_HUSH_CASE
+	static const struct reserved_combo reserved_match = {
+		"",        RES_MATCH, NOT_ASSIGNMENT , FLAG_MATCH | FLAG_ESAC
+	};
+# endif
+	const struct reserved_combo *r;
+
+	if (ctx->word.has_quoted_part)
+		return 0;
+	r = match_reserved_word(&ctx->word);
+	if (!r)
+		return r; /* NULL */
+
+	debug_printf("found reserved word %s, res %d\n", r->literal, r->res);
+# if ENABLE_HUSH_CASE
+	if (r->res == RES_IN && ctx->ctx_res_w == RES_CASE_IN) {
+		/* "case word IN ..." - IN part starts first MATCH part */
+		r = &reserved_match;
+	} else
+# endif
+	if (r->flag == 0) { /* '!' */
+		if (ctx->ctx_inverted) { /* bash doesn't accept '! ! true' */
+			syntax_error("! ! command");
+			ctx->ctx_res_w = RES_SNTX;
+		}
+		ctx->ctx_inverted = 1;
+		return r;
+	}
+	if (r->flag & FLAG_START) {
+		struct parse_context *old;
+
+		old = xmemdup(ctx, sizeof(*ctx));
+		debug_printf_parse("push stack %p\n", old);
+		initialize_context(ctx);
+		ctx->stack = old;
+	} else if (/*ctx->ctx_res_w == RES_NONE ||*/ !(ctx->old_flag & (1 << r->res))) {
+		syntax_error_at(ctx->word.data);
+		ctx->ctx_res_w = RES_SNTX;
+		return r;
+	} else {
+		/* "{...} fi" is ok. "{...} if" is not
+		 * Example:
+		 * if { echo foo; } then { echo bar; } fi */
+		if (ctx->command->group)
+			done_pipe(ctx, PIPE_SEQ);
+	}
+
+	ctx->ctx_res_w = r->res;
+	ctx->old_flag = r->flag;
+	ctx->is_assignment = r->assignment_flag;
+	debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]);
+
+	if (ctx->old_flag & FLAG_END) {
+		struct parse_context *old;
+
+		done_pipe(ctx, PIPE_SEQ);
+		debug_printf_parse("pop stack %p\n", ctx->stack);
+		old = ctx->stack;
+		old->command->group = ctx->list_head;
+		old->command->cmd_type = CMD_NORMAL;
+# if !BB_MMU
+		/* At this point, the compound command's string is in
+		 * ctx->as_string... except for the leading keyword!
+		 * Consider this example: "echo a | if true; then echo a; fi"
+		 * ctx->as_string will contain "true; then echo a; fi",
+		 * with "if " remaining in old->as_string!
+		 */
+		{
+			char *str;
+			int len = old->as_string.length;
+			/* Concatenate halves */
+			o_addstr(&old->as_string, ctx->as_string.data);
+			o_free(&ctx->as_string);
+			/* Find where leading keyword starts in first half */
+			str = old->as_string.data + len;
+			if (str > old->as_string.data)
+				str--; /* skip whitespace after keyword */
+			while (str > old->as_string.data && isalpha(str[-1]))
+				str--;
+			/* Ugh, we're done with this horrid hack */
+			old->command->group_as_string = xstrdup(str);
+			debug_printf_parse("pop, remembering as:'%s'\n",
+					old->command->group_as_string);
+		}
+# endif
+		*ctx = *old;   /* physical copy */
+		free(old);
+	}
+	return r;
+}
+#endif /* HAS_KEYWORDS */
+
+/* Word is complete, look at it and update parsing context.
+ * Normal return is 0. Syntax errors return 1.
+ * Note: on return, word is reset, but not o_free'd!
+ */
+static int done_word(struct parse_context *ctx)
+{
+	struct command *command = ctx->command;
+
+	debug_printf_parse("done_word entered: '%s' %p\n", ctx->word.data, command);
+	if (ctx->word.length == 0 && !ctx->word.has_quoted_part) {
+		debug_printf_parse("done_word return 0: true null, ignored\n");
+		return 0;
+	}
+
+	if (ctx->pending_redirect) {
+		/* We do not glob in e.g. >*.tmp case. bash seems to glob here
+		 * only if run as "bash", not "sh" */
+		/* http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
+		 * "2.7 Redirection
+		 * If the redirection operator is "<<" or "<<-", the word
+		 * that follows the redirection operator shall be
+		 * subjected to quote removal; it is unspecified whether
+		 * any of the other expansions occur. For the other
+		 * redirection operators, the word that follows the
+		 * redirection operator shall be subjected to tilde
+		 * expansion, parameter expansion, command substitution,
+		 * arithmetic expansion, and quote removal.
+		 * Pathname expansion shall not be performed
+		 * on the word by a non-interactive shell; an interactive
+		 * shell may perform it, but shall do so only when
+		 * the expansion would result in one word."
+		 */
+//bash does not do parameter/command substitution or arithmetic expansion
+//for _heredoc_ redirection word: these constructs look for exact eof marker
+// as written:
+// <<EOF$t
+// <<EOF$((1))
+// <<EOF`true`  [this case also makes heredoc "quoted", a-la <<"EOF". Probably bash-4.3.43 bug]
+
+		ctx->pending_redirect->rd_filename = xstrdup(ctx->word.data);
+		/* Cater for >\file case:
+		 * >\a creates file a; >\\a, >"\a", >"\\a" create file \a
+		 * Same with heredocs:
+		 * for <<\H delim is H; <<\\H, <<"\H", <<"\\H" - \H
+		 */
+		if (ctx->pending_redirect->rd_type == REDIRECT_HEREDOC) {
+			unbackslash(ctx->pending_redirect->rd_filename);
+			/* Is it <<"HEREDOC"? */
+			if (ctx->word.has_quoted_part) {
+				ctx->pending_redirect->rd_dup |= HEREDOC_QUOTED;
+			}
+		}
+		debug_printf_parse("word stored in rd_filename: '%s'\n", ctx->word.data);
+		ctx->pending_redirect = NULL;
+	} else {
+#if HAS_KEYWORDS
+# if ENABLE_HUSH_CASE
+		if (ctx->ctx_dsemicolon
+		 && strcmp(ctx->word.data, "esac") != 0 /* not "... pattern) cmd;; esac" */
+		) {
+			/* already done when ctx_dsemicolon was set to 1: */
+			/* ctx->ctx_res_w = RES_MATCH; */
+			ctx->ctx_dsemicolon = 0;
+		} else
+# endif
+# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
+		if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB
+		 && strcmp(ctx->word.data, "]]") == 0
+		) {
+			/* allow "[[ ]] >file" etc */
+			command->cmd_type = CMD_SINGLEWORD_NOGLOB;
+		} else
+# endif
+		if (!command->argv /* if it's the first word... */
+# if ENABLE_HUSH_LOOPS
+		 && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */
+		 && ctx->ctx_res_w != RES_IN
+# endif
+# if ENABLE_HUSH_CASE
+		 && ctx->ctx_res_w != RES_CASE
+# endif
+		) {
+			const struct reserved_combo *reserved;
+			reserved = reserved_word(ctx);
+			debug_printf_parse("checking for reserved-ness: %d\n", !!reserved);
+			if (reserved) {
+# if ENABLE_HUSH_LINENO_VAR
+/* Case:
+ * "while ...; do
+ *	cmd ..."
+ * If we don't close the pipe _now_, immediately after "do", lineno logic
+ * sees "cmd" as starting at "do" - i.e., at the previous line.
+ */
+				if (0
+				 IF_HUSH_IF(|| reserved->res == RES_THEN)
+				 IF_HUSH_IF(|| reserved->res == RES_ELIF)
+				 IF_HUSH_IF(|| reserved->res == RES_ELSE)
+				 IF_HUSH_LOOPS(|| reserved->res == RES_DO)
+				) {
+					done_pipe(ctx, PIPE_SEQ);
+				}
+# endif
+				o_reset_to_empty_unquoted(&ctx->word);
+				debug_printf_parse("done_word return %d\n",
+						(ctx->ctx_res_w == RES_SNTX));
+				return (ctx->ctx_res_w == RES_SNTX);
+			}
+# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
+			if (strcmp(ctx->word.data, "[[") == 0) {
+				command->cmd_type = CMD_TEST2_SINGLEWORD_NOGLOB;
+			} else
+# endif
+# if defined(CMD_SINGLEWORD_NOGLOB)
+			if (0
+			/* In bash, local/export/readonly are special, args
+			 * are assignments and therefore expansion of them
+			 * should be "one-word" expansion:
+			 *  $ export i=`echo 'a  b'` # one arg: "i=a  b"
+			 * compare with:
+			 *  $ ls i=`echo 'a  b'`     # two args: "i=a" and "b"
+			 *  ls: cannot access i=a: No such file or directory
+			 *  ls: cannot access b: No such file or directory
+			 * Note: bash 3.2.33(1) does this only if export word
+			 * itself is not quoted:
+			 *  $ export i=`echo 'aaa  bbb'`; echo "$i"
+			 *  aaa  bbb
+			 *  $ "export" i=`echo 'aaa  bbb'`; echo "$i"
+			 *  aaa
+			 */
+			 IF_HUSH_LOCAL(   || strcmp(ctx->word.data, "local") == 0)
+			 IF_HUSH_EXPORT(  || strcmp(ctx->word.data, "export") == 0)
+			 IF_HUSH_READONLY(|| strcmp(ctx->word.data, "readonly") == 0)
+			) {
+				command->cmd_type = CMD_SINGLEWORD_NOGLOB;
+			}
+# else
+			{ /* empty block to pair "if ... else" */ }
+# endif
+		}
+#endif /* HAS_KEYWORDS */
+
+		if (command->group) {
+			/* "{ echo foo; } echo bar" - bad */
+			syntax_error_at(ctx->word.data);
+			debug_printf_parse("done_word return 1: syntax error, "
+					"groups and arglists don't mix\n");
+			return 1;
+		}
+
+		/* If this word wasn't an assignment, next ones definitely
+		 * can't be assignments. Even if they look like ones. */
+		if (ctx->is_assignment != DEFINITELY_ASSIGNMENT
+		 && ctx->is_assignment != WORD_IS_KEYWORD
+		) {
+			ctx->is_assignment = NOT_ASSIGNMENT;
+		} else {
+			if (ctx->is_assignment == DEFINITELY_ASSIGNMENT) {
+				command->assignment_cnt++;
+				debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt);
+			}
+			debug_printf_parse("ctx->is_assignment was:'%s'\n", assignment_flag[ctx->is_assignment]);
+			ctx->is_assignment = MAYBE_ASSIGNMENT;
+		}
+		debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]);
+		command->argv = add_string_to_strings(command->argv, xstrdup(ctx->word.data));
+		debug_print_strings("word appended to argv", command->argv);
+	}
+
+#if ENABLE_HUSH_LOOPS
+	if (ctx->ctx_res_w == RES_FOR) {
+		if (ctx->word.has_quoted_part
+		 || endofname(command->argv[0])[0] != '\0'
+		) {
+			/* bash says just "not a valid identifier" */
+			syntax_error("bad variable name in for");
+			return 1;
+		}
+		/* Force FOR to have just one word (variable name) */
+		/* NB: basically, this makes hush see "for v in ..."
+		 * syntax as if it is "for v; in ...". FOR and IN become
+		 * two pipe structs in parse tree. */
+		done_pipe(ctx, PIPE_SEQ);
+	}
+#endif
+#if ENABLE_HUSH_CASE
+	/* Force CASE to have just one word */
+	if (ctx->ctx_res_w == RES_CASE) {
+		done_pipe(ctx, PIPE_SEQ);
+	}
+#endif
+
+	o_reset_to_empty_unquoted(&ctx->word);
+
+	debug_printf_parse("done_word return 0\n");
+	return 0;
+}
+
+
+/* Peek ahead in the input to find out if we have a "&n" construct,
+ * as in "2>&1", that represents duplicating a file descriptor.
+ * Return:
+ * REDIRFD_CLOSE if >&- "close fd" construct is seen,
+ * REDIRFD_SYNTAX_ERR if syntax error,
+ * REDIRFD_TO_FILE if no & was seen,
+ * or the number found.
+ */
+#if BB_MMU
+#define parse_redir_right_fd(as_string, input) \
+	parse_redir_right_fd(input)
+#endif
+static int parse_redir_right_fd(o_string *as_string, struct in_str *input)
+{
+	int ch, d, ok;
+
+	ch = i_peek(input);
+	if (ch != '&')
+		return REDIRFD_TO_FILE;
+
+	ch = i_getch(input);  /* get the & */
+	nommu_addchr(as_string, ch);
+	ch = i_peek(input);
+	if (ch == '-') {
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+		return REDIRFD_CLOSE;
+	}
+	d = 0;
+	ok = 0;
+	while (ch != EOF && isdigit(ch)) {
+		d = d*10 + (ch-'0');
+		ok = 1;
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+		ch = i_peek(input);
+	}
+	if (ok) return d;
+
+//TODO: this is the place to catch ">&file" bashism (redirect both fd 1 and 2)
+
+	bb_simple_error_msg("ambiguous redirect");
+	return REDIRFD_SYNTAX_ERR;
+}
+
+/* Return code is 0 normal, 1 if a syntax error is detected
+ */
+static int parse_redirect(struct parse_context *ctx,
+		int fd,
+		redir_type style,
+		struct in_str *input)
+{
+	struct command *command = ctx->command;
+	struct redir_struct *redir;
+	struct redir_struct **redirp;
+	int dup_num;
+
+	dup_num = REDIRFD_TO_FILE;
+	if (style != REDIRECT_HEREDOC) {
+		/* Check for a '>&1' type redirect */
+		dup_num = parse_redir_right_fd(&ctx->as_string, input);
+		if (dup_num == REDIRFD_SYNTAX_ERR)
+			return 1;
+	} else {
+		int ch = i_peek_and_eat_bkslash_nl(input);
+		dup_num = (ch == '-'); /* HEREDOC_SKIPTABS bit is 1 */
+		if (dup_num) { /* <<-... */
+			ch = i_getch(input);
+			nommu_addchr(&ctx->as_string, ch);
+			ch = i_peek(input);
+		}
+	}
+
+	if (style == REDIRECT_OVERWRITE && dup_num == REDIRFD_TO_FILE) {
+		int ch = i_peek_and_eat_bkslash_nl(input);
+		if (ch == '|') {
+			/* >|FILE redirect ("clobbering" >).
+			 * Since we do not support "set -o noclobber" yet,
+			 * >| and > are the same for now. Just eat |.
+			 */
+			ch = i_getch(input);
+			nommu_addchr(&ctx->as_string, ch);
+		}
+	}
+
+	/* Create a new redir_struct and append it to the linked list */
+	redirp = &command->redirects;
+	while ((redir = *redirp) != NULL) {
+		redirp = &(redir->next);
+	}
+	*redirp = redir = xzalloc(sizeof(*redir));
+	/* redir->next = NULL; */
+	/* redir->rd_filename = NULL; */
+	redir->rd_type = style;
+	redir->rd_fd = (fd == -1) ? redir_table[style].default_fd : fd;
+
+	debug_printf_parse("redirect type %d %s\n", redir->rd_fd,
+				redir_table[style].descrip);
+
+	redir->rd_dup = dup_num;
+	if (style != REDIRECT_HEREDOC && dup_num != REDIRFD_TO_FILE) {
+		/* Erik had a check here that the file descriptor in question
+		 * is legit; I postpone that to "run time"
+		 * A "-" representation of "close me" shows up as a -3 here */
+		debug_printf_parse("duplicating redirect '%d>&%d'\n",
+				redir->rd_fd, redir->rd_dup);
+	} else {
+#if 0		/* Instead we emit error message at run time */
+		if (ctx->pending_redirect) {
+			/* For example, "cmd > <file" */
+			syntax_error("invalid redirect");
+		}
+#endif
+		/* Set ctx->pending_redirect, so we know what to do at the
+		 * end of the next parsed word. */
+		ctx->pending_redirect = redir;
+	}
+	return 0;
+}
+
+/* If a redirect is immediately preceded by a number, that number is
+ * supposed to tell which file descriptor to redirect.  This routine
+ * looks for such preceding numbers.  In an ideal world this routine
+ * needs to handle all the following classes of redirects...
+ *     echo 2>foo     # redirects fd  2 to file "foo", nothing passed to echo
+ *     echo 49>foo    # redirects fd 49 to file "foo", nothing passed to echo
+ *     echo -2>foo    # redirects fd  1 to file "foo",    "-2" passed to echo
+ *     echo 49x>foo   # redirects fd  1 to file "foo",   "49x" passed to echo
+ *
+ * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html
+ * "2.7 Redirection
+ * ... If n is quoted, the number shall not be recognized as part of
+ * the redirection expression. For example:
+ * echo \2>a
+ * writes the character 2 into file a"
+ * We are getting it right by setting ->has_quoted_part on any \<char>
+ *
+ * A -1 return means no valid number was found,
+ * the caller should use the appropriate default for this redirection.
+ */
+static int redirect_opt_num(o_string *o)
+{
+	int num;
+
+	if (o->data == NULL)
+		return -1;
+	num = bb_strtou(o->data, NULL, 10);
+	if (errno || num < 0)
+		return -1;
+	o_reset_to_empty_unquoted(o);
+	return num;
+}
+
+#if BB_MMU
+#define fetch_till_str(as_string, input, word, skip_tabs) \
+	fetch_till_str(input, word, skip_tabs)
+#endif
+static char *fetch_till_str(o_string *as_string,
+		struct in_str *input,
+		const char *word,
+		int heredoc_flags)
+{
+	o_string heredoc = NULL_O_STRING;
+	unsigned past_EOL;
+	int prev = 0; /* not \ */
+	int ch;
+
+	/* Starting with "" is necessary for this case:
+	 * cat <<EOF
+	 *
+	 * xxx
+	 * EOF
+	 */
+	heredoc.data = xzalloc(1); /* start as "", not as NULL */
+
+	goto jump_in;
+
+	while (1) {
+		ch = i_getch(input);
+		if (ch != EOF)
+			nommu_addchr(as_string, ch);
+		if (ch == '\n' || ch == EOF) {
+ check_heredoc_end:
+			if ((heredoc_flags & HEREDOC_QUOTED) || prev != '\\') {
+				/* End-of-line, and not a line continuation */
+				if (strcmp(heredoc.data + past_EOL, word) == 0) {
+					heredoc.data[past_EOL] = '\0';
+					debug_printf_heredoc("parsed '%s' heredoc '%s'\n", word, heredoc.data);
+					return heredoc.data;
+				}
+				if (ch == '\n') {
+					/* This is a new line.
+					 * Remember position and backslash-escaping status.
+					 */
+					o_addchr(&heredoc, ch);
+					prev = ch;
+ jump_in:
+					past_EOL = heredoc.length;
+					/* Get 1st char of next line, possibly skipping leading tabs */
+					do {
+						ch = i_getch(input);
+						if (ch != EOF)
+							nommu_addchr(as_string, ch);
+					} while ((heredoc_flags & HEREDOC_SKIPTABS) && ch == '\t');
+					/* If this immediately ended the line,
+					 * go back to end-of-line checks.
+					 */
+					if (ch == '\n')
+						goto check_heredoc_end;
+				}
+			} else {
+				/* Backslash-line continuation in an unquoted
+				 * heredoc. This does not need special handling
+				 * for heredoc body (unquoted heredocs are
+				 * expanded on "execution" and that would take
+				 * care of this case too), but not the case
+				 * of line continuation *in terminator*:
+				 *  cat <<EOF
+				 *  Ok1
+				 *  EO\
+				 *  F
+				 */
+				heredoc.data[--heredoc.length] = '\0';
+				prev = 0; /* not '\' */
+				continue;
+			}
+		}
+		if (ch == EOF) {
+			o_free(&heredoc);
+			return NULL; /* error */
+		}
+		o_addchr(&heredoc, ch);
+		nommu_addchr(as_string, ch);
+		if (prev == '\\' && ch == '\\')
+			/* Correctly handle foo\\<eol> (not a line cont.) */
+			prev = 0; /* not '\' */
+		else
+			prev = ch;
+	}
+}
+
+/* Look at entire parse tree for not-yet-loaded REDIRECT_HEREDOCs
+ * and load them all. There should be exactly heredoc_cnt of them.
+ */
+#if BB_MMU
+#define fetch_heredocs(as_string, pi, heredoc_cnt, input) \
+	fetch_heredocs(pi, heredoc_cnt, input)
+#endif
+static int fetch_heredocs(o_string *as_string, struct pipe *pi, int heredoc_cnt, struct in_str *input)
+{
+	while (pi && heredoc_cnt) {
+		int i;
+		struct command *cmd = pi->cmds;
+
+		debug_printf_heredoc("fetch_heredocs: num_cmds:%d cmd argv0:'%s'\n",
+				pi->num_cmds,
+				cmd->argv ? cmd->argv[0] : "NONE"
+		);
+		for (i = 0; i < pi->num_cmds; i++) {
+			struct redir_struct *redir = cmd->redirects;
+
+			debug_printf_heredoc("fetch_heredocs: %d cmd argv0:'%s'\n",
+					i, cmd->argv ? cmd->argv[0] : "NONE");
+			while (redir) {
+				if (redir->rd_type == REDIRECT_HEREDOC) {
+					char *p;
+
+					redir->rd_type = REDIRECT_HEREDOC2;
+					/* redir->rd_dup is (ab)used to indicate <<- */
+					p = fetch_till_str(as_string, input,
+							redir->rd_filename, redir->rd_dup);
+					if (!p) {
+						syntax_error("unexpected EOF in here document");
+						return -1;
+					}
+					free(redir->rd_filename);
+					redir->rd_filename = p;
+					heredoc_cnt--;
+				}
+				redir = redir->next;
+			}
+			if (cmd->group) {
+				//bb_error_msg("%s:%u heredoc_cnt:%d", __func__, __LINE__, heredoc_cnt);
+				heredoc_cnt = fetch_heredocs(as_string, cmd->group, heredoc_cnt, input);
+				//bb_error_msg("%s:%u heredoc_cnt:%d", __func__, __LINE__, heredoc_cnt);
+				if (heredoc_cnt < 0)
+					return heredoc_cnt; /* error */
+			}
+			cmd++;
+		}
+		pi = pi->next;
+	}
+	return heredoc_cnt;
+}
+
+
+static int run_list(struct pipe *pi);
+#if BB_MMU
+#define parse_stream(pstring, heredoc_cnt_ptr, input, end_trigger) \
+	parse_stream(heredoc_cnt_ptr, input, end_trigger)
+#endif
+static struct pipe *parse_stream(char **pstring,
+		int *heredoc_cnt_ptr,
+		struct in_str *input,
+		int end_trigger);
+
+/* Returns number of heredocs not yet consumed,
+ * or -1 on error.
+ */
+static int parse_group(struct parse_context *ctx,
+		struct in_str *input, int ch)
+{
+	/* ctx->word contains characters seen prior to ( or {.
+	 * Typically it's empty, but for function defs,
+	 * it contains function name (without '()'). */
+#if BB_MMU
+# define as_string NULL
+#else
+	char *as_string = NULL;
+#endif
+	struct pipe *pipe_list;
+	int heredoc_cnt = 0;
+	int endch;
+	struct command *command = ctx->command;
+
+	debug_printf_parse("parse_group entered\n");
+#if ENABLE_HUSH_FUNCTIONS
+	if (ch == '(' && !ctx->word.has_quoted_part) {
+		if (ctx->word.length)
+			if (done_word(ctx))
+				return -1;
+		if (!command->argv)
+			goto skip; /* (... */
+		if (command->argv[1]) { /* word word ... (... */
+			syntax_error_unexpected_ch('(');
+			return -1;
+		}
+		/* it is "word(..." or "word (..." */
+		do
+			ch = i_getch(input);
+		while (ch == ' ' || ch == '\t');
+		if (ch != ')') {
+			syntax_error_unexpected_ch(ch);
+			return -1;
+		}
+		nommu_addchr(&ctx->as_string, ch);
+		do
+			ch = i_getch(input);
+		while (ch == ' ' || ch == '\t' || ch == '\n');
+		if (ch != '{' && ch != '(') {
+			syntax_error_unexpected_ch(ch);
+			return -1;
+		}
+		nommu_addchr(&ctx->as_string, ch);
+		command->cmd_type = CMD_FUNCDEF;
+		goto skip;
+	}
+#endif
+
+#if 0 /* Prevented by caller */
+	if (command->argv /* word [word]{... */
+	 || ctx->word.length /* word{... */
+	 || ctx->word.has_quoted_part /* ""{... */
+	) {
+		syntax_error(NULL);
+		debug_printf_parse("parse_group return -1: "
+			"syntax error, groups and arglists don't mix\n");
+		return -1;
+	}
+#endif
+
+ IF_HUSH_FUNCTIONS(skip:)
+
+	endch = '}';
+	if (ch == '(') {
+		endch = ')';
+		IF_HUSH_FUNCTIONS(if (command->cmd_type != CMD_FUNCDEF))
+			command->cmd_type = CMD_SUBSHELL;
+	} else {
+		/* bash does not allow "{echo...", requires whitespace */
+		ch = i_peek(input);
+		if (ch != ' ' && ch != '\t' && ch != '\n'
+		 && ch != '('	/* but "{(..." is allowed (without whitespace) */
+		) {
+			syntax_error_unexpected_ch(ch);
+			return -1;
+		}
+		if (ch != '(') {
+			ch = i_getch(input);
+			nommu_addchr(&ctx->as_string, ch);
+		}
+	}
+
+	debug_printf_heredoc("calling parse_stream, heredoc_cnt:%d\n", heredoc_cnt);
+	pipe_list = parse_stream(&as_string, &heredoc_cnt, input, endch);
+	debug_printf_heredoc("parse_stream returned: heredoc_cnt:%d\n", heredoc_cnt);
+#if !BB_MMU
+	if (as_string)
+		o_addstr(&ctx->as_string, as_string);
+#endif
+
+	/* empty ()/{} or parse error? */
+	if (!pipe_list || pipe_list == ERR_PTR) {
+		/* parse_stream already emitted error msg */
+		if (!BB_MMU)
+			free(as_string);
+		debug_printf_parse("parse_group return -1: "
+			"parse_stream returned %p\n", pipe_list);
+		return -1;
+	}
+#if !BB_MMU
+	as_string[strlen(as_string) - 1] = '\0'; /* plink ')' or '}' */
+	command->group_as_string = as_string;
+	debug_printf_parse("end of group, remembering as:'%s'\n",
+			command->group_as_string);
+#endif
+
+#if ENABLE_HUSH_FUNCTIONS
+	/* Convert "f() (cmds)" to "f() {(cmds)}" */
+	if (command->cmd_type == CMD_FUNCDEF && endch == ')') {
+		struct command *cmd2;
+
+		cmd2 = xzalloc(sizeof(*cmd2));
+		cmd2->cmd_type = CMD_SUBSHELL;
+		cmd2->group = pipe_list;
+# if !BB_MMU
+//UNTESTED!
+		cmd2->group_as_string = command->group_as_string;
+		command->group_as_string = xasprintf("(%s)", command->group_as_string);
+# endif
+
+		pipe_list = new_pipe();
+		pipe_list->cmds = cmd2;
+		pipe_list->num_cmds = 1;
+	}
+#endif
+
+	command->group = pipe_list;
+
+	debug_printf_parse("parse_group return %d\n", heredoc_cnt);
+	return heredoc_cnt;
+	/* command remains "open", available for possible redirects */
+#undef as_string
+}
+
+#if ENABLE_HUSH_TICK || ENABLE_FEATURE_SH_MATH || ENABLE_HUSH_DOLLAR_OPS
+/* Subroutines for copying $(...) and `...` things */
+/* '...' */
+static int add_till_single_quote(o_string *dest, struct in_str *input)
+{
+	while (1) {
+		int ch = i_getch(input);
+		if (ch == EOF) {
+			syntax_error_unterm_ch('\'');
+			return 0;
+		}
+		if (ch == '\'')
+			return 1;
+		o_addchr(dest, ch);
+	}
+}
+static int add_till_single_quote_dquoted(o_string *dest, struct in_str *input)
+{
+	while (1) {
+		int ch = i_getch(input);
+		if (ch == EOF) {
+			syntax_error_unterm_ch('\'');
+			return 0;
+		}
+		if (ch == '\'')
+			return 1;
+		o_addqchr(dest, ch);
+	}
+}
+/* "...\"...`..`...." - do we need to handle "...$(..)..." too? */
+static int add_till_backquote(o_string *dest, struct in_str *input, int in_dquote);
+static int add_till_double_quote(o_string *dest, struct in_str *input)
+{
+	while (1) {
+		int ch = i_getch(input);
+		if (ch == EOF) {
+			syntax_error_unterm_ch('"');
+			return 0;
+		}
+		if (ch == '"')
+			return 1;
+		if (ch == '\\') {  /* \x. Copy both chars. */
+			o_addchr(dest, ch);
+			ch = i_getch(input);
+		}
+		o_addchr(dest, ch);
+		if (ch == '`') {
+			if (!add_till_backquote(dest, input, /*in_dquote:*/ 1))
+				return 0;
+			o_addchr(dest, ch);
+			continue;
+		}
+		//if (ch == '$') ...
+	}
+}
+/* Process `cmd` - copy contents until "`" is seen. Complicated by
+ * \` quoting.
+ * "Within the backquoted style of command substitution, backslash
+ * shall retain its literal meaning, except when followed by: '$', '`', or '\'.
+ * The search for the matching backquote shall be satisfied by the first
+ * backquote found without a preceding backslash; during this search,
+ * if a non-escaped backquote is encountered within a shell comment,
+ * a here-document, an embedded command substitution of the $(command)
+ * form, or a quoted string, undefined results occur. A single-quoted
+ * or double-quoted string that begins, but does not end, within the
+ * "`...`" sequence produces undefined results."
+ * Example                               Output
+ * echo `echo '\'TEST\`echo ZZ\`BEST`    \TESTZZBEST
+ */
+static int add_till_backquote(o_string *dest, struct in_str *input, int in_dquote)
+{
+	while (1) {
+		int ch = i_getch(input);
+		if (ch == '`')
+			return 1;
+		if (ch == '\\') {
+			/* \x. Copy both unless it is \`, \$, \\ and maybe \" */
+			ch = i_getch(input);
+			if (ch != '`'
+			 && ch != '$'
+			 && ch != '\\'
+			 && (!in_dquote || ch != '"')
+			) {
+				o_addchr(dest, '\\');
+			}
+		}
+		if (ch == EOF) {
+			syntax_error_unterm_ch('`');
+			return 0;
+		}
+		o_addchr(dest, ch);
+	}
+}
+/* Process $(cmd) - copy contents until ")" is seen. Complicated by
+ * quoting and nested ()s.
+ * "With the $(command) style of command substitution, all characters
+ * following the open parenthesis to the matching closing parenthesis
+ * constitute the command. Any valid shell script can be used for command,
+ * except a script consisting solely of redirections which produces
+ * unspecified results."
+ * Example                              Output
+ * echo $(echo '(TEST)' BEST)           (TEST) BEST
+ * echo $(echo 'TEST)' BEST)            TEST) BEST
+ * echo $(echo \(\(TEST\) BEST)         ((TEST) BEST
+ *
+ * Also adapted to eat ${var%...} and $((...)) constructs, since ... part
+ * can contain arbitrary constructs, just like $(cmd).
+ * In bash compat mode, it needs to also be able to stop on ':' or '/'
+ * for ${var:N[:M]} and ${var/P[/R]} parsing.
+ */
+#define DOUBLE_CLOSE_CHAR_FLAG 0x80
+static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsigned end_ch)
+{
+	int ch;
+	char dbl = end_ch & DOUBLE_CLOSE_CHAR_FLAG;
+# if BASH_SUBSTR || BASH_PATTERN_SUBST
+	char end_char2 = end_ch >> 8;
+# endif
+	end_ch &= (DOUBLE_CLOSE_CHAR_FLAG - 1);
+
+# if ENABLE_HUSH_INTERACTIVE
+	G.promptmode = 1; /* PS2 */
+# endif
+	debug_printf_prompt("%s promptmode=%d\n", __func__, G.promptmode);
+
+	while (1) {
+		ch = i_getch(input);
+		if (ch == EOF) {
+			syntax_error_unterm_ch(end_ch);
+			return 0;
+		}
+		if (ch == end_ch
+# if BASH_SUBSTR || BASH_PATTERN_SUBST
+		 || ch == end_char2
+# endif
+		) {
+			if (!dbl)
+				break;
+			/* we look for closing )) of $((EXPR)) */
+			if (i_peek_and_eat_bkslash_nl(input) == end_ch) {
+				i_getch(input); /* eat second ')' */
+				break;
+			}
+		}
+		o_addchr(dest, ch);
+		//bb_error_msg("%s:o_addchr('%c')", __func__, ch);
+		if (ch == '(' || ch == '{') {
+			ch = (ch == '(' ? ')' : '}');
+			if (!add_till_closing_bracket(dest, input, ch))
+				return 0;
+			o_addchr(dest, ch);
+			continue;
+		}
+		if (ch == '\'') {
+			if (!add_till_single_quote(dest, input))
+				return 0;
+			o_addchr(dest, ch);
+			continue;
+		}
+		if (ch == '"') {
+			if (!add_till_double_quote(dest, input))
+				return 0;
+			o_addchr(dest, ch);
+			continue;
+		}
+		if (ch == '`') {
+			if (!add_till_backquote(dest, input, /*in_dquote:*/ 0))
+				return 0;
+			o_addchr(dest, ch);
+			continue;
+		}
+		if (ch == '\\') {
+			/* \x. Copy verbatim. Important for  \(, \) */
+			ch = i_getch(input);
+			if (ch == EOF) {
+				syntax_error_unterm_ch(end_ch);
+				return 0;
+			}
+# if 0
+			if (ch == '\n') {
+				/* "backslash+newline", ignore both */
+				o_delchr(dest); /* undo insertion of '\' */
+				continue;
+			}
+# endif
+			o_addchr(dest, ch);
+			//bb_error_msg("%s:o_addchr('%c') after '\\'", __func__, ch);
+			continue;
+		}
+	}
+	debug_printf_parse("%s return '%s' ch:'%c'\n", __func__, dest->data, ch);
+	return ch;
+}
+#endif /* ENABLE_HUSH_TICK || ENABLE_FEATURE_SH_MATH || ENABLE_HUSH_DOLLAR_OPS */
+
+#if BASH_DOLLAR_SQUOTE
+/* Return code: 1 for "found and parsed", 0 for "seen something else" */
+# if BB_MMU
+#define parse_dollar_squote(as_string, dest, input) \
+	parse_dollar_squote(dest, input)
+#define as_string NULL
+# endif
+static int parse_dollar_squote(o_string *as_string, o_string *dest, struct in_str *input)
+{
+	int start;
+	int ch = i_peek_and_eat_bkslash_nl(input);  /* first character after the $ */
+	debug_printf_parse("parse_dollar_squote entered: ch='%c'\n", ch);
+	if (ch != '\'')
+		return 0;
+
+	dest->has_quoted_part = 1;
+	start = dest->length;
+
+	ch = i_getch(input); /* eat ' */
+	nommu_addchr(as_string, ch);
+	while (1) {
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+		if (ch == EOF) {
+			syntax_error_unterm_ch('\'');
+			return 0;
+		}
+		if (ch == '\'')
+			break;
+		if (ch == SPECIAL_VAR_SYMBOL) {
+			/* Convert raw ^C to corresponding special variable reference */
+			o_addchr(dest, SPECIAL_VAR_SYMBOL);
+			o_addchr(dest, SPECIAL_VAR_QUOTED_SVS);
+			/* will addchr() another SPECIAL_VAR_SYMBOL (see after the if() block) */
+		} else if (ch == '\\') {
+			static const char C_escapes[] ALIGN1 = "nrbtfav""x\\01234567";
+
+			ch = i_getch(input);
+			nommu_addchr(as_string, ch);
+			if (strchr(C_escapes, ch)) {
+				char buf[4];
+				char *p = buf;
+				int cnt = 2;
+
+				buf[0] = ch;
+				if ((unsigned char)(ch - '0') <= 7) { /* \ooo */
+					do {
+						ch = i_peek(input);
+						if ((unsigned char)(ch - '0') > 7)
+							break;
+						*++p = ch = i_getch(input);
+						nommu_addchr(as_string, ch);
+					} while (--cnt != 0);
+				} else if (ch == 'x') { /* \xHH */
+					do {
+						ch = i_peek(input);
+						if (!isxdigit(ch))
+							break;
+						*++p = ch = i_getch(input);
+						nommu_addchr(as_string, ch);
+					} while (--cnt != 0);
+					if (cnt == 2) { /* \x but next char is "bad" */
+						ch = 'x';
+						goto unrecognized;
+					}
+				} /* else simple seq like \\ or \t */
+				*++p = '\0';
+				p = buf;
+				ch = bb_process_escape_sequence((void*)&p);
+				//bb_error_msg("buf:'%s' ch:%x", buf, ch);
+				if (ch == '\0')
+					continue; /* bash compat: $'...\0...' emits nothing */
+			} else { /* unrecognized "\z": encode both chars unless ' or " */
+				if (ch != '\'' && ch != '"') {
+ unrecognized:
+					o_addqchr(dest, '\\');
+				}
+			}
+		} /* if (\...) */
+		o_addqchr(dest, ch);
+	}
+
+	if (dest->length == start) {
+		/* $'', $'\0', $'\000\x00' and the like */
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+	}
+
+	return 1;
+# undef as_string
+}
+#else
+# #define parse_dollar_squote(as_string, dest, input) 0
+#endif /* BASH_DOLLAR_SQUOTE */
+
+/* Return code: 0 for OK, 1 for syntax error */
+#if BB_MMU
+#define parse_dollar(as_string, dest, input, quote_mask) \
+	parse_dollar(dest, input, quote_mask)
+#define as_string NULL
+#endif
+static int parse_dollar(o_string *as_string,
+		o_string *dest,
+		struct in_str *input, unsigned char quote_mask)
+{
+	int ch = i_peek_and_eat_bkslash_nl(input);  /* first character after the $ */
+
+	debug_printf_parse("parse_dollar entered: ch='%c' quote_mask:0x%x\n", ch, quote_mask);
+	if (isalpha(ch)) {
+ make_var:
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+ /*make_var1:*/
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		while (1) {
+			debug_printf_parse(": '%c'\n", ch);
+			o_addchr(dest, ch | quote_mask);
+			quote_mask = 0;
+			ch = i_peek_and_eat_bkslash_nl(input);
+			if (!isalnum(ch) && ch != '_') {
+				/* End of variable name reached */
+				break;
+			}
+			ch = i_getch(input);
+			nommu_addchr(as_string, ch);
+		}
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+	} else if (isdigit(ch)) {
+ make_one_char_var:
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		debug_printf_parse(": '%c'\n", ch);
+		o_addchr(dest, ch | quote_mask);
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+	} else switch (ch) {
+	case '$': /* pid */
+	case '!': /* last bg pid */
+	case '?': /* last exit code */
+	case '#': /* number of args */
+	case '*': /* args */
+	case '@': /* args */
+	case '-': /* $- option flags set by set builtin or shell options (-i etc) */
+		goto make_one_char_var;
+	case '{': {
+		char len_single_ch;
+
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+
+		ch = i_getch(input); /* eat '{' */
+		nommu_addchr(as_string, ch);
+
+		ch = i_getch_and_eat_bkslash_nl(input); /* first char after '{' */
+		/* It should be ${?}, or ${#var},
+		 * or even ${?+subst} - operator acting on a special variable,
+		 * or the beginning of variable name.
+		 */
+		if (ch == EOF
+		 || (!strchr(_SPECIAL_VARS_STR, ch) && !isalnum(ch)) /* not one of those */
+		) {
+ bad_dollar_syntax:
+			syntax_error_unterm_str("${name}");
+			debug_printf_parse("parse_dollar return 0: unterminated ${name}\n");
+			return 0;
+		}
+		nommu_addchr(as_string, ch);
+		len_single_ch = ch;
+		ch |= quote_mask;
+
+		/* It's possible to just call add_till_closing_bracket() at this point.
+		 * However, this regresses some of our testsuite cases
+		 * which check invalid constructs like ${%}.
+		 * Oh well... let's check that the var name part is fine... */
+
+		if (isdigit(len_single_ch)
+		 || (len_single_ch == '#' && isdigit(i_peek_and_eat_bkslash_nl(input)))
+		) {
+			/* Execution engine uses plain xatoi_positive()
+			 * to interpret ${NNN} and {#NNN},
+			 * check syntax here in the parser.
+			 * (bash does not support expressions in ${#NN},
+			 * e.g. ${#$var} and {#1:+WORD} are not supported).
+			 */
+			unsigned cnt = 9; /* max 9 digits for ${NN} and 8 for {#NN} */
+			while (1) {
+				o_addchr(dest, ch);
+				debug_printf_parse(": '%c'\n", ch);
+				ch = i_getch_and_eat_bkslash_nl(input);
+				nommu_addchr(as_string, ch);
+				if (ch == '}')
+					break;
+				if (--cnt == 0)
+					goto bad_dollar_syntax;
+				if (len_single_ch != '#' && strchr(VAR_SUBST_OPS, ch))
+					/* ${NN<op>...} is valid */
+					goto eat_until_closing;
+				if (!isdigit(ch))
+					goto bad_dollar_syntax;
+			}
+		} else
+		while (1) {
+			unsigned pos;
+
+			o_addchr(dest, ch);
+			debug_printf_parse(": '%c'\n", ch);
+
+			ch = i_getch(input);
+			nommu_addchr(as_string, ch);
+			if (ch == '}')
+				break;
+			if (!isalnum(ch) && ch != '_') {
+				unsigned end_ch;
+				unsigned char last_ch;
+				/* handle parameter expansions
+				 * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
+				 */
+				if (!strchr(VAR_SUBST_OPS, ch)) { /* ${var<bad_char>... */
+					if (len_single_ch != '#'
+					/*|| !strchr(SPECIAL_VARS_STR, ch) - disallow errors like ${#+} ? */
+					 || i_peek(input) != '}'
+					) {
+						goto bad_dollar_syntax;
+					}
+					/* else: it's "length of C" ${#C} op,
+					 * where C is a single char
+					 * special var name, e.g. ${#!}.
+					 */
+				}
+ eat_until_closing:
+				/* Eat everything until closing '}' (or ':') */
+				end_ch = '}';
+				if (BASH_SUBSTR
+				 && ch == ':'
+				 && !strchr(MINUS_PLUS_EQUAL_QUESTION, i_peek(input))
+				) {
+					/* It's ${var:N[:M]} thing */
+					end_ch = '}' * 0x100 + ':';
+				}
+				if (BASH_PATTERN_SUBST
+				 && ch == '/'
+				) {
+					/* It's ${var/[/]pattern[/repl]} thing */
+					if (i_peek(input) == '/') { /* ${var//pattern[/repl]}? */
+						i_getch(input);
+						nommu_addchr(as_string, '/');
+						ch = '\\';
+					}
+					end_ch = '}' * 0x100 + '/';
+				}
+				o_addchr(dest, ch);
+				/* The pattern can't be empty.
+				 * IOW: if the first char after "${v//" is a slash,
+				 * it does not terminate the pattern - it's the first char of the pattern:
+				 *  v=/dev/ram; echo ${v////-}  prints -dev-ram (pattern is "/")
+				 *  v=/dev/ram; echo ${v///r/-} prints /dev-am  (pattern is "/r")
+				 */
+				if (i_peek(input) == '/') {
+					o_addchr(dest, i_getch(input));
+				}
+ again:
+				if (!BB_MMU)
+					pos = dest->length;
+#if ENABLE_HUSH_DOLLAR_OPS
+				last_ch = add_till_closing_bracket(dest, input, end_ch);
+				if (last_ch == 0) /* error? */
+					return 0;
+#else
+# error Simple code to only allow ${var} is not implemented
+#endif
+				if (as_string) {
+					o_addstr(as_string, dest->data + pos);
+					o_addchr(as_string, last_ch);
+				}
+
+				if ((BASH_SUBSTR || BASH_PATTERN_SUBST)
+					 && (end_ch & 0xff00)
+				) {
+					/* close the first block: */
+					o_addchr(dest, SPECIAL_VAR_SYMBOL);
+					/* while parsing N from ${var:N[:M]}
+					 * or pattern from ${var/[/]pattern[/repl]} */
+					if ((end_ch & 0xff) == last_ch) {
+						/* got ':' or '/'- parse the rest */
+						end_ch = '}';
+						goto again;
+					}
+					/* got '}' */
+					if (BASH_SUBSTR && end_ch == '}' * 0x100 + ':') {
+						/* it's ${var:N} - emulate :999999999 */
+						o_addstr(dest, "999999999");
+					} /* else: it's ${var/[/]pattern} */
+				}
+				break;
+			}
+			len_single_ch = 0; /* it can't be ${#C} op */
+		}
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		break;
+	}
+#if ENABLE_FEATURE_SH_MATH || ENABLE_HUSH_TICK
+	case '(': {
+		unsigned pos;
+
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+# if ENABLE_FEATURE_SH_MATH
+		if (i_peek_and_eat_bkslash_nl(input) == '(') {
+			ch = i_getch(input);
+			nommu_addchr(as_string, ch);
+			o_addchr(dest, SPECIAL_VAR_SYMBOL);
+			o_addchr(dest, quote_mask | '+');
+			if (!BB_MMU)
+				pos = dest->length;
+			if (!add_till_closing_bracket(dest, input, ')' | DOUBLE_CLOSE_CHAR_FLAG))
+				return 0; /* error */
+			if (as_string) {
+				o_addstr(as_string, dest->data + pos);
+				o_addchr(as_string, ')');
+				o_addchr(as_string, ')');
+			}
+			o_addchr(dest, SPECIAL_VAR_SYMBOL);
+			break;
+		}
+# endif
+# if ENABLE_HUSH_TICK
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		o_addchr(dest, quote_mask | '`');
+		if (!BB_MMU)
+			pos = dest->length;
+		if (!add_till_closing_bracket(dest, input, ')'))
+			return 0; /* error */
+		if (as_string) {
+			o_addstr(as_string, dest->data + pos);
+			o_addchr(as_string, ')');
+		}
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+# endif
+		break;
+	}
+#endif
+	case '_':
+		goto make_var;
+#if 0
+	/* TODO: $_: */
+	/* $_ Shell or shell script name; or last argument of last command
+	 * (if last command wasn't a pipe; if it was, bash sets $_ to "");
+	 * but in command's env, set to full pathname used to invoke it */
+		ch = i_getch(input);
+		nommu_addchr(as_string, ch);
+		ch = i_peek_and_eat_bkslash_nl(input);
+		if (isalnum(ch)) { /* it's $_name or $_123 */
+			ch = '_';
+			goto make_var1;
+		}
+		/* else: it's $_ */
+#endif
+	default:
+		o_addQchr(dest, '$');
+	}
+	debug_printf_parse("parse_dollar return 1 (ok)\n");
+	return 1;
+#undef as_string
+}
+
+#if BB_MMU
+#define encode_string(as_string, dest, input, dquote_end) \
+	encode_string(dest, input, dquote_end)
+#define as_string NULL
+#endif
+static int encode_string(o_string *as_string,
+		o_string *dest,
+		struct in_str *input,
+		int dquote_end)
+{
+	int ch;
+	int next;
+
+ again:
+	ch = i_getch(input);
+	if (ch != EOF)
+		nommu_addchr(as_string, ch);
+	if (ch == dquote_end) { /* may be only '"' or EOF */
+		debug_printf_parse("encode_string return 1 (ok)\n");
+		return 1;
+	}
+	/* note: can't move it above ch == dquote_end check! */
+	if (ch == EOF) {
+		syntax_error_unterm_ch('"');
+		return 0; /* error */
+	}
+	next = '\0';
+	if (ch != '\n') {
+		next = i_peek(input);
+	}
+	debug_printf_parse("\" ch=%c (%d) escape=%d\n",
+			ch, ch, !!(dest->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+	if (ch == '\\') {
+		if (next == EOF) {
+			/* Testcase: in interactive shell a file with
+			 *  echo "unterminated string\<eof>
+			 * is sourced.
+			 */
+			syntax_error_unterm_ch('"');
+			return 0; /* error */
+		}
+		/* bash:
+		 * "The backslash retains its special meaning [in "..."]
+		 * only when followed by one of the following characters:
+		 * $, `, ", \, or <newline>.  A double quote may be quoted
+		 * within double quotes by preceding it with a backslash."
+		 * NB: in (unquoted) heredoc, above does not apply to ",
+		 * therefore we check for it by "next == dquote_end" cond.
+		 */
+		if (next == dquote_end || strchr("$`\\\n", next)) {
+			ch = i_getch(input); /* eat next */
+			if (ch == '\n')
+				goto again; /* skip \<newline> */
+		} /* else: ch remains == '\\', and we double it below: */
+		o_addqchr(dest, ch); /* \c if c is a glob char, else just c */
+		nommu_addchr(as_string, ch);
+		goto again;
+	}
+	if (ch == '$') {
+		//if (parse_dollar_squote(as_string, dest, input))
+		//	goto again;
+		if (!parse_dollar(as_string, dest, input, /*quote_mask:*/ 0x80)) {
+			debug_printf_parse("encode_string return 0: "
+					"parse_dollar returned 0 (error)\n");
+			return 0;
+		}
+		goto again;
+	}
+#if ENABLE_HUSH_TICK
+	if (ch == '`') {
+		//unsigned pos = dest->length;
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		o_addchr(dest, 0x80 | '`');
+		if (!add_till_backquote(dest, input, /*in_dquote:*/ dquote_end == '"'))
+			return 0; /* error */
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+		//debug_printf_subst("SUBST RES3 '%s'\n", dest->data + pos);
+		goto again;
+	}
+#endif
+	o_addQchr(dest, ch);
+	if (ch == SPECIAL_VAR_SYMBOL) {
+		/* Convert "^C" to corresponding special variable reference */
+		o_addchr(dest, SPECIAL_VAR_QUOTED_SVS);
+		o_addchr(dest, SPECIAL_VAR_SYMBOL);
+	}
+	goto again;
+#undef as_string
+}
+
+/*
+ * Scan input until EOF or end_trigger char.
+ * Return a list of pipes to execute, or NULL on EOF
+ * or if end_trigger character is met.
+ * On syntax error, exit if shell is not interactive,
+ * reset parsing machinery and start parsing anew,
+ * or return ERR_PTR.
+ */
+static struct pipe *parse_stream(char **pstring,
+		int *heredoc_cnt_ptr,
+		struct in_str *input,
+		int end_trigger)
+{
+	struct parse_context ctx;
+	int heredoc_cnt;
+
+	/* Single-quote triggers a bypass of the main loop until its mate is
+	 * found.  When recursing, quote state is passed in via ctx.word.o_expflags.
+	 */
+	debug_printf_parse("parse_stream entered, end_trigger='%c'\n",
+			end_trigger ? end_trigger : 'X');
+	debug_enter();
+
+	initialize_context(&ctx);
+
+	/* If very first arg is "" or '', ctx.word.data may end up NULL.
+	 * Preventing this:
+	 */
+	ctx.word.data = xzalloc(1); /* start as "", not as NULL */
+
+	/* We used to separate words on $IFS here. This was wrong.
+	 * $IFS is used only for word splitting when $var is expanded,
+	 * here we should use blank chars as separators, not $IFS
+	 */
+
+	heredoc_cnt = 0;
+	while (1) {
+		const char *is_blank;
+		const char *is_special;
+		int ch;
+		int next;
+		int redir_fd;
+		redir_type redir_style;
+
+		ch = i_getch(input);
+		debug_printf_parse(": ch=%c (%d) escape=%d\n",
+				ch, ch, !!(ctx.word.o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+		if (ch == EOF) {
+			struct pipe *pi;
+
+			if (heredoc_cnt) {
+				syntax_error_unterm_str("here document");
+				goto parse_error_exitcode1;
+			}
+			if (end_trigger == ')') {
+				syntax_error_unterm_ch('(');
+				goto parse_error_exitcode1;
+			}
+			if (end_trigger == '}') {
+				syntax_error_unterm_ch('{');
+				goto parse_error_exitcode1;
+			}
+
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			o_free_and_set_NULL(&ctx.word);
+			done_pipe(&ctx, PIPE_SEQ);
+			pi = ctx.list_head;
+			/* If we got nothing... */
+			/* (this makes bare "&" cmd a no-op.
+			 * bash says: "syntax error near unexpected token '&'") */
+			if (pi->num_cmds == 0
+			IF_HAS_KEYWORDS(&& pi->res_word == RES_NONE)
+			) {
+				free_pipe_list(pi);
+				pi = NULL;
+			}
+#if !BB_MMU
+			debug_printf_parse("as_string1 '%s'\n", ctx.as_string.data);
+			if (pstring)
+				*pstring = ctx.as_string.data;
+			else
+				o_free(&ctx.as_string);
+#endif
+			// heredoc_cnt must be 0 here anyway
+			//if (heredoc_cnt_ptr)
+			//	*heredoc_cnt_ptr = heredoc_cnt;
+			debug_leave();
+			debug_printf_heredoc("parse_stream return heredoc_cnt:%d\n", heredoc_cnt);
+			debug_printf_parse("parse_stream return %p\n", pi);
+			return pi;
+		}
+
+		/* Handle "'" and "\" first, as they won't play nice with
+		 * i_peek_and_eat_bkslash_nl() anyway:
+		 *   echo z\\
+		 * and
+		 *   echo '\
+		 *   '
+		 * would break.
+		 */
+		if (ch == '\\') {
+			ch = i_getch(input);
+			if (ch == '\n')
+				continue; /* drop \<newline>, get next char */
+			nommu_addchr(&ctx.as_string, '\\');
+			if (ch == SPECIAL_VAR_SYMBOL) {
+				nommu_addchr(&ctx.as_string, ch);
+				/* Convert \^C to corresponding special variable reference */
+				goto case_SPECIAL_VAR_SYMBOL;
+			}
+			o_addchr(&ctx.word, '\\');
+			if (ch == EOF) {
+				/* Testcase: eval 'echo Ok\' */
+				/* bash-4.3.43 was removing backslash,
+				 * but 4.4.19 retains it, most other shells too
+				 */
+				continue; /* get next char */
+			}
+			/* Example: echo Hello \2>file
+			 * we need to know that word 2 is quoted
+			 */
+			ctx.word.has_quoted_part = 1;
+			nommu_addchr(&ctx.as_string, ch);
+			o_addchr(&ctx.word, ch);
+			continue; /* get next char */
+		}
+		nommu_addchr(&ctx.as_string, ch);
+		if (ch == '\'') {
+			ctx.word.has_quoted_part = 1;
+			next = i_getch(input);
+			if (next == '\'' && !ctx.pending_redirect)
+				goto insert_empty_quoted_str_marker;
+
+			ch = next;
+			while (1) {
+				if (ch == EOF) {
+					syntax_error_unterm_ch('\'');
+					goto parse_error_exitcode1;
+				}
+				nommu_addchr(&ctx.as_string, ch);
+				if (ch == '\'')
+					break;
+				if (ch == SPECIAL_VAR_SYMBOL) {
+					/* Convert raw ^C to corresponding special variable reference */
+					o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
+					o_addchr(&ctx.word, SPECIAL_VAR_QUOTED_SVS);
+				}
+				o_addqchr(&ctx.word, ch);
+				ch = i_getch(input);
+			}
+			continue; /* get next char */
+		}
+
+		next = '\0';
+		if (ch != '\n')
+			next = i_peek_and_eat_bkslash_nl(input);
+
+		is_special = "{}<>&|();#" /* special outside of "str" */
+				"$\"" IF_HUSH_TICK("`") /* always special */
+				SPECIAL_VAR_SYMBOL_STR;
+#if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
+		if (ctx.command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB) {
+			/* In [[ ]], {}<>&|() are not special */
+			is_special += 8;
+		} else
+#endif
+		/* Are { and } special here? */
+		if (ctx.command->argv /* word [word]{... - non-special */
+		 || ctx.word.length       /* word{... - non-special */
+		 || ctx.word.has_quoted_part     /* ""{... - non-special */
+		 || (next != ';'             /* }; - special */
+		    && next != ')'           /* }) - special */
+		    && next != '('           /* {( - special */
+		    && next != '&'           /* }& and }&& ... - special */
+		    && next != '|'           /* }|| ... - special */
+		    && !strchr(defifs, next) /* {word - non-special */
+		    )
+		) {
+			/* They are not special, skip "{}" */
+			is_special += 2;
+		}
+		is_special = strchr(is_special, ch);
+		is_blank = strchr(defifs, ch);
+
+		if (!is_special && !is_blank) { /* ordinary char */
+ ordinary_char:
+			o_addQchr(&ctx.word, ch);
+			if ((ctx.is_assignment == MAYBE_ASSIGNMENT
+			    || ctx.is_assignment == WORD_IS_KEYWORD)
+			 && ch == '='
+			 && endofname(ctx.word.data)[0] == '='
+			) {
+				ctx.is_assignment = DEFINITELY_ASSIGNMENT;
+				debug_printf_parse("ctx.is_assignment='%s'\n", assignment_flag[ctx.is_assignment]);
+			}
+			continue;
+		}
+
+		if (is_blank) {
+#if ENABLE_HUSH_LINENO_VAR
+/* Case:
+ * "while ...; do<whitespace><newline>
+ *	cmd ..."
+ * would think that "cmd" starts in <whitespace> -
+ * i.e., at the previous line.
+ * We need to skip all whitespace before newlines.
+ */
+			while (ch != '\n') {
+				next = i_peek(input);
+				if (next != ' ' && next != '\t' && next != '\n')
+					break; /* next char is not ws */
+				ch = i_getch(input);
+			}
+			/* ch == last eaten whitespace char */
+#endif
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			if (ch == '\n') {
+				/* Is this a case when newline is simply ignored?
+				 * Some examples:
+				 * "cmd | <newline> cmd ..."
+				 * "case ... in <newline> word) ..."
+				 */
+				if (IS_NULL_CMD(ctx.command)
+				 && ctx.word.length == 0
+				 && !ctx.word.has_quoted_part
+				 && heredoc_cnt == 0
+				) {
+					/* This newline can be ignored. But...
+					 * Without check #1, interactive shell
+					 * ignores even bare <newline>,
+					 * and shows the continuation prompt:
+					 * ps1_prompt$ <enter>
+					 * ps2> _   <=== wrong, should be ps1
+					 * Without check #2, "cmd & <newline>"
+					 * is similarly mistreated.
+					 * (BTW, this makes "cmd & cmd"
+					 * and "cmd && cmd" non-orthogonal.
+					 * Really, ask yourself, why
+					 * "cmd && <newline>" doesn't start
+					 * cmd but waits for more input?
+					 * 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 */
+					 && pi->followup != PIPE_BG /* check #2 */
+					) {
+						continue;
+					}
+				}
+				/* Treat newline as a command separator. */
+				done_pipe(&ctx, PIPE_SEQ);
+				debug_printf_heredoc("heredoc_cnt:%d\n", heredoc_cnt);
+				if (heredoc_cnt) {
+					heredoc_cnt = fetch_heredocs(&ctx.as_string, ctx.list_head, heredoc_cnt, input);
+					if (heredoc_cnt != 0)
+						goto parse_error_exitcode1;
+				}
+				ctx.is_assignment = MAYBE_ASSIGNMENT;
+				debug_printf_parse("ctx.is_assignment='%s'\n", assignment_flag[ctx.is_assignment]);
+				ch = ';';
+				/* note: if (is_blank) continue;
+				 * will still trigger for us */
+			}
+		}
+
+		/* "cmd}" or "cmd }..." without semicolon or &:
+		 * } is an ordinary char in this case, even inside { cmd; }
+		 * Pathological example: { ""}; } should exec "}" cmd
+		 */
+		if (ch == '}') {
+			if (ctx.word.length != 0 /* word} */
+			 || ctx.word.has_quoted_part    /* ""} */
+			) {
+				goto ordinary_char;
+			}
+			if (!IS_NULL_CMD(ctx.command)) { /* cmd } */
+				/* Generally, there should be semicolon: "cmd; }"
+				 * However, bash allows to omit it if "cmd" is
+				 * a group. Examples:
+				 * { { echo 1; } }
+				 * {(echo 1)}
+				 * { echo 0 >&2 | { echo 1; } }
+				 * { while false; do :; done }
+				 * { case a in b) ;; esac }
+				 */
+				if (ctx.command->group)
+					goto term_group;
+				goto ordinary_char;
+			}
+			if (!IS_NULL_PIPE(ctx.pipe)) /* cmd | } */
+				/* Can't be an end of {cmd}, skip the check */
+				goto skip_end_trigger;
+			/* else: } does terminate a group */
+		}
+ term_group:
+		if (end_trigger && end_trigger == ch
+		 && (ch != ';' || heredoc_cnt == 0)
+#if ENABLE_HUSH_CASE
+		 && (ch != ')'
+		    || ctx.ctx_res_w != RES_MATCH
+		    || (!ctx.word.has_quoted_part && strcmp(ctx.word.data, "esac") == 0)
+		    )
+#endif
+		) {
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			done_pipe(&ctx, PIPE_SEQ);
+			ctx.is_assignment = MAYBE_ASSIGNMENT;
+			debug_printf_parse("ctx.is_assignment='%s'\n", assignment_flag[ctx.is_assignment]);
+			/* Do we sit outside of any if's, loops or case's? */
+			if (!HAS_KEYWORDS
+			IF_HAS_KEYWORDS(|| (ctx.ctx_res_w == RES_NONE && ctx.old_flag == 0))
+			) {
+				o_free_and_set_NULL(&ctx.word);
+#if !BB_MMU
+				debug_printf_parse("as_string2 '%s'\n", ctx.as_string.data);
+				if (pstring)
+					*pstring = ctx.as_string.data;
+				else
+					o_free(&ctx.as_string);
+#endif
+				if (ch != ';' && IS_NULL_PIPE(ctx.list_head)) {
+					/* Example: bare "{ }", "()" */
+					G.last_exitcode = 2; /* bash compat */
+					syntax_error_unexpected_ch(ch);
+					goto parse_error;
+				}
+				if (heredoc_cnt_ptr)
+					*heredoc_cnt_ptr = heredoc_cnt;
+				debug_printf_heredoc("parse_stream return heredoc_cnt:%d\n", heredoc_cnt);
+				debug_printf_parse("parse_stream return %p: "
+						"end_trigger char found\n",
+						ctx.list_head);
+				debug_leave();
+				return ctx.list_head;
+			}
+		}
+
+		if (is_blank)
+			continue;
+
+		/* Catch <, > before deciding whether this word is
+		 * an assignment. a=1 2>z b=2: b=2 is still assignment */
+		switch (ch) {
+		case '>':
+			redir_fd = redirect_opt_num(&ctx.word);
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			redir_style = REDIRECT_OVERWRITE;
+			if (next == '>') {
+				redir_style = REDIRECT_APPEND;
+				ch = i_getch(input);
+				nommu_addchr(&ctx.as_string, ch);
+			}
+#if 0
+			else if (next == '(') {
+				syntax_error(">(process) not supported");
+				goto parse_error_exitcode1;
+			}
+#endif
+			if (parse_redirect(&ctx, redir_fd, redir_style, input))
+				goto parse_error_exitcode1;
+			continue; /* get next char */
+		case '<':
+			redir_fd = redirect_opt_num(&ctx.word);
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			redir_style = REDIRECT_INPUT;
+			if (next == '<') {
+				redir_style = REDIRECT_HEREDOC;
+				heredoc_cnt++;
+				debug_printf_heredoc("++heredoc_cnt=%d\n", heredoc_cnt);
+				ch = i_getch(input);
+				nommu_addchr(&ctx.as_string, ch);
+			} else if (next == '>') {
+				redir_style = REDIRECT_IO;
+				ch = i_getch(input);
+				nommu_addchr(&ctx.as_string, ch);
+			}
+#if 0
+			else if (next == '(') {
+				syntax_error("<(process) not supported");
+				goto parse_error_exitcode1;
+			}
+#endif
+			if (parse_redirect(&ctx, redir_fd, redir_style, input))
+				goto parse_error_exitcode1;
+			continue; /* get next char */
+		case '#':
+			if (ctx.word.length == 0 && !ctx.word.has_quoted_part) {
+				/* skip "#comment" */
+				/* note: we do not add it to &ctx.as_string */
+/* TODO: in bash:
+ * comment inside $() goes to the next \n, even inside quoted string (!):
+ * cmd "$(cmd2 #comment)" - syntax error
+ * cmd "`cmd2 #comment`" - ok
+ * We accept both (comment ends where command subst ends, in both cases).
+ */
+				while (1) {
+					ch = i_peek(input);
+					if (ch == '\n') {
+						nommu_addchr(&ctx.as_string, '\n');
+						break;
+					}
+					ch = i_getch(input);
+					if (ch == EOF)
+						break;
+				}
+				continue; /* get next char */
+			}
+			break;
+		}
+ skip_end_trigger:
+
+		if (ctx.is_assignment == MAYBE_ASSIGNMENT
+		 /* check that we are not in word in "a=1 2>word b=1": */
+		 && !ctx.pending_redirect
+		) {
+			/* ch is a special char and thus this word
+			 * cannot be an assignment */
+			ctx.is_assignment = NOT_ASSIGNMENT;
+			debug_printf_parse("ctx.is_assignment='%s'\n", assignment_flag[ctx.is_assignment]);
+		}
+
+		/* Note: nommu_addchr(&ctx.as_string, ch) is already done */
+
+		switch (ch) {
+		case_SPECIAL_VAR_SYMBOL:
+		case SPECIAL_VAR_SYMBOL:
+			/* Convert raw ^C to corresponding special variable reference */
+			o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
+			o_addchr(&ctx.word, SPECIAL_VAR_QUOTED_SVS);
+			/* fall through */
+		case '#':
+			/* non-comment #: "echo a#b" etc */
+			o_addchr(&ctx.word, ch);
+			continue; /* get next char */
+		case '$':
+			if (parse_dollar_squote(&ctx.as_string, &ctx.word, input))
+				continue; /* get next char */
+			if (!parse_dollar(&ctx.as_string, &ctx.word, input, /*quote_mask:*/ 0)) {
+				debug_printf_parse("parse_stream parse error: "
+					"parse_dollar returned 0 (error)\n");
+				goto parse_error_exitcode1;
+			}
+			continue; /* get next char */
+		case '"':
+			ctx.word.has_quoted_part = 1;
+			if (next == '"' && !ctx.pending_redirect) {
+				i_getch(input); /* eat second " */
+ insert_empty_quoted_str_marker:
+				nommu_addchr(&ctx.as_string, next);
+				o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
+				o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
+				continue; /* get next char */
+			}
+			if (ctx.is_assignment == NOT_ASSIGNMENT)
+				ctx.word.o_expflags |= EXP_FLAG_ESC_GLOB_CHARS;
+			if (!encode_string(&ctx.as_string, &ctx.word, input, '"'))
+				goto parse_error_exitcode1;
+			ctx.word.o_expflags &= ~EXP_FLAG_ESC_GLOB_CHARS;
+			continue; /* get next char */
+#if ENABLE_HUSH_TICK
+		case '`': {
+			USE_FOR_NOMMU(unsigned pos;)
+
+			o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
+			o_addchr(&ctx.word, '`');
+			USE_FOR_NOMMU(pos = ctx.word.length;)
+			if (!add_till_backquote(&ctx.word, input, /*in_dquote:*/ 0))
+				goto parse_error_exitcode1;
+# if !BB_MMU
+			o_addstr(&ctx.as_string, ctx.word.data + pos);
+			o_addchr(&ctx.as_string, '`');
+# endif
+			o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
+			//debug_printf_subst("SUBST RES3 '%s'\n", ctx.word.data + pos);
+			continue; /* get next char */
+		}
+#endif
+		case ';':
+#if ENABLE_HUSH_CASE
+ case_semi:
+#endif
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			done_pipe(&ctx, PIPE_SEQ);
+#if ENABLE_HUSH_CASE
+			/* Eat multiple semicolons, detect
+			 * whether it means something special */
+			while (1) {
+				ch = i_peek_and_eat_bkslash_nl(input);
+				if (ch != ';')
+					break;
+				ch = i_getch(input);
+				nommu_addchr(&ctx.as_string, ch);
+				if (ctx.ctx_res_w == RES_CASE_BODY) {
+					ctx.ctx_dsemicolon = 1;
+					ctx.ctx_res_w = RES_MATCH;
+					break;
+				}
+			}
+#endif
+ new_cmd:
+			/* We just finished a cmd. New one may start
+			 * with an assignment */
+			ctx.is_assignment = MAYBE_ASSIGNMENT;
+			debug_printf_parse("ctx.is_assignment='%s'\n", assignment_flag[ctx.is_assignment]);
+			continue; /* get next char */
+		case '&':
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+			if (next == '&') {
+				ch = i_getch(input);
+				nommu_addchr(&ctx.as_string, ch);
+				done_pipe(&ctx, PIPE_AND);
+			} else {
+				done_pipe(&ctx, PIPE_BG);
+			}
+			goto new_cmd;
+		case '|':
+			if (done_word(&ctx)) {
+				goto parse_error_exitcode1;
+			}
+#if ENABLE_HUSH_CASE
+			if (ctx.ctx_res_w == RES_MATCH)
+				break; /* we are in case's "word | word)" */
+#endif
+			if (next == '|') { /* || */
+				ch = i_getch(input);
+				nommu_addchr(&ctx.as_string, ch);
+				done_pipe(&ctx, PIPE_OR);
+			} else {
+				/* we could pick up a file descriptor choice here
+				 * with redirect_opt_num(), but bash doesn't do it.
+				 * "echo foo 2| cat" yields "foo 2". */
+				done_command(&ctx);
+			}
+			goto new_cmd;
+		case '(':
+#if ENABLE_HUSH_CASE
+			/* "case... in [(]word)..." - skip '(' */
+			if (ctx.ctx_res_w == RES_MATCH
+			 && ctx.command->argv == NULL /* not (word|(... */
+			 && ctx.word.length == 0 /* not word(... */
+			 && ctx.word.has_quoted_part == 0 /* not ""(... */
+			) {
+				continue; /* get next char */
+			}
+#endif
+			/* fall through */
+		case '{': {
+			int n = parse_group(&ctx, input, ch);
+			if (n < 0) {
+				goto parse_error_exitcode1;
+			}
+			debug_printf_heredoc("parse_group done, needs heredocs:%d\n", n);
+			heredoc_cnt += n;
+			goto new_cmd;
+		}
+		case ')':
+#if ENABLE_HUSH_CASE
+			if (ctx.ctx_res_w == RES_MATCH)
+				goto case_semi;
+#endif
+		case '}':
+			/* proper use of this character is caught by end_trigger:
+			 * if we see {, we call parse_group(..., end_trigger='}')
+			 * and it will match } earlier (not here). */
+			G.last_exitcode = 2;
+			syntax_error_unexpected_ch(ch);
+			goto parse_error;
+		default:
+			if (HUSH_DEBUG)
+				bb_error_msg_and_die("BUG: unexpected %c", ch);
+		}
+	} /* while (1) */
+
+ parse_error_exitcode1:
+	G.last_exitcode = 1;
+ parse_error:
+	{
+		struct parse_context *pctx;
+		IF_HAS_KEYWORDS(struct parse_context *p2;)
+
+		/* Clean up allocated tree.
+		 * Sample for finding leaks on syntax error recovery path.
+		 * Run it from interactive shell, watch pmap `pidof hush`.
+		 * while if false; then false; fi; do break; fi
+		 * Samples to catch leaks at execution:
+		 * while if (true | { true;}); then echo ok; fi; do break; done
+		 * while if (true | { true;}); then echo ok; fi; do (if echo ok; break; then :; fi) | cat; break; done
+		 */
+		pctx = &ctx;
+		do {
+			/* Update pipe/command counts,
+			 * otherwise freeing may miss some */
+			done_pipe(pctx, PIPE_SEQ);
+			debug_printf_clean("freeing list %p from ctx %p\n",
+					pctx->list_head, pctx);
+			debug_print_tree(pctx->list_head, 0);
+			free_pipe_list(pctx->list_head);
+			debug_printf_clean("freed list %p\n", pctx->list_head);
+#if !BB_MMU
+			o_free(&pctx->as_string);
+#endif
+			IF_HAS_KEYWORDS(p2 = pctx->stack;)
+			if (pctx != &ctx) {
+				free(pctx);
+			}
+			IF_HAS_KEYWORDS(pctx = p2;)
+		} while (HAS_KEYWORDS && pctx);
+
+		o_free(&ctx.word);
+#if !BB_MMU
+		if (pstring)
+			*pstring = NULL;
+#endif
+		debug_leave();
+		return ERR_PTR;
+	}
+}
+
+
+/*** Execution routines ***/
+
+/* Expansion can recurse, need forward decls: */
+#if !BASH_PATTERN_SUBST && !ENABLE_HUSH_CASE
+#define expand_string_to_string(str, EXP_flags, do_unbackslash) \
+	expand_string_to_string(str)
+#endif
+static char *expand_string_to_string(const char *str, int EXP_flags, int do_unbackslash);
+#if ENABLE_HUSH_TICK
+static int process_command_subs(o_string *dest, const char *s);
+#endif
+static int expand_vars_to_list(o_string *output, int n, char *arg);
+
+/* 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).
+ * This new list is allocated as a single malloc block.
+ * NULL-terminated list of char* pointers is at the beginning of it,
+ * followed by strings themselves.
+ * Caller can deallocate entire list by single free(list). */
+
+/* A horde of its helpers come first: */
+
+static void o_addblock_duplicate_backslash(o_string *o, const char *str, int len)
+{
+	while (--len >= 0) {
+		char c = *str++;
+
+#if ENABLE_HUSH_BRACE_EXPANSION
+		if (c == '{' || c == '}') {
+			/* { -> \{, } -> \} */
+			o_addchr(o, '\\');
+			/* And now we want to add { or } and continue:
+			 *  o_addchr(o, c);
+			 *  continue;
+			 * luckily, just falling through achieves this.
+			 */
+		}
+#endif
+		o_addchr(o, c);
+		if (c == '\\') {
+			/* \z -> \\\z; \<eol> -> \\<eol> */
+			o_addchr(o, '\\');
+			if (len) {
+				len--;
+				o_addchr(o, '\\');
+				o_addchr(o, *str++);
+			}
+		}
+	}
+}
+
+/* Store given string, finalizing the word and starting new one whenever
+ * we encounter IFS char(s). This is used for expanding variable values.
+ * End-of-string does NOT finalize word: think about 'echo -$VAR-'.
+ * Return in output->ended_in_ifs:
+ * 1 - ended with IFS char, else 0 (this includes case of empty str).
+ */
+static int expand_on_ifs(o_string *output, int n, const char *str)
+{
+	int last_is_ifs = 0;
+
+	while (1) {
+		int word_len;
+
+		if (!*str)  /* EOL - do not finalize word */
+			break;
+		word_len = strcspn(str, G.ifs);
+		if (word_len) {
+			/* We have WORD_LEN leading non-IFS chars */
+			if (!(output->o_expflags & EXP_FLAG_GLOB)) {
+				o_addblock(output, str, word_len);
+			} else {
+				/* Protect backslashes against globbing up :)
+				 * Example: "v='\*'; echo b$v" prints "b\*"
+				 * (and does not try to glob on "*")
+				 */
+				o_addblock_duplicate_backslash(output, str, word_len);
+				/*/ Why can't we do it easier? */
+				/*o_addblock(output, str, word_len); - WRONG: "v='\*'; echo Z$v" prints "Z*" instead of "Z\*" */
+				/*o_addqblock(output, str, word_len); - WRONG: "v='*'; echo Z$v" prints "Z*" instead of Z* files */
+			}
+			last_is_ifs = 0;
+			str += word_len;
+			if (!*str)  /* EOL - do not finalize word */
+				break;
+		}
+
+		/* We know str here points to at least one IFS char */
+		last_is_ifs = 1;
+		str += strspn(str, G.ifs_whitespace); /* skip IFS whitespace chars */
+		if (!*str)  /* EOL - do not finalize word */
+			break;
+
+		if (G.ifs_whitespace != G.ifs /* usually false ($IFS is usually all whitespace), */
+		 && strchr(G.ifs, *str)       /* the second check would fail */
+		) {
+			/* This is a non-whitespace $IFS char */
+			/* Skip it and IFS whitespace chars, start new word */
+			str++;
+			str += strspn(str, G.ifs_whitespace);
+			goto new_word;
+		}
+
+		/* Start new word... but not always! */
+		/* Case "v=' a'; echo ''$v": we do need to finalize empty word: */
+		if (output->has_quoted_part
+		/*
+		 * Case "v=' a'; echo $v":
+		 * here nothing precedes the space in $v expansion,
+		 * therefore we should not finish the word
+		 * (IOW: if there *is* word to finalize, only then do it):
+		 * It's okay if this accesses the byte before first argv[]:
+		 * past call to o_save_ptr() cleared it to zero byte
+		 * (grep for -prev-ifs-check-).
+		 */
+		 || output->data[output->length - 1]
+		) {
+ new_word:
+			o_addchr(output, '\0');
+			debug_print_list("expand_on_ifs", output, n);
+			n = o_save_ptr(output, n);
+		}
+	}
+
+	output->ended_in_ifs = last_is_ifs;
+	debug_print_list("expand_on_ifs[1]", output, n);
+	return n;
+}
+
+/* Helper to expand $((...)) and heredoc body. These act as if
+ * they are in double quotes, with the exception that they are not :).
+ * Just the rules are similar: "expand only $var and `cmd`"
+ *
+ * Returns malloced string.
+ * As an optimization, we return NULL if expansion is not needed.
+ */
+static char *encode_then_expand_string(const char *str)
+{
+	char *exp_str;
+	struct in_str input;
+	o_string dest = NULL_O_STRING;
+	const char *cp;
+
+	cp = str;
+	for (;;) {
+		if (!*cp) return NULL; /* string has no special chars */
+		if (*cp == '$') break;
+		if (*cp == '\\') break;
+#if ENABLE_HUSH_TICK
+		if (*cp == '`') break;
+#endif
+		cp++;
+	}
+
+	/* We need to expand. Example:
+	 * echo $(($a + `echo 1`)) $((1 + $((2)) ))
+	 */
+	setup_string_in_str(&input, str);
+	encode_string(NULL, &dest, &input, EOF);
+//TODO: error check (encode_string returns 0 on error)?
+	//bb_error_msg("'%s' -> '%s'", str, dest.data);
+	exp_str = expand_string_to_string(dest.data,
+			EXP_FLAG_ESC_GLOB_CHARS,
+			/*unbackslash:*/ 1
+	);
+	//bb_error_msg("'%s' -> '%s'", dest.data, exp_str);
+	o_free(&dest);
+	return exp_str;
+}
+
+static const char *first_special_char_in_vararg(const char *cp)
+{
+	for (;;) {
+		if (!*cp) return NULL; /* string has no special chars */
+		if (*cp == '$') return cp;
+		if (*cp == '\\') return cp;
+		if (*cp == '\'') return cp;
+		if (*cp == '"') return cp;
+#if ENABLE_HUSH_TICK
+		if (*cp == '`') return cp;
+#endif
+		/* dquoted "${x:+ARG}" should not glob, therefore
+		 * '*' et al require some non-literal processing: */
+		if (*cp == '*') return cp;
+		if (*cp == '?') return cp;
+		if (*cp == '[') return cp;
+		cp++;
+	}
+}
+
+/* Expanding ARG in ${var#ARG}, ${var%ARG}, or ${var/ARG/ARG}.
+ * These can contain single- and double-quoted strings,
+ * and treated as if the ARG string is initially unquoted. IOW:
+ * ${var#ARG} and "${var#ARG}" treat ARG the same (ARG can even be
+ * a dquoted string: "${var#"zz"}"), the difference only comes later
+ * (word splitting and globbing of the ${var...} result).
+ */
+#if !BASH_PATTERN_SUBST
+#define encode_then_expand_vararg(str, handle_squotes, do_unbackslash) \
+	encode_then_expand_vararg(str, handle_squotes)
+#endif
+static char *encode_then_expand_vararg(const char *str, int handle_squotes, int do_unbackslash)
+{
+#if !BASH_PATTERN_SUBST && ENABLE_HUSH_CASE
+	const int do_unbackslash = 0;
+#endif
+	char *exp_str;
+	struct in_str input;
+	o_string dest = NULL_O_STRING;
+
+	if (!first_special_char_in_vararg(str)) {
+		/* string has no special chars */
+		return NULL;
+	}
+
+	setup_string_in_str(&input, str);
+	dest.data = xzalloc(1); /* start as "", not as NULL */
+	exp_str = NULL;
+
+	for (;;) {
+		int ch;
+
+		ch = i_getch(&input);
+		debug_printf_parse("%s: ch=%c (%d) escape=%d\n",
+				__func__, ch, ch, !!dest.o_expflags);
+
+		if (!dest.o_expflags) {
+			if (ch == EOF)
+				break;
+			if (handle_squotes && ch == '\'') {
+				if (!add_till_single_quote_dquoted(&dest, &input))
+					goto ret; /* error */
+				continue;
+			}
+		}
+		if (ch == EOF) {
+			syntax_error_unterm_ch('"');
+			goto ret; /* error */
+		}
+		if (ch == '"') {
+			dest.o_expflags ^= EXP_FLAG_ESC_GLOB_CHARS;
+			continue;
+		}
+		if (ch == '\\') {
+			ch = i_getch(&input);
+			if (ch == EOF) {
+//example? error message?	syntax_error_unterm_ch('"');
+				debug_printf_parse("%s: error: \\<eof>\n", __func__);
+				goto ret;
+			}
+			o_addqchr(&dest, ch);
+			continue;
+		}
+		if (ch == '$') {
+			if (parse_dollar_squote(NULL, &dest, &input))
+				continue;
+			if (!parse_dollar(NULL, &dest, &input, /*quote_mask:*/ 0x80)) {
+				debug_printf_parse("%s: error: parse_dollar returned 0 (error)\n", __func__);
+				goto ret;
+			}
+			continue;
+		}
+#if ENABLE_HUSH_TICK
+		if (ch == '`') {
+			//unsigned pos = dest->length;
+			o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+			o_addchr(&dest, 0x80 | '`');
+			if (!add_till_backquote(&dest, &input,
+					/*in_dquote:*/ dest.o_expflags /* nonzero if EXP_FLAG_ESC_GLOB_CHARS set */
+				)
+			) {
+				goto ret; /* error */
+			}
+			o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+			//debug_printf_subst("SUBST RES3 '%s'\n", dest->data + pos);
+			continue;
+		}
+#endif
+		o_addQchr(&dest, ch);
+	} /* for (;;) */
+
+	debug_printf_parse("encode: '%s' -> '%s'\n", str, dest.data);
+	exp_str = expand_string_to_string(dest.data,
+			do_unbackslash ? EXP_FLAG_ESC_GLOB_CHARS : 0,
+			do_unbackslash
+	);
+ ret:
+	debug_printf_parse("expand: '%s' -> '%s'\n", dest.data, exp_str);
+	o_free(&dest);
+	return exp_str;
+}
+
+/* Expanding ARG in ${var+ARG}, ${var-ARG}
+ */
+static int encode_then_append_var_plusminus(o_string *output, int n,
+		char *str, int dquoted)
+{
+	struct in_str input;
+	o_string dest = NULL_O_STRING;
+
+	if (!first_special_char_in_vararg(str)
+	 && '\0' == str[strcspn(str, G.ifs)]
+	) {
+		/* string has no special chars
+		 * && string has no $IFS chars
+		 */
+		if (dquoted) {
+			/* Prints 1 (quoted expansion is a "" word, not nothing):
+			 * set -- "${notexist-}"; echo $#
+			 */
+			output->has_quoted_part = 1;
+		}
+		return expand_vars_to_list(output, n, str);
+	}
+
+	setup_string_in_str(&input, str);
+
+	for (;;) {
+		int ch;
+
+		ch = i_getch(&input);
+		debug_printf_parse("%s: ch=%c (%d) escape=%x\n",
+				__func__, ch, ch, dest.o_expflags);
+
+		if (!dest.o_expflags) {
+			if (ch == EOF)
+				break;
+			if (!dquoted && strchr(G.ifs, ch)) {
+				/* PREFIX${x:d${e}f ...} and we met space: expand "d${e}f" and start new word.
+				 * do not assume we are at the start of the word (PREFIX above).
+				 */
+				if (dest.data) {
+					n = expand_vars_to_list(output, n, dest.data);
+					o_free_and_set_NULL(&dest);
+					o_addchr(output, '\0');
+					n = o_save_ptr(output, n); /* create next word */
+				} else
+				if (output->length != o_get_last_ptr(output, n)
+				 || output->has_quoted_part
+				) {
+					/* For these cases:
+					 * f() { for i; do echo "|$i|"; done; }; x=x
+					 * f a${x:+ }b  # 1st condition
+					 * |a|
+					 * |b|
+					 * f ""${x:+ }b  # 2nd condition
+					 * ||
+					 * |b|
+					 */
+					o_addchr(output, '\0');
+					n = o_save_ptr(output, n); /* create next word */
+				}
+				continue;
+			}
+			if (!dquoted && ch == '\'') {
+				if (!add_till_single_quote_dquoted(&dest, &input))
+					goto ret; /* error */
+				o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+				o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+				continue;
+			}
+		}
+		if (ch == EOF) {
+			syntax_error_unterm_ch('"');
+			goto ret; /* error */
+		}
+		if (ch == '"') {
+			dest.o_expflags ^= EXP_FLAG_ESC_GLOB_CHARS;
+			if (dest.o_expflags) {
+				o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+				o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+			}
+			continue;
+		}
+		if (ch == '\\') {
+			ch = i_getch(&input);
+			if (ch == EOF) {
+//example? error message?	syntax_error_unterm_ch('"');
+				debug_printf_parse("%s: error: \\<eof>\n", __func__);
+				goto ret;
+			}
+			o_addqchr(&dest, ch);
+			continue;
+		}
+		if (ch == '$') {
+			if (!parse_dollar(NULL, &dest, &input, /*quote_mask:*/ (dest.o_expflags || dquoted) ? 0x80 : 0)) {
+				debug_printf_parse("%s: error: parse_dollar returned 0 (error)\n", __func__);
+				goto ret;
+			}
+			continue;
+		}
+#if ENABLE_HUSH_TICK
+		if (ch == '`') {
+			//unsigned pos = dest->length;
+			o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+			o_addchr(&dest, (dest.o_expflags || dquoted) ? 0x80 | '`' : '`');
+			if (!add_till_backquote(&dest, &input,
+					/*in_dquote:*/ dest.o_expflags /* nonzero if EXP_FLAG_ESC_GLOB_CHARS set */
+				)
+			) {
+				goto ret; /* error */
+			}
+			o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+			//debug_printf_subst("SUBST RES3 '%s'\n", dest->data + pos);
+			continue;
+		}
+#endif
+		if (dquoted) {
+			/* Always glob-protect if in dquotes:
+			 * x=x; echo "${x:+/bin/c*}" - prints: /bin/c*
+			 * x=x; echo "${x:+"/bin/c*"}" - prints: /bin/c*
+			 */
+			o_addqchr(&dest, ch);
+		} else {
+			/* Glob-protect only if char is quoted:
+			 * x=x; echo ${x:+/bin/c*} - prints many filenames
+			 * x=x; echo ${x:+"/bin/c*"} - prints: /bin/c*
+			 */
+			o_addQchr(&dest, ch);
+		}
+	} /* for (;;) */
+
+	if (dest.data) {
+		n = expand_vars_to_list(output, n, dest.data);
+	}
+ ret:
+	o_free(&dest);
+	return n;
+}
+
+#if ENABLE_FEATURE_SH_MATH
+static arith_t expand_and_evaluate_arith(const char *arg, const char **errmsg_p)
+{
+	arith_state_t math_state;
+	arith_t res;
+	char *exp_str;
+
+	math_state.lookupvar = get_local_var_value;
+	math_state.setvar = set_local_var_from_halves;
+	//math_state.endofname = endofname;
+	exp_str = encode_then_expand_string(arg);
+	res = arith(&math_state, exp_str ? exp_str : arg);
+	free(exp_str);
+	if (errmsg_p)
+		*errmsg_p = math_state.errmsg;
+	if (math_state.errmsg)
+		msg_and_die_if_script(math_state.errmsg);
+	return res;
+}
+#endif
+
+#if BASH_PATTERN_SUBST
+/* ${var/[/]pattern[/repl]} helpers */
+static char *strstr_pattern(char *val, const char *pattern, int *size)
+{
+	int sz = strcspn(pattern, "*?[\\");
+	if (pattern[sz] == '\0') {
+		/* Optimization for trivial patterns.
+		 * Testcase for very slow replace (performs about 22k replaces):
+		 * x=::::::::::::::::::::::
+		 * x=$x$x;x=$x$x;x=$x$x;x=$x$x;x=$x$x;x=$x$x;x=$x$x;x=$x$x;x=$x$x;x=$x$x;echo ${#x}
+		 * echo "${x//:/|}"
+		 */
+		*size = sz;
+		return strstr(val, pattern);
+	}
+
+	while (1) {
+		char *end = scan_and_match(val, pattern, SCAN_MOVE_FROM_RIGHT + SCAN_MATCH_LEFT_HALF);
+		debug_printf_varexp("val:'%s' pattern:'%s' end:'%s'\n", val, pattern, end);
+		if (end) {
+			*size = end - val;
+			return val;
+		}
+		if (*val == '\0')
+			return NULL;
+		/* Optimization: if "*pat" did not match the start of "string",
+		 * we know that "tring", "ring" etc will not match too:
+		 */
+		if (pattern[0] == '*')
+			return NULL;
+		val++;
+	}
+}
+static char *replace_pattern(char *val, const char *pattern, const char *repl, char exp_op)
+{
+	char *result = NULL;
+	unsigned res_len = 0;
+	unsigned repl_len = strlen(repl);
+
+	/* Null pattern never matches, including if "var" is empty */
+	if (!pattern[0])
+		return result; /* NULL, no replaces happened */
+
+	while (1) {
+		int size;
+		char *s = strstr_pattern(val, pattern, &size);
+		if (!s)
+			break;
+
+		result = xrealloc(result, res_len + (s - val) + repl_len + 1);
+		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 && result) {
+		result = xrealloc(result, res_len + strlen(val) + 1);
+		strcpy(result + res_len, val);
+		debug_printf_varexp("val:'%s' result:'%s'\n", val, result);
+	}
+	debug_printf_varexp("result:'%s'\n", result);
+	return result;
+}
+#endif /* BASH_PATTERN_SUBST */
+
+static int append_str_maybe_ifs_split(o_string *output, int n,
+		int first_ch, const char *val)
+{
+	if (!(first_ch & 0x80)) { /* unquoted $VAR */
+		debug_printf_expand("unquoted '%s', output->o_escape:%d\n", val,
+				!!(output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+		if (val && val[0])
+			n = expand_on_ifs(output, n, val);
+	} else { /* quoted "$VAR" */
+		output->has_quoted_part = 1;
+		debug_printf_expand("quoted '%s', output->o_escape:%d\n", val,
+				!!(output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+		if (val && val[0])
+			o_addQstr(output, val);
+	}
+	return n;
+}
+
+/* Handle <SPECIAL_VAR_SYMBOL>varname...<SPECIAL_VAR_SYMBOL> construct.
+ */
+static NOINLINE int expand_one_var(o_string *output, int n,
+		int first_ch, char *arg, char **pp)
+{
+	const char *val;
+	char *to_be_freed;
+	char *p;
+	char *var;
+	char exp_op;
+	char exp_save = exp_save; /* for compiler */
+	char *exp_saveptr; /* points to expansion operator */
+	char *exp_word = exp_word; /* for compiler */
+	char arg0;
+
+	val = NULL;
+	to_be_freed = NULL;
+	p = *pp;
+	*p = '\0'; /* replace trailing SPECIAL_VAR_SYMBOL */
+	var = arg;
+	exp_saveptr = arg[1] ? strchr(VAR_ENCODED_SUBST_OPS, arg[1]) : NULL;
+	arg0 = arg[0];
+	arg[0] = (arg0 & 0x7f);
+	exp_op = 0;
+
+	if (arg[0] == '#' && arg[1] /* ${#...} but not ${#} */
+	 && (!exp_saveptr               /* and ( not(${#<op_char>...}) */
+	    || (arg[2] == '\0' && strchr(SPECIAL_VARS_STR, arg[1])) /* or ${#C} "len of $C" ) */
+	    )		/* NB: skipping ^^^specvar check mishandles ${#::2} */
+	) {
+		/* It must be length operator: ${#var} */
+		var++;
+		exp_op = 'L';
+	} else {
+		/* Maybe handle parameter expansion */
+		if (exp_saveptr /* if 2nd char is one of expansion operators */
+		 && strchr(NUMERIC_SPECVARS_STR, arg[0]) /* 1st char is special variable */
+		) {
+			/* ${?:0}, ${#[:]%0} etc */
+			exp_saveptr = var + 1;
+		} else {
+			/* ${?}, ${var}, ${var:0}, ${var[:]%0} etc */
+			exp_saveptr = var+1 + strcspn(var+1, VAR_ENCODED_SUBST_OPS);
+		}
+		exp_op = exp_save = *exp_saveptr;
+		if (exp_op) {
+			exp_word = exp_saveptr + 1;
+			if (exp_op == ':') {
+				exp_op = *exp_word++;
+//TODO: try ${var:} and ${var:bogus} in non-bash config
+				if (BASH_SUBSTR
+				 && (!exp_op || !strchr(MINUS_PLUS_EQUAL_QUESTION, exp_op))
+				) {
+					/* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
+					exp_op = ':';
+					exp_word--;
+				}
+			}
+			*exp_saveptr = '\0';
+		} /* else: it's not an expansion op, but bare ${var} */
+	}
+
+	/* Look up the variable in question */
+	if (isdigit(var[0])) {
+		/* parse_dollar should have vetted var for us */
+		int nn = xatoi_positive(var);
+		if (nn < G.global_argc)
+			val = G.global_argv[nn];
+		/* else val remains NULL: $N with too big N */
+	} else {
+		switch (var[0]) {
+		case '$': /* pid */
+			val = utoa(G.root_pid);
+			break;
+		case '!': /* bg pid */
+			val = G.last_bg_pid ? utoa(G.last_bg_pid) : "";
+			break;
+		case '?': /* exitcode */
+			val = utoa(G.last_exitcode);
+			break;
+		case '#': /* argc */
+			val = utoa(G.global_argc ? G.global_argc-1 : 0);
+			break;
+		case '-': { /* active options */
+			/* Check set_mode() to see what option chars we support */
+			char *cp;
+			val = cp = G.optstring_buf;
+			if (G.o_opt[OPT_O_ERREXIT])
+				*cp++ = 'e';
+			if (G_interactive_fd)
+				*cp++ = 'i';
+			if (G_x_mode)
+				*cp++ = 'x';
+			/* If G.o_opt[OPT_O_NOEXEC] is true,
+			 * commands read but are not executed,
+			 * so $- can not execute too, 'n' is never seen in $-.
+			 */
+			if (G.opt_c)
+				*cp++ = 'c';
+			if (G.opt_s)
+				*cp++ = 's';
+			*cp = '\0';
+			break;
+		}
+		default:
+			val = get_local_var_value(var);
+		}
+	}
+
+	/* Handle any expansions */
+	if (exp_op == 'L') {
+		reinit_unicode_for_hush();
+		debug_printf_expand("expand: length(%s)=", val);
+		val = utoa(val ? unicode_strlen(val) : 0);
+		debug_printf_expand("%s\n", val);
+	} else if (exp_op) {
+		if (exp_op == '%' || exp_op == '#') {
+			/* Standard-mandated substring removal ops:
+			 * ${parameter%word} - remove smallest suffix pattern
+			 * ${parameter%%word} - remove largest suffix pattern
+			 * ${parameter#word} - remove smallest prefix pattern
+			 * ${parameter##word} - remove largest prefix pattern
+			 *
+			 * Word is expanded to produce a glob pattern.
+			 * Then var's value is matched to it and matching part removed.
+			 */
+			/* bash compat: if x is "" and no shrinking of it is possible,
+			 * inner ${...} is not evaluated. Example:
+			 *  unset b; : ${a%${b=B}}; echo $b
+			 * assignment b=B only happens if $a is not "".
+			 */
+			if (val && val[0]) {
+				char *t;
+				char *exp_exp_word;
+				char *loc;
+				unsigned scan_flags = pick_scan(exp_op, *exp_word);
+				if (exp_op == *exp_word)  /* ## or %% */
+					exp_word++;
+				debug_printf_expand("expand: exp_word:'%s'\n", exp_word);
+				exp_exp_word = encode_then_expand_vararg(exp_word, /*handle_squotes:*/ 1, /*unbackslash:*/ 0);
+				if (exp_exp_word)
+					exp_word = exp_exp_word;
+				debug_printf_expand("expand: exp_word:'%s'\n", exp_word);
+				/*
+				 * HACK ALERT. We depend here on the fact that
+				 * G.global_argv and results of utoa and get_local_var_value
+				 * are actually in writable memory:
+				 * scan_and_match momentarily stores NULs there.
+				 */
+				t = (char*)val;
+				loc = scan_and_match(t, exp_word, scan_flags);
+				debug_printf_expand("op:%c str:'%s' pat:'%s' res:'%s'\n", exp_op, t, exp_word, loc);
+				free(exp_exp_word);
+				if (loc) { /* match was found */
+					if (scan_flags & SCAN_MATCH_LEFT_HALF) /* #[#] */
+						val = loc; /* take right part */
+					else /* %[%] */
+						val = to_be_freed = xstrndup(val, loc - val); /* left */
+				}
+			}
+		}
+#if BASH_PATTERN_SUBST
+		else if (exp_op == '/' || exp_op == '\\') {
+			/* It's ${var/[/]pattern[/repl]} thing.
+			 * Note that in encoded form it has TWO parts:
+			 * var/pattern<SPECIAL_VAR_SYMBOL>repl<SPECIAL_VAR_SYMBOL>
+			 * and if // is used, it is encoded as \:
+			 * var\pattern<SPECIAL_VAR_SYMBOL>repl<SPECIAL_VAR_SYMBOL>
+			 */
+			/* bash compat: if var is "", both pattern and repl
+			 * are still evaluated, if it is unset, then not:
+			 * unset b; a=; : ${a/z/${b=3}}; echo $b      # b=3
+			 * unset b; unset a; : ${a/z/${b=3}}; echo $b # b not set
+			 */
+			if (val /*&& val[0]*/) {
+				/* pattern uses non-standard expansion.
+				 * repl should be unbackslashed and globbed
+				 * by the usual expansion rules:
+				 *  >az >bz
+				 *  v='a bz'; echo "${v/a*z/a*z}" #prints "a*z"
+				 *  v='a bz'; echo "${v/a*z/\z}"  #prints "z"
+				 *  v='a bz'; echo ${v/a*z/a*z}   #prints "az"
+				 *  v='a bz'; echo ${v/a*z/\z}    #prints "z"
+				 * (note that a*z _pattern_ is never globbed!)
+				 */
+				char *pattern, *repl, *t;
+				pattern = encode_then_expand_vararg(exp_word, /*handle_squotes:*/ 1, /*unbackslash:*/ 0);
+				if (!pattern)
+					pattern = xstrdup(exp_word);
+				debug_printf_varexp("pattern:'%s'->'%s'\n", exp_word, pattern);
+				*p++ = SPECIAL_VAR_SYMBOL;
+				exp_word = p;
+				p = strchr(p, SPECIAL_VAR_SYMBOL);
+				*p = '\0';
+				repl = encode_then_expand_vararg(exp_word, /*handle_squotes:*/ 1, /*unbackslash:*/ 1);
+				debug_printf_varexp("repl:'%s'->'%s'\n", exp_word, repl);
+				/* HACK ALERT. We depend here on the fact that
+				 * G.global_argv and results of utoa and get_local_var_value
+				 * are actually in writable memory:
+				 * replace_pattern momentarily stores NULs there. */
+				t = (char*)val;
+				to_be_freed = replace_pattern(t,
+						pattern,
+						(repl ? repl : exp_word),
+						exp_op);
+				if (to_be_freed) /* at least one replace happened */
+					val = to_be_freed;
+				free(pattern);
+				free(repl);
+			} else {
+				/* Unset variable always gives nothing */
+				//  a=; echo ${a/*/w}      # "w"
+				//  unset a; echo ${a/*/w} # ""
+				/* Just skip "replace" part */
+				*p++ = SPECIAL_VAR_SYMBOL;
+				p = strchr(p, SPECIAL_VAR_SYMBOL);
+				*p = '\0';
+			}
+		}
+#endif /* BASH_PATTERN_SUBST */
+		else if (exp_op == ':') {
+#if BASH_SUBSTR && ENABLE_FEATURE_SH_MATH
+			/* It's ${var:N[:M]} bashism.
+			 * Note that in encoded form it has TWO parts:
+			 * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
+			 */
+			arith_t beg, len;
+			unsigned vallen;
+			const char *errmsg;
+
+			beg = expand_and_evaluate_arith(exp_word, &errmsg);
+			if (errmsg)
+				goto empty_result;
+			debug_printf_varexp("beg:'%s'=%lld\n", exp_word, (long long)beg);
+			*p++ = SPECIAL_VAR_SYMBOL;
+			exp_word = p;
+			p = strchr(p, SPECIAL_VAR_SYMBOL);
+			*p = '\0';
+			vallen = val ? strlen(val) : 0;
+			if (beg < 0) {
+				/* negative beg counts from the end */
+				beg = (arith_t)vallen + beg;
+			}
+			/* If expansion will be empty, do not even evaluate len */
+			if (!val || beg < 0 || beg > vallen) {
+				/* Why > vallen, not >=? bash:
+				 * unset b; a=ab; : ${a:2:${b=3}}; echo $b  # "", b=3 (!!!)
+				 * unset b; a=a; : ${a:2:${b=3}}; echo $b   # "", b not set
+				 */
+				goto empty_result;
+			}
+			len = expand_and_evaluate_arith(exp_word, &errmsg);
+			if (errmsg)
+				goto empty_result;
+			debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
+			debug_printf_varexp("from val:'%s'\n", val);
+			if (len < 0) {
+				/* in bash, len=-n means strlen()-n */
+				len = (arith_t)vallen - beg + len;
+				if (len < 0) /* bash compat */
+					msg_and_die_if_script("%s: substring expression < 0", var);
+			}
+			if (len <= 0 || !val /*|| beg >= vallen*/) {
+ empty_result:
+				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) */
+			msg_and_die_if_script("malformed ${%s:...}", var);
+			val = NULL;
+#endif
+		} else { /* one of "-=+?" */
+			/* Standard-mandated substitution ops:
+			 * ${var?word} - indicate error if unset
+			 *      If var is unset, word (or a message indicating it is unset
+			 *      if word is null) is written to standard error
+			 *      and the shell exits with a non-zero exit status.
+			 *      Otherwise, the value of var is substituted.
+			 * ${var-word} - use default value
+			 *      If var is unset, word is substituted.
+			 * ${var=word} - assign and use default value
+			 *      If var is unset, word is assigned to var.
+			 *      In all cases, final value of var is substituted.
+			 * ${var+word} - use alternative value
+			 *      If var is unset, null is substituted.
+			 *      Otherwise, word is substituted.
+			 *
+			 * Word is subjected to tilde expansion, parameter expansion,
+			 * command substitution, and arithmetic expansion.
+			 * If word is not needed, it is not expanded.
+			 *
+			 * Colon forms (${var:-word}, ${var:=word} etc) do the same,
+			 * but also treat null var as if it is unset.
+			 *
+			 * Word-splitting and single quote behavior:
+			 *
+			 * $ f() { for i; do echo "|$i|"; done; }
+			 *
+			 * $ x=; f ${x:?'x y' z}; echo $?
+			 * bash: x: x y z       # neither f nor "echo $?" executes
+			 * (if interactive, bash does not exit, but merely aborts to prompt. $? is set to 1)
+			 * $ x=; f "${x:?'x y' z}"
+			 * bash: x: x y z       # dash prints: dash: x: 'x y' z
+			 *
+			 * $ x=; f ${x:='x y' z}
+			 * |x|
+			 * |y|
+			 * |z|
+			 * $ x=; f "${x:='x y' z}"
+			 * |'x y' z|
+			 *
+			 * $ x=x; f ${x:+'x y' z}
+			 * |x y|
+			 * |z|
+			 * $ x=x; f "${x:+'x y' z}"
+			 * |'x y' z|
+			 *
+			 * $ x=; f ${x:-'x y' z}
+			 * |x y|
+			 * |z|
+			 * $ x=; f "${x:-'x y' z}"
+			 * |'x y' z|
+			 */
+			int use_word = (!val || ((exp_save == ':') && !val[0]));
+			if (exp_op == '+')
+				use_word = !use_word;
+			debug_printf_expand("expand: op:%c (null:%s) test:%i\n", exp_op,
+					(exp_save == ':') ? "true" : "false", use_word);
+			if (use_word) {
+				if (exp_op == '+' || exp_op == '-') {
+					/* ${var+word} - use alternative value */
+					/* ${var-word} - use default value */
+					n = encode_then_append_var_plusminus(output, n, exp_word,
+							/*dquoted:*/ (arg0 & 0x80)
+					);
+					val = NULL;
+				} else {
+					/* ${var?word} - indicate error if unset */
+					/* ${var=word} - assign and use default value */
+					to_be_freed = encode_then_expand_vararg(exp_word,
+							/*handle_squotes:*/ !(arg0 & 0x80),
+							/*unbackslash:*/ 0
+					);
+					if (to_be_freed)
+						exp_word = to_be_freed;
+					if (exp_op == '?') {
+						/* mimic bash message */
+						msg_and_die_if_script("%s: %s",
+							var,
+							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?
+//It aborts the entire line, returns to prompt:
+// $ f() { for i; do echo "|$i|"; done; }; x=; f "${x:?'x y' z}"; echo YO
+// bash: x: x y z
+// $
+// ("echo YO" is not executed, neither the f function call)
+					} else {
+						val = exp_word;
+					}
+					if (exp_op == '=') {
+						/* ${var=[word]} or ${var:=[word]} */
+						if (isdigit(var[0]) || var[0] == '#') {
+							/* mimic bash message */
+							msg_and_die_if_script("$%s: cannot assign in this way", var);
+							val = NULL;
+						} else {
+							char *new_var = xasprintf("%s=%s", var, val);
+							set_local_var(new_var, /*flag:*/ 0);
+						}
+					}
+				}
+			}
+		} /* one of "-=+?" */
+
+		*exp_saveptr = exp_save;
+	} /* if (exp_op) */
+
+	arg[0] = arg0;
+	*pp = p;
+
+	n = append_str_maybe_ifs_split(output, n, first_ch, val);
+
+	free(to_be_freed);
+	return n;
+}
+
+/* Expand all variable references in given string, adding words to list[]
+ * at n, n+1,... positions. Return updated n (so that list[n] is next one
+ * to be filled). This routine is extremely tricky: has to deal with
+ * variables/parameters with whitespace, $* and $@, and constructs like
+ * 'echo -$*-'. If you play here, you must run testsuite afterwards! */
+static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
+{
+	/* output->o_expflags & EXP_FLAG_SINGLEWORD (0x80) if we are in
+	 * expansion of right-hand side of assignment == 1-element expand.
+	 */
+	char cant_be_null = 0; /* only bit 0x80 matters */
+	char *p;
+
+	debug_printf_expand("expand_vars_to_list: arg:'%s' singleword:%x\n", arg,
+			!!(output->o_expflags & EXP_FLAG_SINGLEWORD));
+	debug_print_list("expand_vars_to_list[0]", output, n);
+
+	while ((p = strchr(arg, SPECIAL_VAR_SYMBOL)) != NULL) {
+		char first_ch;
+#if ENABLE_FEATURE_SH_MATH
+		char arith_buf[sizeof(arith_t)*3 + 2];
+#endif
+
+		if (output->ended_in_ifs) {
+			o_addchr(output, '\0');
+			n = o_save_ptr(output, n);
+			output->ended_in_ifs = 0;
+		}
+
+		o_addblock(output, arg, p - arg);
+		debug_print_list("expand_vars_to_list[1]", output, n);
+		arg = ++p;
+		p = strchr(p, SPECIAL_VAR_SYMBOL);
+
+		/* Fetch special var name (if it is indeed one of them)
+		 * and quote bit, force the bit on if singleword expansion -
+		 * important for not getting v=$@ expand to many words. */
+		first_ch = arg[0] | (output->o_expflags & EXP_FLAG_SINGLEWORD);
+
+		/* Is this variable quoted and thus expansion can't be null?
+		 * "$@" is special. Even if quoted, it can still
+		 * expand to nothing (not even an empty string),
+		 * thus it is excluded. */
+		if ((first_ch & 0x7f) != '@')
+			cant_be_null |= first_ch;
+
+		switch (first_ch & 0x7f) {
+		/* Highest bit in first_ch indicates that var is double-quoted */
+		case '*':
+		case '@': {
+			int i;
+			if (!G.global_argv[1])
+				break;
+			i = 1;
+			cant_be_null |= first_ch; /* do it for "$@" _now_, when we know it's not empty */
+			if (!(first_ch & 0x80)) { /* unquoted $* or $@ */
+				while (G.global_argv[i]) {
+					n = expand_on_ifs(output, n, G.global_argv[i]);
+					debug_printf_expand("expand_vars_to_list: argv %d (last %d)\n", i, G.global_argc - 1);
+					if (G.global_argv[i++][0] && G.global_argv[i]) {
+						/* this argv[] is not empty and not last:
+						 * put terminating NUL, start new word */
+						o_addchr(output, '\0');
+						debug_print_list("expand_vars_to_list[2]", output, n);
+						n = o_save_ptr(output, n);
+						debug_print_list("expand_vars_to_list[3]", output, n);
+					}
+				}
+			} else
+			/* If EXP_FLAG_SINGLEWORD, we handle assignment 'a=....$@.....'
+			 * and in this case should treat it like '$*' - see 'else...' below */
+			if (first_ch == (char)('@'|0x80)  /* quoted $@ */
+			 && !(output->o_expflags & EXP_FLAG_SINGLEWORD) /* not v="$@" case */
+			) {
+				while (1) {
+					o_addQstr(output, G.global_argv[i]);
+					if (++i >= G.global_argc)
+						break;
+					o_addchr(output, '\0');
+					debug_print_list("expand_vars_to_list[4]", output, n);
+					n = o_save_ptr(output, n);
+				}
+			} else { /* quoted $* (or v="$@" case): add as one word */
+				while (1) {
+					o_addQstr(output, G.global_argv[i]);
+					if (!G.global_argv[++i])
+						break;
+					if (G.ifs[0])
+						o_addchr(output, G.ifs[0]);
+				}
+				output->has_quoted_part = 1;
+			}
+			break;
+		}
+		case SPECIAL_VAR_SYMBOL: {
+			/* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_SYMBOL> */
+			/* "Empty variable", used to make "" etc to not disappear */
+			output->has_quoted_part = 1;
+			cant_be_null = 0x80;
+			arg++;
+			break;
+		}
+		case SPECIAL_VAR_QUOTED_SVS:
+			/* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_QUOTED_SVS><SPECIAL_VAR_SYMBOL> */
+			/* "^C variable", represents literal ^C char (possible in scripts) */
+			o_addchr(output, SPECIAL_VAR_SYMBOL);
+			arg++;
+			break;
+#if ENABLE_HUSH_TICK
+		case '`': {
+			/* <SPECIAL_VAR_SYMBOL>`cmd<SPECIAL_VAR_SYMBOL> */
+			o_string subst_result = NULL_O_STRING;
+
+			*p = '\0'; /* replace trailing <SPECIAL_VAR_SYMBOL> */
+			arg++;
+			/* Can't just stuff it into output o_string,
+			 * expanded result may need to be globbed
+			 * and $IFS-split */
+			debug_printf_subst("SUBST '%s' first_ch %x\n", arg, first_ch);
+			G.last_exitcode = process_command_subs(&subst_result, arg);
+			G.expand_exitcode = G.last_exitcode;
+			debug_printf_subst("SUBST RES:%d '%s'\n", G.last_exitcode, subst_result.data);
+			n = append_str_maybe_ifs_split(output, n, first_ch, subst_result.data);
+			o_free(&subst_result);
+			break;
+		}
+#endif
+#if ENABLE_FEATURE_SH_MATH
+		case '+': {
+			/* <SPECIAL_VAR_SYMBOL>+arith<SPECIAL_VAR_SYMBOL> */
+			arith_t res;
+
+			arg++; /* skip '+' */
+			*p = '\0'; /* replace trailing <SPECIAL_VAR_SYMBOL> */
+			debug_printf_subst("ARITH '%s' first_ch %x\n", arg, first_ch);
+			res = expand_and_evaluate_arith(arg, NULL);
+			debug_printf_subst("ARITH RES '"ARITH_FMT"'\n", res);
+			sprintf(arith_buf, ARITH_FMT, res);
+			if (res < 0
+			 && first_ch == (char)('+'|0x80)
+			/* && (output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS) */
+			) {
+				/* Quoted negative ariths, like filename[0"$((-9))"],
+				 * should not be interpreted as glob ranges.
+				 * Convert leading '-' to '\-':
+				 */
+				o_grow_by(output, 1);
+				output->data[output->length++] = '\\';
+			}
+			o_addstr(output, arith_buf);
+			break;
+		}
+#endif
+		default:
+			/* <SPECIAL_VAR_SYMBOL>varname[ops]<SPECIAL_VAR_SYMBOL> */
+			n = expand_one_var(output, n, first_ch, arg, &p);
+			break;
+		} /* switch (char after <SPECIAL_VAR_SYMBOL>) */
+
+		/* Restore NULL'ed SPECIAL_VAR_SYMBOL.
+		 * Do the check to avoid writing to a const string. */
+		if (*p != SPECIAL_VAR_SYMBOL)
+			*p = SPECIAL_VAR_SYMBOL;
+		arg = ++p;
+	} /* end of "while (SPECIAL_VAR_SYMBOL is found) ..." */
+
+	if (*arg) {
+		/* handle trailing string */
+		if (output->ended_in_ifs) {
+			o_addchr(output, '\0');
+			n = o_save_ptr(output, n);
+		}
+		debug_print_list("expand_vars_to_list[a]", output, n);
+		/* this part is literal, and it was already pre-quoted
+		 * if needed (much earlier), do not use o_addQstr here!
+		 */
+		o_addstr(output, arg);
+		debug_print_list("expand_vars_to_list[b]", output, n);
+	} else
+	if (output->length == o_get_last_ptr(output, n) /* expansion is empty */
+	 && !(cant_be_null & 0x80)   /* and all vars were not quoted */
+	 && !output->has_quoted_part
+	) {
+		n--;
+		/* allow to reuse list[n] later without re-growth */
+		output->has_empty_slot = 1;
+	}
+
+	return n;
+}
+
+static char **expand_variables(char **argv, unsigned expflags)
+{
+	int n;
+	char **list;
+	o_string output = NULL_O_STRING;
+
+	output.o_expflags = expflags;
+
+	n = 0;
+	for (;;) {
+		/* go to next list[n] */
+		output.ended_in_ifs = 0;
+		n = o_save_ptr(&output, n);
+
+		if (!*argv)
+			break;
+
+		/* expand argv[i] */
+		n = expand_vars_to_list(&output, n, *argv++);
+		/* if (!output->has_empty_slot) -- need this?? */
+			o_addchr(&output, '\0');
+	}
+	debug_print_list("expand_variables", &output, n);
+
+	/* output.data (malloced in one block) gets returned in "list" */
+	list = o_finalize_list(&output, n);
+	debug_print_strings("expand_variables[1]", list);
+	return list;
+}
+
+static char **expand_strvec_to_strvec(char **argv)
+{
+	return expand_variables(argv, EXP_FLAG_GLOB | EXP_FLAG_ESC_GLOB_CHARS);
+}
+
+#if defined(CMD_SINGLEWORD_NOGLOB) || defined(CMD_TEST2_SINGLEWORD_NOGLOB)
+static char **expand_strvec_to_strvec_singleword_noglob(char **argv)
+{
+	return expand_variables(argv, EXP_FLAG_SINGLEWORD);
+}
+#endif
+
+/* Used for expansion of right hand of assignments,
+ * $((...)), heredocs, variable expansion parts.
+ *
+ * NB: should NOT do globbing!
+ * "export v=/bin/c*; env | grep ^v=" outputs "v=/bin/c*"
+ */
+static char *expand_string_to_string(const char *str, int EXP_flags, int do_unbackslash)
+{
+#if !BASH_PATTERN_SUBST && !ENABLE_HUSH_CASE
+	const int do_unbackslash = 1;
+	const int EXP_flags = EXP_FLAG_ESC_GLOB_CHARS;
+#endif
+	char *argv[2], **list;
+
+	debug_printf_expand("string_to_string<='%s'\n", str);
+	/* This is generally an optimization, but it also
+	 * handles "", which otherwise trips over !list[0] check below.
+	 * (is this ever happens that we actually get str="" here?)
+	 */
+	if (!strchr(str, SPECIAL_VAR_SYMBOL) && !strchr(str, '\\')) {
+		//TODO: Can use on strings with \ too, just unbackslash() them?
+		debug_printf_expand("string_to_string(fast)=>'%s'\n", str);
+		return xstrdup(str);
+	}
+
+	argv[0] = (char*)str;
+	argv[1] = NULL;
+	list = expand_variables(argv, EXP_flags | EXP_FLAG_SINGLEWORD);
+	if (!list[0]) {
+		/* Example where it happens:
+		 * x=; echo ${x:-"$@"}
+		 */
+		((char*)list)[0] = '\0';
+	} else {
+		if (HUSH_DEBUG)
+			if (list[1])
+				bb_simple_error_msg_and_die("BUG in varexp2");
+		/* actually, just move string 2*sizeof(char*) bytes back */
+		overlapping_strcpy((char*)list, list[0]);
+		if (do_unbackslash)
+			unbackslash((char*)list);
+	}
+	debug_printf_expand("string_to_string=>'%s'\n", (char*)list);
+	return (char*)list;
+}
+
+#if 0
+static char* expand_strvec_to_string(char **argv)
+{
+	char **list;
+
+	list = expand_variables(argv, EXP_FLAG_SINGLEWORD);
+	/* Convert all NULs to spaces */
+	if (list[0]) {
+		int n = 1;
+		while (list[n]) {
+			if (HUSH_DEBUG)
+				if (list[n-1] + strlen(list[n-1]) + 1 != list[n])
+					bb_error_msg_and_die("BUG in varexp3");
+			/* bash uses ' ' regardless of $IFS contents */
+			list[n][-1] = ' ';
+			n++;
+		}
+	}
+	overlapping_strcpy((char*)list, list[0] ? list[0] : "");
+	debug_printf_expand("strvec_to_string='%s'\n", (char*)list);
+	return (char*)list;
+}
+#endif
+
+static char **expand_assignments(char **argv, int count)
+{
+	int i;
+	char **p;
+
+	G.expanded_assignments = p = NULL;
+	/* Expand assignments into one string each */
+	for (i = 0; i < count; i++) {
+		p = add_string_to_strings(p,
+			expand_string_to_string(argv[i],
+				EXP_FLAG_ESC_GLOB_CHARS,
+				/*unbackslash:*/ 1
+			)
+		);
+		G.expanded_assignments = p;
+	}
+	G.expanded_assignments = NULL;
+	return p;
+}
+
+
+static void switch_off_special_sigs(unsigned mask)
+{
+	unsigned sig = 0;
+	while ((mask >>= 1) != 0) {
+		sig++;
+		if (!(mask & 1))
+			continue;
+#if ENABLE_HUSH_TRAP
+		if (G_traps) {
+			if (G_traps[sig] && !G_traps[sig][0])
+				/* trap is '', has to remain SIG_IGN */
+				continue;
+			free(G_traps[sig]);
+			G_traps[sig] = NULL;
+		}
+#endif
+		/* We are here only if no trap or trap was not '' */
+		install_sighandler(sig, SIG_DFL);
+	}
+}
+
+#if BB_MMU
+/* never called */
+void re_execute_shell(char ***to_free, const char *s,
+		char *g_argv0, char **g_argv,
+		char **builtin_argv) NORETURN;
+
+static void reset_traps_to_defaults(void)
+{
+	/* This function is always called in a child shell
+	 * after fork (not vfork, NOMMU doesn't use this function).
+	 */
+	IF_HUSH_TRAP(unsigned sig;)
+	unsigned mask;
+
+	/* Child shells are not interactive.
+	 * SIGTTIN/SIGTTOU/SIGTSTP should not have special handling.
+	 * Testcase: (while :; do :; done) + ^Z should background.
+	 * Same goes for SIGTERM, SIGHUP, SIGINT.
+	 */
+	mask = (G.special_sig_mask & SPECIAL_INTERACTIVE_SIGS) | G_fatal_sig_mask;
+	if (!G_traps && !mask)
+		return; /* already no traps and no special sigs */
+
+	/* Switch off special sigs */
+	switch_off_special_sigs(mask);
+# if ENABLE_HUSH_JOB
+	G_fatal_sig_mask = 0;
+# endif
+	G.special_sig_mask &= ~SPECIAL_INTERACTIVE_SIGS;
+	/* SIGQUIT,SIGCHLD and maybe SPECIAL_JOBSTOP_SIGS
+	 * remain set in G.special_sig_mask */
+
+# if ENABLE_HUSH_TRAP
+	if (!G_traps)
+		return;
+
+	/* Reset all sigs to default except ones with empty traps */
+	for (sig = 0; sig < NSIG; sig++) {
+		if (!G_traps[sig])
+			continue; /* no trap: nothing to do */
+		if (!G_traps[sig][0])
+			continue; /* empty trap: has to remain SIG_IGN */
+		/* sig has non-empty trap, reset it: */
+		free(G_traps[sig]);
+		G_traps[sig] = NULL;
+		/* There is no signal for trap 0 (EXIT) */
+		if (sig == 0)
+			continue;
+		install_sighandler(sig, pick_sighandler(sig));
+	}
+# endif
+}
+
+#else /* !BB_MMU */
+
+static void re_execute_shell(char ***to_free, const char *s,
+		char *g_argv0, char **g_argv,
+		char **builtin_argv) NORETURN;
+static void re_execute_shell(char ***to_free, const char *s,
+		char *g_argv0, char **g_argv,
+		char **builtin_argv)
+{
+# define NOMMU_HACK_FMT ("-$%x:%x:%x:%x:%x:%llx" IF_HUSH_LOOPS(":%x"))
+	/* delims + 2 * (number of bytes in printed hex numbers) */
+	char param_buf[sizeof(NOMMU_HACK_FMT) + 2 * (sizeof(int)*6 + sizeof(long long)*1)];
+	char *heredoc_argv[4];
+	struct variable *cur;
+# if ENABLE_HUSH_FUNCTIONS
+	struct function *funcp;
+# endif
+	char **argv, **pp;
+	unsigned cnt;
+	unsigned long long empty_trap_mask;
+
+	if (!g_argv0) { /* heredoc */
+		argv = heredoc_argv;
+		argv[0] = (char *) G.argv0_for_re_execing;
+		argv[1] = (char *) "-<";
+		argv[2] = (char *) s;
+		argv[3] = NULL;
+		pp = &argv[3]; /* used as pointer to empty environment */
+		goto do_exec;
+	}
+
+	cnt = 0;
+	pp = builtin_argv;
+	if (pp) while (*pp++)
+		cnt++;
+
+	empty_trap_mask = 0;
+	if (G_traps) {
+		int sig;
+		for (sig = 1; sig < NSIG; sig++) {
+			if (G_traps[sig] && !G_traps[sig][0])
+				empty_trap_mask |= 1LL << sig;
+		}
+	}
+
+	sprintf(param_buf, NOMMU_HACK_FMT
+			, (unsigned) G.root_pid
+			, (unsigned) G.root_ppid
+			, (unsigned) G.last_bg_pid
+			, (unsigned) G.last_exitcode
+			, cnt
+			, empty_trap_mask
+			IF_HUSH_LOOPS(, G.depth_of_loop)
+			);
+# undef NOMMU_HACK_FMT
+	/* 1:hush 2:-$<pid>:<pid>:<exitcode>:<etc...> <vars...> <funcs...>
+	 * 3:-c 4:<cmd> 5:<arg0> <argN...> 6:NULL
+	 */
+	cnt += 6;
+	for (cur = G.top_var; cur; cur = cur->next) {
+		if (!cur->flg_export || cur->flg_read_only)
+			cnt += 2;
+	}
+# if ENABLE_HUSH_FUNCTIONS
+	for (funcp = G.top_func; funcp; funcp = funcp->next)
+		cnt += 3;
+# endif
+	pp = g_argv;
+	while (*pp++)
+		cnt++;
+	*to_free = argv = pp = xzalloc(sizeof(argv[0]) * cnt);
+	*pp++ = (char *) G.argv0_for_re_execing;
+	*pp++ = param_buf;
+	for (cur = G.top_var; cur; cur = cur->next) {
+		if (strcmp(cur->varstr, hush_version_str) == 0)
+			continue;
+		if (cur->flg_read_only) {
+			*pp++ = (char *) "-R";
+			*pp++ = cur->varstr;
+		} else if (!cur->flg_export) {
+			*pp++ = (char *) "-V";
+			*pp++ = cur->varstr;
+		}
+	}
+# if ENABLE_HUSH_FUNCTIONS
+	for (funcp = G.top_func; funcp; funcp = funcp->next) {
+		*pp++ = (char *) "-F";
+		*pp++ = funcp->name;
+		*pp++ = funcp->body_as_string;
+	}
+# endif
+	/* We can pass activated traps here. Say, -Tnn:trap_string
+	 *
+	 * However, POSIX says that subshells reset signals with traps
+	 * to SIG_DFL.
+	 * I tested bash-3.2 and it not only does that with true subshells
+	 * of the form ( list ), but with any forked children shells.
+	 * I set trap "echo W" WINCH; and then tried:
+	 *
+	 * { echo 1; sleep 20; echo 2; } &
+	 * while true; do echo 1; sleep 20; echo 2; break; done &
+	 * true | { echo 1; sleep 20; echo 2; } | cat
+	 *
+	 * In all these cases sending SIGWINCH to the child shell
+	 * did not run the trap. If I add trap "echo V" WINCH;
+	 * _inside_ group (just before echo 1), it works.
+	 *
+	 * I conclude it means we don't need to pass active traps here.
+	 */
+	*pp++ = (char *) "-c";
+	*pp++ = (char *) s;
+	if (builtin_argv) {
+		while (*++builtin_argv)
+			*pp++ = *builtin_argv;
+		*pp++ = (char *) "";
+	}
+	*pp++ = g_argv0;
+	while (*g_argv)
+		*pp++ = *g_argv++;
+	/* *pp = NULL; - is already there */
+	pp = environ;
+
+ do_exec:
+	debug_printf_exec("re_execute_shell pid:%d cmd:'%s'\n", getpid(), s);
+	/* Don't propagate SIG_IGN to the child */
+	if (SPECIAL_JOBSTOP_SIGS != 0)
+		switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
+	execve(bb_busybox_exec_path, argv, pp);
+	/* Fallback. Useful for init=/bin/hush usage etc */
+	if (argv[0][0] == '/')
+		execve(argv[0], argv, pp);
+	xfunc_error_retval = 127;
+	bb_simple_error_msg_and_die("can't re-execute the shell");
+}
+#endif  /* !BB_MMU */
+
+
+static int run_and_free_list(struct pipe *pi);
+
+/* Executing from string: eval, sh -c '...'
+ *          or from file: /etc/profile, . file, sh <script>, sh (intereactive)
+ * end_trigger controls how often we stop parsing
+ * NUL: parse all, execute, return
+ * ';': parse till ';' or newline, execute, repeat till EOF
+ */
+static void parse_and_run_stream(struct in_str *inp, int end_trigger)
+{
+	/* Why we need empty flag?
+	 * An obscure corner case "false; ``; echo $?":
+	 * empty command in `` should still set $? to 0.
+	 * But we can't just set $? to 0 at the start,
+	 * this breaks "false; echo `echo $?`" case.
+	 */
+	bool empty = 1;
+	while (1) {
+		struct pipe *pipe_list;
+
+#if ENABLE_HUSH_INTERACTIVE
+		if (end_trigger == ';') {
+			G.promptmode = 0; /* PS1 */
+			debug_printf_prompt("%s promptmode=%d\n", __func__, G.promptmode);
+		}
+#endif
+		pipe_list = parse_stream(NULL, NULL, inp, end_trigger);
+		if (!pipe_list || pipe_list == ERR_PTR) { /* EOF/error */
+			/* If we are in "big" script
+			 * (not in `cmd` or something similar)...
+			 */
+			if (pipe_list == ERR_PTR && end_trigger == ';') {
+				/* Discard cached input (rest of line) */
+				int ch = inp->last_char;
+				while (ch != EOF && ch != '\n') {
+					//bb_error_msg("Discarded:'%c'", ch);
+					ch = i_getch(inp);
+				}
+				/* Force prompt */
+				inp->p = NULL;
+				/* This stream isn't empty */
+				empty = 0;
+				continue;
+			}
+			if (!pipe_list && empty)
+				G.last_exitcode = 0;
+			break;
+		}
+		debug_print_tree(pipe_list, 0);
+		debug_printf_exec("parse_and_run_stream: run_and_free_list\n");
+		run_and_free_list(pipe_list);
+		empty = 0;
+		if (G_flag_return_in_progress == 1)
+			break;
+	}
+}
+
+static void parse_and_run_string(const char *s)
+{
+	struct in_str input;
+	//IF_HUSH_LINENO_VAR(unsigned sv = G.parse_lineno;)
+
+	setup_string_in_str(&input, s);
+	parse_and_run_stream(&input, '\0');
+	//IF_HUSH_LINENO_VAR(G.parse_lineno = sv;)
+}
+
+static void parse_and_run_file(HFILE *fp)
+{
+	struct in_str input;
+	IF_HUSH_LINENO_VAR(unsigned sv = G.parse_lineno;)
+
+	IF_HUSH_LINENO_VAR(G.parse_lineno = 1;)
+	setup_file_in_str(&input, fp);
+	parse_and_run_stream(&input, ';');
+	IF_HUSH_LINENO_VAR(G.parse_lineno = sv;)
+}
+
+#if ENABLE_HUSH_TICK
+static int generate_stream_from_string(const char *s, pid_t *pid_p)
+{
+	pid_t pid;
+	int channel[2];
+# if !BB_MMU
+	char **to_free = NULL;
+# endif
+
+	xpipe(channel);
+	pid = BB_MMU ? xfork() : xvfork();
+	if (pid == 0) { /* child */
+		disable_restore_tty_pgrp_on_exit();
+		/* Process substitution is not considered to be usual
+		 * 'command execution'.
+		 * SUSv3 says ctrl-Z should be ignored, ctrl-C should not.
+		 */
+		bb_signals(0
+			+ (1 << SIGTSTP)
+			+ (1 << SIGTTIN)
+			+ (1 << SIGTTOU)
+			, SIG_IGN);
+		close(channel[0]); /* NB: close _first_, then move fd! */
+		xmove_fd(channel[1], 1);
+# if ENABLE_HUSH_TRAP
+		/* Awful hack for `trap` or $(trap).
+		 *
+		 * http://www.opengroup.org/onlinepubs/009695399/utilities/trap.html
+		 * contains an example where "trap" is executed in a subshell:
+		 *
+		 * save_traps=$(trap)
+		 * ...
+		 * eval "$save_traps"
+		 *
+		 * Standard does not say that "trap" in subshell shall print
+		 * parent shell's traps. It only says that its output
+		 * must have suitable form, but then, in the above example
+		 * (which is not supposed to be normative), it implies that.
+		 *
+		 * bash (and probably other shell) does implement it
+		 * (traps are reset to defaults, but "trap" still shows them),
+		 * but as a result, "trap" logic is hopelessly messed up:
+		 *
+		 * # trap
+		 * trap -- 'echo Ho' SIGWINCH  <--- we have a handler
+		 * # (trap)        <--- trap is in subshell - no output (correct, traps are reset)
+		 * # true | trap   <--- trap is in subshell - no output (ditto)
+		 * # echo `true | trap`    <--- in subshell - output (but traps are reset!)
+		 * trap -- 'echo Ho' SIGWINCH
+		 * # echo `(trap)`         <--- in subshell in subshell - output
+		 * trap -- 'echo Ho' SIGWINCH
+		 * # echo `true | (trap)`  <--- in subshell in subshell in subshell - output!
+		 * trap -- 'echo Ho' SIGWINCH
+		 *
+		 * The rules when to forget and when to not forget traps
+		 * get really complex and nonsensical.
+		 *
+		 * Our solution: ONLY bare $(trap) or `trap` is special.
+		 */
+		s = skip_whitespace(s);
+		if (is_prefixed_with(s, "trap")
+		 && skip_whitespace(s + 4)[0] == '\0'
+		) {
+			static const char *const argv[] = { NULL, NULL };
+			builtin_trap((char**)argv);
+			fflush_all(); /* important */
+			_exit(0);
+		}
+# endif
+# if BB_MMU
+		/* Prevent it from trying to handle ctrl-z etc */
+		IF_HUSH_JOB(G.run_list_level = 1;)
+		CLEAR_RANDOM_T(&G.random_gen); /* or else $RANDOM repeats in child */
+		reset_traps_to_defaults();
+		IF_HUSH_MODE_X(G.x_mode_depth++;)
+		//bb_error_msg("%s: ++x_mode_depth=%d", __func__, G.x_mode_depth);
+		parse_and_run_string(s);
+		_exit(G.last_exitcode);
+# else
+	/* We re-execute after vfork on NOMMU. This makes this script safe:
+	 * yes "0123456789012345678901234567890" | dd bs=32 count=64k >BIG
+	 * huge=`cat BIG` # was blocking here forever
+	 * echo OK
+	 */
+		re_execute_shell(&to_free,
+				s,
+				G.global_argv[0],
+				G.global_argv + 1,
+				NULL);
+# endif
+	}
+
+	/* parent */
+	*pid_p = pid;
+# if ENABLE_HUSH_FAST
+	G.count_SIGCHLD++;
+//bb_error_msg("[%d] fork in generate_stream_from_string:"
+//		" G.count_SIGCHLD:%d G.handled_SIGCHLD:%d",
+//		getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+# endif
+	enable_restore_tty_pgrp_on_exit();
+# if !BB_MMU
+	free(to_free);
+# endif
+	close(channel[1]);
+	return channel[0];
+}
+
+/* Return code is exit status of the process that is run. */
+static int process_command_subs(o_string *dest, const char *s)
+{
+	FILE *fp;
+	pid_t pid;
+	int status, ch, eol_cnt;
+
+	fp = xfdopen_for_read(generate_stream_from_string(s, &pid));
+
+	/* Now send results of command back into original context */
+	eol_cnt = 0;
+	while ((ch = getc(fp)) != EOF) {
+		if (ch == '\0')
+			continue;
+		if (ch == '\n') {
+			eol_cnt++;
+			continue;
+		}
+		while (eol_cnt) {
+			o_addchr(dest, '\n');
+			eol_cnt--;
+		}
+		o_addQchr(dest, ch);
+	}
+
+	debug_printf("done reading from `cmd` pipe, closing it\n");
+	fclose(fp);
+	/* We need to extract exitcode. Test case
+	 * "true; echo `sleep 1; false` $?"
+	 * should print 1 */
+	safe_waitpid(pid, &status, 0);
+	debug_printf("child exited. returning its exitcode:%d\n", WEXITSTATUS(status));
+	return WEXITSTATUS(status);
+}
+#endif /* ENABLE_HUSH_TICK */
+
+
+static void setup_heredoc(struct redir_struct *redir)
+{
+	struct fd_pair pair;
+	pid_t pid;
+	int len, written;
+	/* the _body_ of heredoc (misleading field name) */
+	const char *heredoc = redir->rd_filename;
+	char *expanded;
+#if !BB_MMU
+	char **to_free;
+#endif
+
+	expanded = NULL;
+	if (!(redir->rd_dup & HEREDOC_QUOTED)) {
+		expanded = encode_then_expand_string(heredoc);
+		if (expanded)
+			heredoc = expanded;
+	}
+	len = strlen(heredoc);
+
+	close(redir->rd_fd); /* often saves dup2+close in xmove_fd */
+	xpiped_pair(pair);
+	xmove_fd(pair.rd, redir->rd_fd);
+
+	/* Try writing without forking. Newer kernels have
+	 * dynamically growing pipes. Must use non-blocking write! */
+	ndelay_on(pair.wr);
+	while (1) {
+		written = write(pair.wr, heredoc, len);
+		if (written <= 0)
+			break;
+		len -= written;
+		if (len == 0) {
+			close(pair.wr);
+			free(expanded);
+			return;
+		}
+		heredoc += written;
+	}
+	ndelay_off(pair.wr);
+
+	/* Okay, pipe buffer was not big enough */
+	/* Note: we must not create a stray child (bastard? :)
+	 * for the unsuspecting parent process. Child creates a grandchild
+	 * and exits before parent execs the process which consumes heredoc
+	 * (that exec happens after we return from this function) */
+#if !BB_MMU
+	to_free = NULL;
+#endif
+	pid = xvfork();
+	if (pid == 0) {
+		/* child */
+		disable_restore_tty_pgrp_on_exit();
+		pid = BB_MMU ? xfork() : xvfork();
+		if (pid != 0)
+			_exit(0);
+		/* grandchild */
+		close(redir->rd_fd); /* read side of the pipe */
+#if BB_MMU
+		full_write(pair.wr, heredoc, len); /* may loop or block */
+		_exit(0);
+#else
+		/* Delegate blocking writes to another process */
+		xmove_fd(pair.wr, STDOUT_FILENO);
+		re_execute_shell(&to_free, heredoc, NULL, NULL, NULL);
+#endif
+	}
+	/* parent */
+#if ENABLE_HUSH_FAST
+	G.count_SIGCHLD++;
+//bb_error_msg("[%d] fork in setup_heredoc: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+#endif
+	enable_restore_tty_pgrp_on_exit();
+#if !BB_MMU
+	free(to_free);
+#endif
+	close(pair.wr);
+	free(expanded);
+	wait(NULL); /* wait till child has died */
+}
+
+struct squirrel {
+	int orig_fd;
+	int moved_to;
+	/* moved_to = n: fd was moved to n; restore back to orig_fd after redir */
+	/* 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;
+
+	i = 0;
+	if (sq) for (; sq[i].orig_fd >= 0; i++) {
+		/* If we collide with an already moved fd... */
+		if (fd == sq[i].moved_to) {
+			sq[i].moved_to = dup_CLOEXEC(sq[i].moved_to, avoid_fd);
+			debug_printf_redir("redirect_fd %d: already busy, moving to %d\n", fd, sq[i].moved_to);
+			if (sq[i].moved_to < 0) /* what? */
+				xfunc_die();
+			return sq;
+		}
+		if (fd == sq[i].orig_fd) {
+			/* Example: echo Hello >/dev/null 1>&2 */
+			debug_printf_redir("redirect_fd %d: already moved\n", fd);
+			return sq;
+		}
+	}
+
+	/* If this fd is open, we move and remember it; if it's closed, moved_to = -1 */
+	moved_to = dup_CLOEXEC(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();
+	return append_squirrel(sq, i, fd, moved_to);
+}
+
+static struct squirrel *add_squirrel_closed(struct squirrel *sq, int fd)
+{
+	int i;
+
+	i = 0;
+	if (sq) for (; sq[i].orig_fd >= 0; i++) {
+		/* If we collide with an already moved fd... */
+		if (fd == sq[i].orig_fd) {
+			/* Examples:
+			 * "echo 3>FILE 3>&- 3>FILE"
+			 * "echo 3>&- 3>FILE"
+			 * No need for last redirect to insert
+			 * another "need to close 3" indicator.
+			 */
+			debug_printf_redir("redirect_fd %d: already moved or closed\n", fd);
+			return sq;
+		}
+	}
+
+	debug_printf_redir("redirect_fd %d: previous fd was closed\n", fd);
+	return append_squirrel(sq, i, fd, -1);
+}
+
+/* fd: redirect wants this fd to be used (e.g. 3>file).
+ * Move all conflicting internally used fds,
+ * and remember them so that we can restore them later.
+ */
+static int save_fd_on_redirect(int fd, int avoid_fd, struct squirrel **sqp)
+{
+	if (avoid_fd < 9) /* the important case here is that it can be -1 */
+		avoid_fd = 9;
+
+#if ENABLE_HUSH_INTERACTIVE
+	if (fd != 0 /* don't trigger for G_interactive_fd == 0 (that's "not interactive" flag) */
+	 && fd == G_interactive_fd
+	) {
+		/* Testcase: "ls -l /proc/$$/fd 255>&-" should work */
+		G_interactive_fd = xdup_CLOEXEC_and_close(G_interactive_fd, avoid_fd);
+		debug_printf_redir("redirect_fd %d: matches interactive_fd, moving it to %d\n", fd, G_interactive_fd);
+		return 1; /* "we closed fd" */
+	}
+#endif
+	/* Are we called from setup_redirects(squirrel==NULL)
+	 * in redirect in a [v]forked child?
+	 */
+	if (sqp == NULL) {
+		/* No need to move script fds.
+		 * For NOMMU case, it's actively wrong: we'd change ->fd
+		 * fields in memory for the parent, but parent's fds
+		 * aren't moved, it would use wrong fd!
+		 * Reproducer: "cmd 3>FILE" in script.
+		 * If we would call move_HFILEs_on_redirect(), child would:
+		 *  fcntl64(3, F_DUPFD_CLOEXEC, 10)   = 10
+		 *  close(3)                          = 0
+		 * and change ->fd to 10 if fd#3 is a script fd. WRONG.
+		 */
+		//bb_error_msg("sqp == NULL: [v]forked child");
+		return 0;
+	}
+
+	/* If this one of script's fds? */
+	if (move_HFILEs_on_redirect(fd, avoid_fd))
+		return 1; /* yes. "we closed fd" (actually moved it) */
+
+	/* Are we called for "exec 3>FILE"? Came through
+	 * redirect_and_varexp_helper(squirrel=ERR_PTR) -> setup_redirects(ERR_PTR)
+	 * This case used to fail for this script:
+	 *  exec 3>FILE
+	 *  echo Ok
+	 *  ...100000 more lines...
+	 *  echo Ok
+	 * as follows:
+	 *  read(3, "exec 3>FILE\necho Ok\necho Ok"..., 1024) = 1024
+	 *  open("FILE", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 4
+	 *  dup2(4, 3)                        = 3
+	 *  ^^^^^^^^ oops, we lost fd#3 opened to our script!
+	 *  close(4)                          = 0
+	 *  write(1, "Ok\n", 3)               = 3
+	 *  ...                               = 3
+	 *  write(1, "Ok\n", 3)               = 3
+	 *  read(3, 0x94fbc08, 1024)          = -1 EBADF (Bad file descriptor)
+	 *  ^^^^^^^^ oops, wrong fd!!!
+	 * With this case separate from sqp == NULL and *after* move_HFILEs,
+	 * it now works:
+	 */
+	if (sqp == ERR_PTR) {
+		/* Don't preserve redirected fds: exec is _meant_ to change these */
+		//bb_error_msg("sqp == ERR_PTR: exec >FILE");
+		return 0;
+	}
+
+	/* Check whether it collides with any open fds (e.g. stdio), save fds as needed */
+	*sqp = add_squirrel(*sqp, fd, avoid_fd);
+	return 0; /* "we did not close fd" */
+}
+
+static void restore_redirects(struct squirrel *sq)
+{
+	if (sq) {
+		int i;
+		for (i = 0; sq[i].orig_fd >= 0; i++) {
+			if (sq[i].moved_to >= 0) {
+				/* We simply die on error */
+				debug_printf_redir("restoring redirected fd from %d to %d\n", sq[i].moved_to, sq[i].orig_fd);
+				xmove_fd(sq[i].moved_to, sq[i].orig_fd);
+			} else {
+				/* cmd1 9>FILE; cmd2_should_see_fd9_closed */
+				debug_printf_redir("restoring redirected fd %d: closing it\n", sq[i].orig_fd);
+				close(sq[i].orig_fd);
+			}
+		}
+		free(sq);
+	}
+	if (G.HFILE_stdin
+	 && G.HFILE_stdin->fd > STDIN_FILENO
+	/* we compare > STDIN, not == STDIN, since hfgetc()
+	 * closes fd and sets ->fd to -1 if EOF is reached.
+	 * Testcase: echo 'pwd' | hush
+	 */
+	) {
+		/* Testcase: interactive "read r <FILE; echo $r; read r; echo $r".
+		 * Redirect moves ->fd to e.g. 10,
+		 * and it is not restored above (we do not restore script fds
+		 * after redirects, we just use new, "moved" fds).
+		 * However for stdin, get_user_input() -> read_line_input(),
+		 * and read builtin, depend on fd == STDIN_FILENO.
+		 */
+		debug_printf_redir("restoring %d to stdin\n", G.HFILE_stdin->fd);
+		xmove_fd(G.HFILE_stdin->fd, STDIN_FILENO);
+		G.HFILE_stdin->fd = STDIN_FILENO;
+	}
+
+	/* If moved, G_interactive_fd stays on new fd, not restoring it */
+}
+
+#if ENABLE_FEATURE_SH_STANDALONE && BB_MMU
+static void close_saved_fds_and_FILE_fds(void)
+{
+	if (G_interactive_fd)
+		close(G_interactive_fd);
+	close_all_HFILE_list();
+}
+#endif
+
+static int internally_opened_fd(int fd, struct squirrel *sq)
+{
+	int i;
+
+#if ENABLE_HUSH_INTERACTIVE
+	if (fd == G_interactive_fd)
+		return 1;
+#endif
+	/* If this one of script's fds? */
+	if (fd_in_HFILEs(fd))
+		return 1;
+
+	if (sq) for (i = 0; sq[i].orig_fd >= 0; i++) {
+		if (fd == sq[i].moved_to)
+			return 1;
+	}
+	return 0;
+}
+
+/* squirrel != NULL means we squirrel away copies of stdin, stdout,
+ * and stderr if they are redirected. */
+static int setup_redirects(struct command *prog, struct squirrel **sqp)
+{
+	struct redir_struct *redir;
+
+	for (redir = prog->redirects; redir; redir = redir->next) {
+		int newfd;
+		int closed;
+
+		if (redir->rd_type == REDIRECT_HEREDOC2) {
+			/* "rd_fd<<HERE" case */
+			save_fd_on_redirect(redir->rd_fd, /*avoid:*/ 0, sqp);
+			/* for REDIRECT_HEREDOC2, rd_filename holds _contents_
+			 * of the heredoc */
+			debug_printf_redir("set heredoc '%s'\n",
+					redir->rd_filename);
+			setup_heredoc(redir);
+			continue;
+		}
+
+		if (redir->rd_dup == REDIRFD_TO_FILE) {
+			/* "rd_fd<*>file" case (<*> is <,>,>>,<>) */
+			char *p;
+			int mode;
+
+			if (redir->rd_filename == NULL) {
+				/* Examples:
+				 * "cmd >" (no filename)
+				 * "cmd > <file" (2nd redirect starts too early)
+				 */
+				syntax_error("invalid redirect");
+				continue;
+			}
+			mode = redir_table[redir->rd_type].mode;
+			p = expand_string_to_string(redir->rd_filename,
+				EXP_FLAG_ESC_GLOB_CHARS, /*unbackslash:*/ 1);
+			newfd = open_or_warn(p, mode);
+			free(p);
+			if (newfd < 0) {
+				/* Error message from open_or_warn can be lost
+				 * if stderr has been redirected, but bash
+				 * and ash both lose it as well
+				 * (though zsh doesn't!)
+				 */
+				return 1;
+			}
+			if (newfd == 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:
+				 */
+				*sqp = add_squirrel_closed(*sqp, newfd);
+				debug_printf_redir("redir to previously closed fd %d\n", newfd);
+			}
+		} else {
+			/* "rd_fd>&rd_dup" or "rd_fd>&-" case */
+			newfd = redir->rd_dup;
+		}
+
+		if (newfd == redir->rd_fd)
+			continue;
+
+		/* if "N>FILE": move newfd to redir->rd_fd */
+		/* if "N>&M": dup newfd to redir->rd_fd */
+		/* if "N>&-": close redir->rd_fd (newfd is REDIRFD_CLOSE) */
+
+		closed = save_fd_on_redirect(redir->rd_fd, /*avoid:*/ newfd, sqp);
+		if (newfd == REDIRFD_CLOSE) {
+			/* "N>&-" means "close me" */
+			if (!closed) {
+				/* ^^^ optimization: saving may already
+				 * have closed it. If not... */
+				close(redir->rd_fd);
+			}
+			/* Sometimes we do another close on restore, getting EBADF.
+			 * Consider "echo 3>FILE 3>&-"
+			 * first redirect remembers "need to close 3",
+			 * and second redirect closes 3! Restore code then closes 3 again.
+			 */
+		} else {
+			/* if newfd is a script fd or saved fd, simulate EBADF */
+			if (internally_opened_fd(newfd, sqp && sqp != ERR_PTR ? *sqp : NULL)) {
+				//errno = EBADF;
+				//bb_perror_msg_and_die("can't duplicate file descriptor");
+				newfd = -1; /* same effect as code above */
+			}
+			xdup2(newfd, redir->rd_fd);
+			if (redir->rd_dup == REDIRFD_TO_FILE)
+				/* "rd_fd > FILE" */
+				close(newfd);
+			/* else: "rd_fd > rd_dup" */
+		}
+	}
+	return 0;
+}
+
+static char *find_in_path(const char *arg)
+{
+	char *ret = NULL;
+	const char *PATH = get_local_var_value("PATH");
+
+	if (!PATH)
+		return NULL;
+
+	while (1) {
+		const char *end = strchrnul(PATH, ':');
+		int sz = end - PATH; /* must be int! */
+
+		free(ret);
+		if (sz != 0) {
+			ret = xasprintf("%.*s/%s", sz, PATH, arg);
+		} else {
+			/* We have xxx::yyyy in $PATH,
+			 * it means "use current dir" */
+			ret = xstrdup(arg);
+		}
+		if (access(ret, F_OK) == 0)
+			break;
+
+		if (*end == '\0') {
+			free(ret);
+			return NULL;
+		}
+		PATH = end + 1;
+	}
+
+	return ret;
+}
+
+static const struct built_in_command *find_builtin_helper(const char *name,
+		const struct built_in_command *x,
+		const struct built_in_command *end)
+{
+	while (x != end) {
+		if (strcmp(name, x->b_cmd) != 0) {
+			x++;
+			continue;
+		}
+		debug_printf_exec("found builtin '%s'\n", name);
+		return x;
+	}
+	return NULL;
+}
+static const struct built_in_command *find_builtin1(const char *name)
+{
+	return find_builtin_helper(name, bltins1, &bltins1[ARRAY_SIZE(bltins1)]);
+}
+static const struct built_in_command *find_builtin(const char *name)
+{
+	const struct built_in_command *x = find_builtin1(name);
+	if (x)
+		return x;
+	return find_builtin_helper(name, bltins2, &bltins2[ARRAY_SIZE(bltins2)]);
+}
+
+#if ENABLE_HUSH_JOB && EDITING_HAS_get_exe_name
+static const char * FAST_FUNC get_builtin_name(int i)
+{
+	if (/*i >= 0 && */ i < ARRAY_SIZE(bltins1)) {
+		return bltins1[i].b_cmd;
+	}
+	i -= ARRAY_SIZE(bltins1);
+	if (i < ARRAY_SIZE(bltins2)) {
+		return bltins2[i].b_cmd;
+	}
+	return NULL;
+}
+#endif
+
+static void remove_nested_vars(void)
+{
+	struct variable *cur;
+	struct variable **cur_pp;
+
+	cur_pp = &G.top_var;
+	while ((cur = *cur_pp) != NULL) {
+		if (cur->var_nest_level <= G.var_nest_level) {
+			cur_pp = &cur->next;
+			continue;
+		}
+		/* Unexport */
+		if (cur->flg_export) {
+			debug_printf_env("unexporting nested '%s'/%u\n", cur->varstr, cur->var_nest_level);
+			bb_unsetenv(cur->varstr);
+		}
+		/* Remove from global list */
+		*cur_pp = cur->next;
+		/* Free */
+		if (!cur->max_len) {
+			debug_printf_env("freeing nested '%s'/%u\n", cur->varstr, cur->var_nest_level);
+			free(cur->varstr);
+		}
+		free(cur);
+	}
+}
+
+static void enter_var_nest_level(void)
+{
+	G.var_nest_level++;
+	debug_printf_env("var_nest_level++ %u\n", G.var_nest_level);
+
+	/* Try:	f() { echo -n .; f; }; f
+	 * struct variable::var_nest_level is uint16_t,
+	 * thus limiting recursion to < 2^16.
+	 * In any case, with 8 Mbyte stack SEGV happens
+	 * not too long after 2^16 recursions anyway.
+	 */
+	if (G.var_nest_level > 0xff00)
+		bb_error_msg_and_die("fatal recursion (depth %u)", G.var_nest_level);
+}
+
+static void leave_var_nest_level(void)
+{
+	G.var_nest_level--;
+	debug_printf_env("var_nest_level-- %u\n", G.var_nest_level);
+	if (HUSH_DEBUG && (int)G.var_nest_level < 0)
+		bb_simple_error_msg_and_die("BUG: nesting underflow");
+
+	remove_nested_vars();
+}
+
+#if ENABLE_HUSH_FUNCTIONS
+static struct function **find_function_slot(const char *name)
+{
+	struct function *funcp;
+	struct function **funcpp = &G.top_func;
+
+	while ((funcp = *funcpp) != NULL) {
+		if (strcmp(name, funcp->name) == 0) {
+			debug_printf_exec("found function '%s'\n", name);
+			break;
+		}
+		funcpp = &funcp->next;
+	}
+	return funcpp;
+}
+
+static ALWAYS_INLINE const struct function *find_function(const char *name)
+{
+	const struct function *funcp = *find_function_slot(name);
+	return funcp;
+}
+
+/* Note: takes ownership on name ptr */
+static struct function *new_function(char *name)
+{
+	struct function **funcpp = find_function_slot(name);
+	struct function *funcp = *funcpp;
+
+	if (funcp != NULL) {
+		struct command *cmd = funcp->parent_cmd;
+		debug_printf_exec("func %p parent_cmd %p\n", funcp, cmd);
+		if (!cmd) {
+			debug_printf_exec("freeing & replacing function '%s'\n", funcp->name);
+			free(funcp->name);
+			/* Note: if !funcp->body, do not free body_as_string!
+			 * This is a special case of "-F name body" function:
+			 * body_as_string was not malloced! */
+			if (funcp->body) {
+				free_pipe_list(funcp->body);
+# if !BB_MMU
+				free(funcp->body_as_string);
+# endif
+			}
+		} else {
+			debug_printf_exec("reinserting in tree & replacing function '%s'\n", funcp->name);
+			cmd->argv[0] = funcp->name;
+			cmd->group = funcp->body;
+# if !BB_MMU
+			cmd->group_as_string = funcp->body_as_string;
+# endif
+		}
+	} else {
+		debug_printf_exec("remembering new function '%s'\n", name);
+		funcp = *funcpp = xzalloc(sizeof(*funcp));
+		/*funcp->next = NULL;*/
+	}
+
+	funcp->name = name;
+	return funcp;
+}
+
+# if ENABLE_HUSH_UNSET
+static void unset_func(const char *name)
+{
+	struct function **funcpp = find_function_slot(name);
+	struct function *funcp = *funcpp;
+
+	if (funcp != NULL) {
+		debug_printf_exec("freeing function '%s'\n", funcp->name);
+		*funcpp = funcp->next;
+		/* funcp is unlinked now, deleting it.
+		 * Note: if !funcp->body, the function was created by
+		 * "-F name body", do not free ->body_as_string
+		 * and ->name as they were not malloced. */
+		if (funcp->body) {
+			free_pipe_list(funcp->body);
+			free(funcp->name);
+#  if !BB_MMU
+			free(funcp->body_as_string);
+#  endif
+		}
+		free(funcp);
+	}
+}
+# endif
+
+# if BB_MMU
+#define exec_function(to_free, funcp, argv) \
+	exec_function(funcp, argv)
+# endif
+static void exec_function(char ***to_free,
+		const struct function *funcp,
+		char **argv) NORETURN;
+static void exec_function(char ***to_free,
+		const struct function *funcp,
+		char **argv)
+{
+# if BB_MMU
+	int n;
+
+	argv[0] = G.global_argv[0];
+	G.global_argv = argv;
+	G.global_argc = n = 1 + string_array_len(argv + 1);
+
+// Example when we are here: "cmd | func"
+// func will run with saved-redirect fds open.
+// $ f() { echo /proc/self/fd/*; }
+// $ true | f
+// /proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2 /proc/self/fd/255 /proc/self/fd/3
+// stdio^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ G_interactive_fd^ DIR fd for glob
+// Same in script:
+// $ . ./SCRIPT
+// /proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2 /proc/self/fd/255 /proc/self/fd/3 /proc/self/fd/4
+// stdio^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ G_interactive_fd^ opened ./SCRIPT DIR fd for glob
+// They are CLOEXEC so external programs won't see them, but
+// for "more correctness" we might want to close those extra fds here:
+//?	close_saved_fds_and_FILE_fds();
+
+	/* "we are in a function, ok to use return" */
+	G_flag_return_in_progress = -1;
+	enter_var_nest_level();
+	IF_HUSH_LOCAL(G.func_nest_level++;)
+
+	/* On MMU, funcp->body is always non-NULL */
+	n = run_list(funcp->body);
+	_exit(n);
+# else
+//?	close_saved_fds_and_FILE_fds();
+
+//TODO: check whether "true | func_with_return" works
+
+	re_execute_shell(to_free,
+			funcp->body_as_string,
+			G.global_argv[0],
+			argv + 1,
+			NULL);
+# endif
+}
+
+static int run_function(const struct function *funcp, char **argv)
+{
+	int rc;
+	save_arg_t sv;
+	smallint sv_flg;
+
+	save_and_replace_G_args(&sv, argv);
+
+	/* "We are in function, ok to use return" */
+	sv_flg = G_flag_return_in_progress;
+	G_flag_return_in_progress = -1;
+
+	/* Make "local" variables properly shadow previous ones */
+	IF_HUSH_LOCAL(enter_var_nest_level();)
+	IF_HUSH_LOCAL(G.func_nest_level++;)
+
+	/* On MMU, funcp->body is always non-NULL */
+# if !BB_MMU
+	if (!funcp->body) {
+		/* Function defined by -F */
+		parse_and_run_string(funcp->body_as_string);
+		rc = G.last_exitcode;
+	} else
+# endif
+	{
+		rc = run_list(funcp->body);
+	}
+
+	IF_HUSH_LOCAL(G.func_nest_level--;)
+	IF_HUSH_LOCAL(leave_var_nest_level();)
+
+	G_flag_return_in_progress = sv_flg;
+# if ENABLE_HUSH_TRAP
+	debug_printf_exec("G.return_exitcode=-1\n");
+	G.return_exitcode = -1; /* invalidate stashed return value */
+# endif
+
+	restore_G_args(&sv, argv);
+
+	return rc;
+}
+#endif /* ENABLE_HUSH_FUNCTIONS */
+
+
+#if BB_MMU
+#define exec_builtin(to_free, x, argv) \
+	exec_builtin(x, argv)
+#else
+#define exec_builtin(to_free, x, argv) \
+	exec_builtin(to_free, argv)
+#endif
+static void exec_builtin(char ***to_free,
+		const struct built_in_command *x,
+		char **argv) NORETURN;
+static void exec_builtin(char ***to_free,
+		const struct built_in_command *x,
+		char **argv)
+{
+#if BB_MMU
+	int rcode;
+//?	close_saved_fds_and_FILE_fds();
+	rcode = x->b_function(argv);
+	fflush_all();
+	_exit(rcode);
+#else
+	fflush_all();
+	/* On NOMMU, we must never block!
+	 * Example: { sleep 99 | read line; } & echo Ok
+	 */
+	re_execute_shell(to_free,
+			argv[0],
+			G.global_argv[0],
+			G.global_argv + 1,
+			argv);
+#endif
+}
+
+
+static void execvp_or_die(char **argv) NORETURN;
+static void execvp_or_die(char **argv)
+{
+	int e;
+	debug_printf_exec("execing '%s'\n", argv[0]);
+	/* Don't propagate SIG_IGN to the child */
+	if (SPECIAL_JOBSTOP_SIGS != 0)
+		switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
+	execvp(argv[0], argv);
+	e = 2;
+	if (errno == EACCES) e = 126;
+	if (errno == ENOENT) e = 127;
+	bb_perror_msg("can't execute '%s'", argv[0]);
+	_exit(e);
+}
+
+#if ENABLE_HUSH_MODE_X
+static void x_mode_print_optionally_squoted(const char *str)
+{
+	unsigned len;
+	const char *cp;
+
+	cp = str;
+
+	/* the set of chars which-cause-string-to-be-squoted mimics bash */
+	/* test a char with: bash -c 'set -x; echo "CH"' */
+	if (str[strcspn(str, "\\\"'`$(){}[]<>;#&|~*?!^"
+			" " "\001\002\003\004\005\006\007"
+			"\010\011\012\013\014\015\016\017"
+			"\020\021\022\023\024\025\026\027"
+			"\030\031\032\033\034\035\036\037"
+			)
+		] == '\0'
+	) {
+		/* string has no special chars */
+		x_mode_addstr(str);
+		return;
+	}
+
+	cp = str;
+	for (;;) {
+		/* print '....' up to EOL or first squote */
+		len = (int)(strchrnul(cp, '\'') - cp);
+		if (len != 0) {
+			x_mode_addchr('\'');
+			x_mode_addblock(cp, len);
+			x_mode_addchr('\'');
+			cp += len;
+		}
+		if (*cp == '\0')
+			break;
+		/* string contains squote(s), print them as \' */
+		x_mode_addchr('\\');
+		x_mode_addchr('\'');
+		cp++;
+	}
+}
+static void dump_cmd_in_x_mode(char **argv)
+{
+	if (G_x_mode && argv) {
+		unsigned n;
+
+		/* "+[+++...][ cmd...]\n\0" */
+		x_mode_prefix();
+		n = 0;
+		while (argv[n]) {
+			x_mode_addchr(' ');
+			if (argv[n][0] == '\0') {
+				x_mode_addchr('\'');
+				x_mode_addchr('\'');
+			} else {
+				x_mode_print_optionally_squoted(argv[n]);
+			}
+			n++;
+		}
+		x_mode_flush();
+	}
+}
+#else
+# define dump_cmd_in_x_mode(argv) ((void)0)
+#endif
+
+#if ENABLE_HUSH_COMMAND
+static void if_command_vV_print_and_exit(char opt_vV, char *cmd, const char *explanation)
+{
+	char *to_free;
+
+	if (!opt_vV)
+		return;
+
+	to_free = NULL;
+	if (!explanation) {
+		char *path = getenv("PATH");
+		explanation = to_free = find_executable(cmd, &path); /* path == NULL is ok */
+		if (!explanation)
+			_exit(1); /* PROG was not found */
+		if (opt_vV != 'V')
+			cmd = to_free; /* -v PROG prints "/path/to/PROG" */
+	}
+	printf((opt_vV == 'V') ? "%s is %s\n" : "%s\n", cmd, explanation);
+	free(to_free);
+	fflush_all();
+	_exit(0);
+}
+#else
+# define if_command_vV_print_and_exit(a,b,c) ((void)0)
+#endif
+
+#if BB_MMU
+#define pseudo_exec_argv(nommu_save, argv, assignment_cnt, argv_expanded) \
+	pseudo_exec_argv(argv, assignment_cnt, argv_expanded)
+#define pseudo_exec(nommu_save, command, argv_expanded) \
+	pseudo_exec(command, argv_expanded)
+#endif
+
+/* Called after [v]fork() in run_pipe, or from builtin_exec.
+ * Never returns.
+ * Don't exit() here.  If you don't exec, use _exit instead.
+ * The at_exit handlers apparently confuse the calling process,
+ * in particular stdin handling. Not sure why? -- because of vfork! (vda)
+ */
+static void pseudo_exec_argv(nommu_save_t *nommu_save,
+		char **argv, int assignment_cnt,
+		char **argv_expanded) NORETURN;
+static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
+		char **argv, int assignment_cnt,
+		char **argv_expanded)
+{
+	const struct built_in_command *x;
+	struct variable **sv_shadowed;
+	char **new_env;
+	IF_HUSH_COMMAND(char opt_vV = 0;)
+	IF_HUSH_FUNCTIONS(const struct function *funcp;)
+
+	new_env = expand_assignments(argv, assignment_cnt);
+	dump_cmd_in_x_mode(new_env);
+
+	if (!argv[assignment_cnt]) {
+		/* Case when we are here: ... | var=val | ...
+		 * (note that we do not exit early, i.e., do not optimize out
+		 * expand_assignments(): think about ... | var=`sleep 1` | ...
+		 */
+		free_strings(new_env);
+		_exit(EXIT_SUCCESS);
+	}
+
+	sv_shadowed = G.shadowed_vars_pp;
+#if BB_MMU
+	G.shadowed_vars_pp = NULL; /* "don't save, free them instead" */
+#else
+	G.shadowed_vars_pp = &nommu_save->old_vars;
+	G.var_nest_level++;
+#endif
+	set_vars_and_save_old(new_env);
+	G.shadowed_vars_pp = sv_shadowed;
+
+	if (argv_expanded) {
+		argv = argv_expanded;
+	} else {
+		argv = expand_strvec_to_strvec(argv + assignment_cnt);
+#if !BB_MMU
+		nommu_save->argv = argv;
+#endif
+	}
+	dump_cmd_in_x_mode(argv);
+
+#if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
+	if (strchr(argv[0], '/') != NULL)
+		goto skip;
+#endif
+
+#if ENABLE_HUSH_FUNCTIONS
+	/* Check if the command matches any functions (this goes before bltins) */
+	funcp = find_function(argv[0]);
+	if (funcp)
+		exec_function(&nommu_save->argv_from_re_execing, funcp, argv);
+#endif
+
+#if ENABLE_HUSH_COMMAND
+	/* "command BAR": run BAR without looking it up among functions
+	 * "command -v BAR": print "BAR" or "/path/to/BAR"; or exit 1
+	 * "command -V BAR": print "BAR is {a function,a shell builtin,/path/to/BAR}"
+	 */
+	while (strcmp(argv[0], "command") == 0 && argv[1]) {
+		char *p;
+
+		argv++;
+		p = *argv;
+		if (p[0] != '-' || !p[1])
+			continue; /* bash allows "command command command [-OPT] BAR" */
+
+		for (;;) {
+			p++;
+			switch (*p) {
+			case '\0':
+				argv++;
+				p = *argv;
+				if (p[0] != '-' || !p[1])
+					goto after_opts;
+				continue; /* next arg is also -opts, process it too */
+			case 'v':
+			case 'V':
+				opt_vV = *p;
+				continue;
+			default:
+				bb_error_msg_and_die("%s: %s: invalid option", "command", argv[0]);
+			}
+		}
+	}
+ after_opts:
+# if ENABLE_HUSH_FUNCTIONS
+	if (opt_vV && find_function(argv[0]))
+		if_command_vV_print_and_exit(opt_vV, argv[0], "a function");
+# endif
+#endif
+
+	/* Check if the command matches any of the builtins.
+	 * Depending on context, this might be redundant.  But it's
+	 * easier to waste a few CPU cycles than it is to figure out
+	 * if this is one of those cases.
+	 */
+	/* Why "BB_MMU ? :" difference in logic? -
+	 * On NOMMU, it is more expensive to re-execute shell
+	 * just in order to run echo or test builtin.
+	 * It's better to skip it here and run corresponding
+	 * non-builtin later. */
+	x = BB_MMU ? find_builtin(argv[0]) : find_builtin1(argv[0]);
+	if (x) {
+		if_command_vV_print_and_exit(opt_vV, argv[0], "a shell builtin");
+		exec_builtin(&nommu_save->argv_from_re_execing, x, argv);
+	}
+
+#if ENABLE_FEATURE_SH_STANDALONE
+	/* Check if the command matches any busybox applets */
+	{
+		int a = find_applet_by_name(argv[0]);
+		if (a >= 0) {
+			if_command_vV_print_and_exit(opt_vV, argv[0], "an applet");
+# if BB_MMU /* see above why on NOMMU it is not allowed */
+			if (APPLET_IS_NOEXEC(a)) {
+				/* Do not leak open fds from opened script files etc.
+				 * Testcase: interactive "ls -l /proc/self/fd"
+				 * should not show tty fd open.
+				 */
+				close_saved_fds_and_FILE_fds();
+//FIXME: should also close saved redir fds
+//This casuses test failures in
+//redir_children_should_not_see_saved_fd_2.tests
+//redir_children_should_not_see_saved_fd_3.tests
+//if you replace "busybox find" with just "find" in them
+				/* Without this, "rm -i FILE" can't be ^C'ed: */
+				switch_off_special_sigs(G.special_sig_mask);
+				debug_printf_exec("running applet '%s'\n", argv[0]);
+				run_noexec_applet_and_exit(a, argv[0], argv);
+			}
+# endif
+			/* Re-exec ourselves */
+			debug_printf_exec("re-execing applet '%s'\n", argv[0]);
+			/* Don't propagate SIG_IGN to the child */
+			if (SPECIAL_JOBSTOP_SIGS != 0)
+				switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
+			execv(bb_busybox_exec_path, argv);
+			/* If they called chroot or otherwise made the binary no longer
+			 * executable, fall through */
+		}
+	}
+#endif
+
+#if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
+ skip:
+#endif
+	if_command_vV_print_and_exit(opt_vV, argv[0], NULL);
+	execvp_or_die(argv);
+}
+
+/* Called after [v]fork() in run_pipe
+ */
+static void pseudo_exec(nommu_save_t *nommu_save,
+		struct command *command,
+		char **argv_expanded) NORETURN;
+static void pseudo_exec(nommu_save_t *nommu_save,
+		struct command *command,
+		char **argv_expanded)
+{
+#if ENABLE_HUSH_FUNCTIONS
+	if (command->cmd_type == CMD_FUNCDEF) {
+		/* Ignore funcdefs in pipes:
+		 * true | f() { cmd }
+		 */
+		_exit(0);
+	}
+#endif
+
+	if (command->argv) {
+		pseudo_exec_argv(nommu_save, command->argv,
+				command->assignment_cnt, argv_expanded);
+	}
+
+	if (command->group) {
+		/* Cases when we are here:
+		 * ( list )
+		 * { list } &
+		 * ... | ( list ) | ...
+		 * ... | { list } | ...
+		 */
+#if BB_MMU
+		int rcode;
+		debug_printf_exec("pseudo_exec: run_list\n");
+		reset_traps_to_defaults();
+		rcode = run_list(command->group);
+		/* OK to leak memory by not calling free_pipe_list,
+		 * since this process is about to exit */
+		_exit(rcode);
+#else
+		re_execute_shell(&nommu_save->argv_from_re_execing,
+				command->group_as_string,
+				G.global_argv[0],
+				G.global_argv + 1,
+				NULL);
+#endif
+	}
+
+	/* Case when we are here: ... | >file */
+	debug_printf_exec("pseudo_exec'ed null command\n");
+	_exit(EXIT_SUCCESS);
+}
+
+#if ENABLE_HUSH_JOB
+static const char *get_cmdtext(struct pipe *pi)
+{
+	char **argv;
+	char *p;
+	int len;
+
+	/* This is subtle. ->cmdtext is created only on first backgrounding.
+	 * (Think "cat, <ctrl-z>, fg, <ctrl-z>, fg, <ctrl-z>...." here...)
+	 * On subsequent bg argv is trashed, but we won't use it */
+	if (pi->cmdtext)
+		return pi->cmdtext;
+
+	argv = pi->cmds[0].argv;
+	if (!argv) {
+		pi->cmdtext = xzalloc(1);
+		return pi->cmdtext;
+	}
+	len = 0;
+	do {
+		len += strlen(*argv) + 1;
+	} while (*++argv);
+	p = xmalloc(len);
+	pi->cmdtext = p;
+	argv = pi->cmds[0].argv;
+	do {
+		p = stpcpy(p, *argv);
+		*p++ = ' ';
+	} while (*++argv);
+	p[-1] = '\0';
+	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;
+	while ((job = *jobp) != NULL) {
+		if (job->jobid > i)
+			i = job->jobid;
+		jobp = &job->next;
+	}
+	pi->jobid = i + 1;
+
+	/* 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 */
+	for (i = 0; i < pi->num_cmds; i++) {
+		job->cmds[i].pid = pi->cmds[i].pid;
+		/* all other fields are not used and stay zero */
+	}
+	job->cmdtext = xstrdup(get_cmdtext(pi));
+
+	if (G_interactive_fd)
+		printf("[%u] %u %s\n", job->jobid, (unsigned)job->cmds[0].pid, job->cmdtext);
+	G.last_jobid = job->jobid;
+}
+#endif /* JOB */
+
+static int job_exited_or_stopped(struct pipe *pi)
+{
+	int rcode, i;
+
+	if (pi->alive_cmds != pi->stopped_cmds)
+		return -1;
+
+	/* All processes in fg pipe have exited or stopped */
+	rcode = 0;
+	i = pi->num_cmds;
+	while (--i >= 0) {
+		rcode = pi->cmds[i].cmd_exitcode;
+		/* usually last process gives overall exitstatus,
+		 * but with "set -o pipefail", last *failed* process does */
+		if (G.o_opt[OPT_O_PIPEFAIL] == 0 || rcode != 0)
+			break;
+	}
+	IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+	return rcode;
+}
+
+static int process_wait_result(struct pipe *fg_pipe, pid_t childpid, int status)
+{
+#if ENABLE_HUSH_JOB
+	struct pipe *pi;
+#endif
+	int i, dead;
+
+	dead = WIFEXITED(status) || WIFSIGNALED(status);
+
+#if DEBUG_JOBS
+	if (WIFSTOPPED(status))
+		debug_printf_jobs("pid %d stopped by sig %d (exitcode %d)\n",
+				childpid, WSTOPSIG(status), WEXITSTATUS(status));
+	if (WIFSIGNALED(status))
+		debug_printf_jobs("pid %d killed by sig %d (exitcode %d)\n",
+				childpid, WTERMSIG(status), WEXITSTATUS(status));
+	if (WIFEXITED(status))
+		debug_printf_jobs("pid %d exited, exitcode %d\n",
+				childpid, WEXITSTATUS(status));
+#endif
+	/* Were we asked to wait for a fg pipe? */
+	if (fg_pipe) {
+		i = fg_pipe->num_cmds;
+
+		while (--i >= 0) {
+			int rcode;
+
+			debug_printf_jobs("check pid %d\n", fg_pipe->cmds[i].pid);
+			if (fg_pipe->cmds[i].pid != childpid)
+				continue;
+			if (dead) {
+				int ex;
+				fg_pipe->cmds[i].pid = 0;
+				fg_pipe->alive_cmds--;
+				ex = WEXITSTATUS(status);
+				/* bash prints killer signal's name for *last*
+				 * process in pipe (prints just newline for SIGINT/SIGPIPE).
+				 * Mimic this. Example: "sleep 5" + (^\ or kill -QUIT)
+				 */
+				if (WIFSIGNALED(status)) {
+					int sig = WTERMSIG(status);
+#if ENABLE_HUSH_JOB
+					if (G.run_list_level == 1
+					/* ^^^^^ Do not print in nested contexts, example:
+					 * echo `sleep 1; sh -c 'kill -9 $$'` - prints "137", NOT "Killed 137"
+					 */
+					 && i == fg_pipe->num_cmds-1
+					) {
+						/* strsignal() is for bash compat. ~600 bloat versus bbox's get_signame() */
+						puts(sig == SIGINT || sig == SIGPIPE ? "" : strsignal(sig));
+					}
+#endif
+					/* TODO: if (WCOREDUMP(status)) + " (core dumped)"; */
+					/* MIPS has 128 sigs (1..128), if sig==128,
+					 * 128 + sig would result in exitcode 256 -> 0!
+					 */
+					ex = 128 | sig;
+				}
+				fg_pipe->cmds[i].cmd_exitcode = ex;
+			} else {
+				fg_pipe->stopped_cmds++;
+			}
+			debug_printf_jobs("fg_pipe: alive_cmds %d stopped_cmds %d\n",
+					fg_pipe->alive_cmds, fg_pipe->stopped_cmds);
+			rcode = job_exited_or_stopped(fg_pipe);
+			if (rcode >= 0) {
+/* Note: *non-interactive* bash does not continue if all processes in fg pipe
+ * are stopped. Testcase: "cat | cat" in a script (not on command line!)
+ * and "killall -STOP cat" */
+				if (G_interactive_fd) {
+#if ENABLE_HUSH_JOB
+					if (fg_pipe->alive_cmds != 0)
+						insert_job_into_table(fg_pipe);
+#endif
+					return rcode;
+				}
+				if (fg_pipe->alive_cmds == 0)
+					return rcode;
+			}
+			/* There are still running processes in the fg_pipe */
+			return -1;
+		}
+		/* It wasn't in fg_pipe, look for process in bg pipes */
+	}
+
+#if ENABLE_HUSH_JOB
+	/* We were asked to wait for bg or orphaned children */
+	/* No need to remember exitcode in this case */
+	for (pi = G.job_list; pi; pi = pi->next) {
+		for (i = 0; i < pi->num_cmds; i++) {
+			if (pi->cmds[i].pid == childpid)
+				goto found_pi_and_prognum;
+		}
+	}
+	/* Happens when shell is used as init process (init=/bin/sh) */
+	debug_printf("checkjobs: pid %d was not in our list!\n", childpid);
+	return -1; /* this wasn't a process from fg_pipe */
+
+ found_pi_and_prognum:
+	if (dead) {
+		/* child exited */
+		int rcode = WEXITSTATUS(status);
+		if (WIFSIGNALED(status))
+			/* NB: not 128 + sig, MIPS has sig 128 */
+			rcode = 128 | WTERMSIG(status);
+		pi->cmds[i].cmd_exitcode = rcode;
+		if (G.last_bg_pid == pi->cmds[i].pid)
+			G.last_bg_pid_exitcode = rcode;
+		pi->cmds[i].pid = 0;
+		pi->alive_cmds--;
+		if (!pi->alive_cmds) {
+# if ENABLE_HUSH_BASH_COMPAT
+			G.dead_job_exitcode = job_exited_or_stopped(pi);
+# endif
+			if (G_interactive_fd) {
+				printf(JOB_STATUS_FORMAT, pi->jobid,
+						"Done", pi->cmdtext);
+				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 */
+		pi->stopped_cmds++;
+	}
+#endif
+	return -1; /* this wasn't a process from fg_pipe */
+}
+
+/* Check to see if any processes have exited -- if they have,
+ * figure out why and see if a job has completed.
+ *
+ * If non-NULL fg_pipe: wait for its completion or stop.
+ * Return its exitcode or zero if stopped.
+ *
+ * Alternatively (fg_pipe == NULL, waitfor_pid != 0):
+ * waitpid(WNOHANG), if waitfor_pid exits or stops, return exitcode+1,
+ * else return <0 if waitpid errors out (e.g. ECHILD: nothing to wait for)
+ * or 0 if no children changed status.
+ *
+ * Alternatively (fg_pipe == NULL, waitfor_pid == 0),
+ * return <0 if waitpid errors out (e.g. ECHILD: nothing to wait for)
+ * or 0 if no children changed status.
+ */
+static int checkjobs(struct pipe *fg_pipe, pid_t waitfor_pid)
+{
+	int attributes;
+	int status;
+	int rcode = 0;
+
+	debug_printf_jobs("checkjobs %p\n", fg_pipe);
+
+	attributes = WUNTRACED;
+	if (fg_pipe == NULL)
+		attributes |= WNOHANG;
+
+	errno = 0;
+#if ENABLE_HUSH_FAST
+	if (G.handled_SIGCHLD == G.count_SIGCHLD) {
+//bb_error_msg("[%d] checkjobs: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d children?:%d fg_pipe:%p",
+//getpid(), G.count_SIGCHLD, G.handled_SIGCHLD, G.we_have_children, fg_pipe);
+		/* There was neither fork nor SIGCHLD since last waitpid */
+		/* Avoid doing waitpid syscall if possible */
+		if (!G.we_have_children) {
+			errno = ECHILD;
+			return -1;
+		}
+		if (fg_pipe == NULL) { /* is WNOHANG set? */
+			/* We have children, but they did not exit
+			 * or stop yet (we saw no SIGCHLD) */
+			return 0;
+		}
+		/* else: !WNOHANG, waitpid will block, can't short-circuit */
+	}
+#endif
+
+/* Do we do this right?
+ * bash-3.00# sleep 20 | false
+ * <ctrl-Z pressed>
+ * [3]+  Stopped          sleep 20 | false
+ * bash-3.00# echo $?
+ * 1   <========== bg pipe is not fully done, but exitcode is already known!
+ * [hush 1.14.0: yes we do it right]
+ */
+	while (1) {
+		pid_t childpid;
+#if ENABLE_HUSH_FAST
+		int i;
+		i = G.count_SIGCHLD;
+#endif
+		childpid = waitpid(-1, &status, attributes);
+		if (childpid <= 0) {
+			if (childpid && errno != ECHILD)
+				bb_simple_perror_msg("waitpid");
+#if ENABLE_HUSH_FAST
+			else { /* Until next SIGCHLD, waitpid's are useless */
+				G.we_have_children = (childpid == 0);
+				G.handled_SIGCHLD = i;
+//bb_error_msg("[%d] checkjobs: waitpid returned <= 0, G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+			}
+#endif
+			/* ECHILD (no children), or 0 (no change in children status) */
+			rcode = childpid;
+			break;
+		}
+		rcode = process_wait_result(fg_pipe, childpid, status);
+		if (rcode >= 0) {
+			/* fg_pipe exited or stopped */
+			break;
+		}
+		if (childpid == waitfor_pid) { /* "wait PID" */
+			debug_printf_exec("childpid==waitfor_pid:%d status:0x%08x\n", childpid, status);
+			rcode = WEXITSTATUS(status);
+			if (WIFSIGNALED(status))
+				rcode = 128 | WTERMSIG(status);
+			if (WIFSTOPPED(status))
+				/* bash: "cmd & wait $!" and cmd stops: $? = 128 | stopsig */
+				rcode = 128 | WSTOPSIG(status);
+			rcode++;
+			break; /* "wait PID" called us, give it exitcode+1 */
+		}
+#if ENABLE_HUSH_BASH_COMPAT
+		if (-1 == waitfor_pid /* "wait -n" (wait for any one job) */
+		 && G.dead_job_exitcode >= 0 /* some job did finish */
+		) {
+			debug_printf_exec("waitfor_pid:-1\n");
+			rcode = G.dead_job_exitcode + 1;
+			break;
+		}
+#endif
+		/* This wasn't one of our processes, or */
+		/* fg_pipe still has running processes, do waitpid again */
+	} /* while (waitpid succeeds)... */
+
+	return rcode;
+}
+
+#if ENABLE_HUSH_JOB
+static int checkjobs_and_fg_shell(struct pipe *fg_pipe)
+{
+	pid_t p;
+	int rcode = checkjobs(fg_pipe, 0 /*(no pid to wait for)*/);
+	if (G_saved_tty_pgrp) {
+		/* Job finished, move the shell to the foreground */
+		p = getpgrp(); /* our process group id */
+		debug_printf_jobs("fg'ing ourself: getpgrp()=%d\n", (int)p);
+		tcsetpgrp(G_interactive_fd, p);
+	}
+	return rcode;
+}
+#endif
+
+/* Start all the jobs, but don't wait for anything to finish.
+ * See checkjobs().
+ *
+ * Return code is normally -1, when the caller has to wait for children
+ * to finish to determine the exit status of the pipe.  If the pipe
+ * is a simple builtin command, however, the action is done by the
+ * time run_pipe returns, and the exit code is provided as the
+ * return value.
+ *
+ * Returns -1 only if started some children. IOW: we have to
+ * mask out retvals of builtins etc with 0xff!
+ *
+ * The only case when we do not need to [v]fork is when the pipe
+ * is single, non-backgrounded, non-subshell command. Examples:
+ * cmd ; ...   { list } ; ...
+ * cmd && ...  { list } && ...
+ * cmd || ...  { list } || ...
+ * If it is, then we can run cmd as a builtin, NOFORK,
+ * or (if SH_STANDALONE) an applet, and we can run the { list }
+ * with run_list. If it isn't one of these, we fork and exec cmd.
+ *
+ * Cases when we must fork:
+ * non-single:   cmd | cmd
+ * backgrounded: cmd &     { list } &
+ * subshell:     ( list ) [&]
+ */
+#if !ENABLE_HUSH_MODE_X
+#define redirect_and_varexp_helper(command, sqp, argv_expanded) \
+	redirect_and_varexp_helper(command, sqp)
+#endif
+static int redirect_and_varexp_helper(
+		struct command *command,
+		struct squirrel **sqp,
+		char **argv_expanded)
+{
+	/* Assignments occur before redirects. Try:
+	 * a=`sleep 1` sleep 2 3>/qwe/rty
+	 */
+
+	char **new_env = expand_assignments(command->argv, command->assignment_cnt);
+	dump_cmd_in_x_mode(new_env);
+	dump_cmd_in_x_mode(argv_expanded);
+	/* this takes ownership of new_env[i] elements, and frees new_env: */
+	set_vars_and_save_old(new_env);
+
+	return setup_redirects(command, sqp);
+}
+static NOINLINE int run_pipe(struct pipe *pi)
+{
+	static const char *const null_ptr = NULL;
+
+	int cmd_no;
+	int next_infd;
+	struct command *command;
+	char **argv_expanded;
+	char **argv;
+	struct squirrel *squirrel = NULL;
+	int rcode;
+
+	debug_printf_exec("run_pipe start: members:%d\n", pi->num_cmds);
+	debug_enter();
+
+	/* Testcase: set -- q w e; (IFS='' echo "$*"; IFS=''; echo "$*"); echo "$*"
+	 * Result should be 3 lines: q w e, qwe, q w e
+	 */
+	if (G.ifs_whitespace != G.ifs)
+		free(G.ifs_whitespace);
+	G.ifs = get_local_var_value("IFS");
+	if (G.ifs) {
+		char *p;
+		G.ifs_whitespace = (char*)G.ifs;
+		p = skip_whitespace(G.ifs);
+		if (*p) {
+			/* Not all $IFS is whitespace */
+			char *d;
+			int len = p - G.ifs;
+			p = skip_non_whitespace(p);
+			G.ifs_whitespace = xmalloc(len + strlen(p) + 1); /* can overestimate */
+			d = mempcpy(G.ifs_whitespace, G.ifs, len);
+			while (*p) {
+				if (isspace(*p))
+					*d++ = *p;
+				p++;
+			}
+			*d = '\0';
+		}
+	} else {
+		G.ifs = defifs;
+		G.ifs_whitespace = (char*)G.ifs;
+	}
+
+	IF_HUSH_JOB(pi->pgrp = -1;)
+	pi->stopped_cmds = 0;
+	command = &pi->cmds[0];
+	argv_expanded = NULL;
+
+	if (pi->num_cmds != 1
+	 || pi->followup == PIPE_BG
+	 || command->cmd_type == CMD_SUBSHELL
+	) {
+		goto must_fork;
+	}
+
+	pi->alive_cmds = 1;
+
+	debug_printf_exec(": group:%p argv:'%s'\n",
+		command->group, command->argv ? command->argv[0] : "NONE");
+
+	if (command->group) {
+#if ENABLE_HUSH_FUNCTIONS
+		if (command->cmd_type == CMD_FUNCDEF) {
+			/* "executing" func () { list } */
+			struct function *funcp;
+
+			funcp = new_function(command->argv[0]);
+			/* funcp->name is already set to argv[0] */
+			funcp->body = command->group;
+# if !BB_MMU
+			funcp->body_as_string = command->group_as_string;
+			command->group_as_string = NULL;
+# endif
+			command->group = NULL;
+			command->argv[0] = NULL;
+			debug_printf_exec("cmd %p has child func at %p\n", command, funcp);
+			funcp->parent_cmd = command;
+			command->child_func = funcp;
+
+			debug_printf_exec("run_pipe: return EXIT_SUCCESS\n");
+			debug_leave();
+			return EXIT_SUCCESS;
+		}
+#endif
+		/* { list } */
+		debug_printf_exec("non-subshell group\n");
+		rcode = 1; /* exitcode if redir failed */
+		if (setup_redirects(command, &squirrel) == 0) {
+			debug_printf_exec(": run_list\n");
+//FIXME: we need to pass squirrel down into run_list()
+//for SH_STANDALONE case, or else this construct:
+// { find /proc/self/fd; true; } >FILE; cmd2
+//has no way of closing saved fd#1 for "find",
+//and in SH_STANDALONE mode, "find" is not execed,
+//therefore CLOEXEC on saved fd does not help.
+			rcode = run_list(command->group) & 0xff;
+		}
+		restore_redirects(squirrel);
+		IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+		debug_leave();
+		debug_printf_exec("run_pipe: return %d\n", rcode);
+		return rcode;
+	}
+
+	argv = command->argv ? command->argv : (char **) &null_ptr;
+	{
+		const struct built_in_command *x;
+		IF_HUSH_FUNCTIONS(const struct function *funcp;)
+		IF_NOT_HUSH_FUNCTIONS(enum { funcp = 0 };)
+		struct variable **sv_shadowed;
+		struct variable *old_vars;
+
+#if ENABLE_HUSH_LINENO_VAR
+		G.execute_lineno = command->lineno;
+#endif
+
+		if (argv[command->assignment_cnt] == NULL) {
+			/* Assignments, but no command.
+			 * Ensure redirects take effect (that is, create files).
+			 * Try "a=t >file"
+			 */
+			unsigned i;
+			G.expand_exitcode = 0;
+ only_assignments:
+			rcode = setup_redirects(command, &squirrel);
+			restore_redirects(squirrel);
+
+			/* Set shell variables */
+			i = 0;
+			while (i < command->assignment_cnt) {
+				char *p = expand_string_to_string(argv[i],
+						EXP_FLAG_ESC_GLOB_CHARS,
+						/*unbackslash:*/ 1
+				);
+#if ENABLE_HUSH_MODE_X
+				if (G_x_mode) {
+					char *eq;
+					if (i == 0)
+						x_mode_prefix();
+					x_mode_addchr(' ');
+					eq = strchrnul(p, '=');
+					if (*eq) eq++;
+					x_mode_addblock(p, (eq - p));
+					x_mode_print_optionally_squoted(eq);
+					x_mode_flush();
+				}
+#endif
+				debug_printf_env("set shell var:'%s'->'%s'\n", *argv, p);
+				if (set_local_var(p, /*flag:*/ 0)) {
+					/* assignment to readonly var / putenv error? */
+					rcode = 1;
+				}
+				i++;
+			}
+			/* Redirect error sets $? to 1. Otherwise,
+			 * if evaluating assignment value set $?, retain it.
+			 * Else, clear $?:
+			 *  false; q=`exit 2`; echo $? - should print 2
+			 *  false; x=1; echo $? - should print 0
+			 * Because of the 2nd case, we can't just use G.last_exitcode.
+			 */
+			if (rcode == 0)
+				rcode = G.expand_exitcode;
+			IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+			debug_leave();
+			debug_printf_exec("run_pipe: return %d\n", rcode);
+			return rcode;
+		}
+
+		/* Expand the rest into (possibly) many strings each */
+#if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
+		if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB)
+			argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt);
+		else
+#endif
+#if defined(CMD_SINGLEWORD_NOGLOB)
+		if (command->cmd_type == CMD_SINGLEWORD_NOGLOB)
+			argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt);
+		else
+#endif
+			argv_expanded = expand_strvec_to_strvec(argv + command->assignment_cnt);
+
+		/* If someone gives us an empty string: `cmd with empty output` */
+		if (!argv_expanded[0]) {
+			free(argv_expanded);
+			/* `false` still has to set exitcode 1 */
+			G.expand_exitcode = G.last_exitcode;
+			goto only_assignments;
+		}
+
+		old_vars = NULL;
+		sv_shadowed = G.shadowed_vars_pp;
+
+		/* Check if argv[0] matches any functions (this goes before bltins) */
+		IF_HUSH_FUNCTIONS(funcp = find_function(argv_expanded[0]);)
+		IF_HUSH_FUNCTIONS(x = NULL;)
+		IF_HUSH_FUNCTIONS(if (!funcp))
+			x = find_builtin(argv_expanded[0]);
+		if (x || funcp) {
+			if (x && x->b_function == builtin_exec && argv_expanded[1] == NULL) {
+				debug_printf("exec with redirects only\n");
+				/*
+				 * Variable assignments are executed, but then "forgotten":
+				 *  a=`sleep 1;echo A` exec 3>&-; echo $a
+				 * sleeps, but prints nothing.
+				 */
+				enter_var_nest_level();
+				G.shadowed_vars_pp = &old_vars;
+				rcode = redirect_and_varexp_helper(command,
+					/*squirrel:*/ ERR_PTR,
+					argv_expanded
+				);
+				G.shadowed_vars_pp = sv_shadowed;
+				/* rcode=1 can be if redir file can't be opened */
+
+				goto clean_up_and_ret1;
+			}
+
+			/* Bump var nesting, or this will leak exported $a:
+			 * a=b true; env | grep ^a=
+			 */
+			enter_var_nest_level();
+			/* Collect all variables "shadowed" by helper
+			 * (IOW: old vars overridden by "var1=val1 var2=val2 cmd..." syntax)
+			 * into old_vars list:
+			 */
+			G.shadowed_vars_pp = &old_vars;
+			rcode = redirect_and_varexp_helper(command, &squirrel, argv_expanded);
+			if (rcode == 0) {
+				if (!funcp) {
+					/* Do not collect *to old_vars list* vars shadowed
+					 * by e.g. "local VAR" builtin (collect them
+					 * in the previously nested list instead):
+					 * don't want them to be restored immediately
+					 * after "local" completes.
+					 */
+					G.shadowed_vars_pp = sv_shadowed;
+
+					debug_printf_exec(": builtin '%s' '%s'...\n",
+						x->b_cmd, argv_expanded[1]);
+					fflush_all();
+					rcode = x->b_function(argv_expanded) & 0xff;
+					fflush_all();
+				}
+#if ENABLE_HUSH_FUNCTIONS
+				else {
+					debug_printf_exec(": function '%s' '%s'...\n",
+						funcp->name, argv_expanded[1]);
+					rcode = run_function(funcp, argv_expanded) & 0xff;
+					/*
+					 * But do collect *to old_vars list* vars shadowed
+					 * within function execution. To that end, restore
+					 * this pointer _after_ function run:
+					 */
+					G.shadowed_vars_pp = sv_shadowed;
+				}
+#endif
+			}
+		} else
+		if (ENABLE_FEATURE_SH_NOFORK && NUM_APPLETS > 1) {
+			int n = find_applet_by_name(argv_expanded[0]);
+			if (n < 0 || !APPLET_IS_NOFORK(n))
+				goto must_fork;
+
+			enter_var_nest_level();
+			/* Collect all variables "shadowed" by helper into old_vars list */
+			G.shadowed_vars_pp = &old_vars;
+			rcode = redirect_and_varexp_helper(command, &squirrel, argv_expanded);
+			G.shadowed_vars_pp = sv_shadowed;
+
+			if (rcode == 0) {
+				debug_printf_exec(": run_nofork_applet '%s' '%s'...\n",
+					argv_expanded[0], argv_expanded[1]);
+				/*
+				 * Note: signals (^C) can't interrupt here.
+				 * We remember them and they will be acted upon
+				 * after applet returns.
+				 * This makes applets which can run for a long time
+				 * and/or wait for user input ineligible for NOFORK:
+				 * for example, "yes" or "rm" (rm -i waits for input).
+				 */
+				rcode = run_nofork_applet(n, argv_expanded);
+			}
+		} else
+			goto must_fork;
+
+		restore_redirects(squirrel);
+ clean_up_and_ret1:
+		leave_var_nest_level();
+		add_vars(old_vars);
+
+		/*
+		 * Try "usleep 99999999" + ^C + "echo $?"
+		 * with FEATURE_SH_NOFORK=y.
+		 */
+		if (!funcp) {
+			/* It was builtin or nofork.
+			 * if this would be a real fork/execed program,
+			 * it should have died if a fatal sig was received.
+			 * But OTOH, there was no separate process,
+			 * the sig was sent to _shell_, not to non-existing
+			 * child.
+			 * Let's just handle ^C only, this one is obvious:
+			 * we aren't ok with exitcode 0 when ^C was pressed
+			 * during builtin/nofork.
+			 */
+			if (sigismember(&G.pending_set, SIGINT))
+				rcode = 128 | SIGINT;
+		}
+		free(argv_expanded);
+		IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+		debug_leave();
+		debug_printf_exec("run_pipe return %d\n", rcode);
+		return rcode;
+	}
+
+ must_fork:
+	/* NB: argv_expanded may already be created, and that
+	 * might include `cmd` runs! Do not rerun it! We *must*
+	 * use argv_expanded if it's non-NULL */
+
+	/* Going to fork a child per each pipe member */
+	pi->alive_cmds = 0;
+	next_infd = 0;
+
+	cmd_no = 0;
+	while (cmd_no < pi->num_cmds) {
+		struct fd_pair pipefds;
+#if !BB_MMU
+		int sv_var_nest_level = G.var_nest_level;
+		volatile nommu_save_t nommu_save;
+		nommu_save.old_vars = NULL;
+		nommu_save.argv = NULL;
+		nommu_save.argv_from_re_execing = NULL;
+#endif
+		command = &pi->cmds[cmd_no];
+		cmd_no++;
+		if (command->argv) {
+			debug_printf_exec(": pipe member '%s' '%s'...\n",
+					command->argv[0], command->argv[1]);
+		} else {
+			debug_printf_exec(": pipe member with no argv\n");
+		}
+
+		/* pipes are inserted between pairs of commands */
+		pipefds.rd = 0;
+		pipefds.wr = 1;
+		if (cmd_no < pi->num_cmds)
+			xpiped_pair(pipefds);
+
+#if ENABLE_HUSH_LINENO_VAR
+		G.execute_lineno = command->lineno;
+#endif
+
+		command->pid = BB_MMU ? fork() : vfork();
+		if (!command->pid) { /* child */
+#if ENABLE_HUSH_JOB
+			disable_restore_tty_pgrp_on_exit();
+			CLEAR_RANDOM_T(&G.random_gen); /* or else $RANDOM repeats in child */
+
+			/* Every child adds itself to new process group
+			 * with pgid == pid_of_first_child_in_pipe */
+			if (G.run_list_level == 1 && G_interactive_fd) {
+				pid_t pgrp;
+				pgrp = pi->pgrp;
+				if (pgrp < 0) /* true for 1st process only */
+					pgrp = getpid();
+				if (setpgid(0, pgrp) == 0
+				 && pi->followup != PIPE_BG
+				 && G_saved_tty_pgrp /* we have ctty */
+				) {
+					/* We do it in *every* child, not just first,
+					 * to avoid races */
+					tcsetpgrp(G_interactive_fd, pgrp);
+				}
+			}
+#endif
+			if (pi->alive_cmds == 0 && pi->followup == PIPE_BG) {
+				/* 1st cmd in backgrounded pipe
+				 * should have its stdin /dev/null'ed */
+				close(0);
+				if (open(bb_dev_null, O_RDONLY))
+					xopen("/", O_RDONLY);
+			} else {
+				xmove_fd(next_infd, 0);
+			}
+			xmove_fd(pipefds.wr, 1);
+			if (pipefds.rd > 1)
+				close(pipefds.rd);
+			/* Like bash, explicit redirects override pipes,
+			 * and the pipe fd (fd#1) is available for dup'ing:
+			 * "cmd1 2>&1 | cmd2": fd#1 is duped to fd#2, thus stderr
+			 * of cmd1 goes into pipe.
+			 */
+			if (setup_redirects(command, NULL)) {
+				/* Happens when redir file can't be opened:
+				 * $ hush -c 'echo FOO >&2 | echo BAR 3>/qwe/rty; echo BAZ'
+				 * FOO
+				 * hush: can't open '/qwe/rty': No such file or directory
+				 * BAZ
+				 * (echo BAR is not executed, it hits _exit(1) below)
+				 */
+				_exit(1);
+			}
+
+			/* Stores to nommu_save list of env vars putenv'ed
+			 * (NOMMU, on MMU we don't need that) */
+			/* cast away volatility... */
+			pseudo_exec((nommu_save_t*) &nommu_save, command, argv_expanded);
+			/* pseudo_exec() does not return */
+		}
+
+		/* parent or error */
+#if ENABLE_HUSH_FAST
+		G.count_SIGCHLD++;
+//bb_error_msg("[%d] fork in run_pipe: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+#endif
+		enable_restore_tty_pgrp_on_exit();
+#if !BB_MMU
+		/* Clean up after vforked child */
+		free(nommu_save.argv);
+		free(nommu_save.argv_from_re_execing);
+		G.var_nest_level = sv_var_nest_level;
+		remove_nested_vars();
+		add_vars(nommu_save.old_vars);
+#endif
+		free(argv_expanded);
+		argv_expanded = NULL;
+		if (command->pid < 0) { /* [v]fork failed */
+			/* Clearly indicate, was it fork or vfork */
+			bb_simple_perror_msg(BB_MMU ? "vfork"+1 : "vfork");
+		} else {
+			pi->alive_cmds++;
+#if ENABLE_HUSH_JOB
+			/* Second and next children need to know pid of first one */
+			if (pi->pgrp < 0)
+				pi->pgrp = command->pid;
+#endif
+		}
+
+		if (cmd_no > 1)
+			close(next_infd);
+		if (cmd_no < pi->num_cmds)
+			close(pipefds.wr);
+		/* Pass read (output) pipe end to next iteration */
+		next_infd = pipefds.rd;
+	}
+
+	if (!pi->alive_cmds) {
+		debug_leave();
+		debug_printf_exec("run_pipe return 1 (all forks failed, no children)\n");
+		return 1;
+	}
+
+	debug_leave();
+	debug_printf_exec("run_pipe return -1 (%u children started)\n", pi->alive_cmds);
+	return -1;
+}
+
+/* NB: called by pseudo_exec, and therefore must not modify any
+ * global data until exec/_exit (we can be a child after vfork!) */
+static int run_list(struct pipe *pi)
+{
+#if ENABLE_HUSH_CASE
+	char *case_word = NULL;
+#endif
+#if ENABLE_HUSH_LOOPS
+	struct pipe *loop_top = NULL;
+	char **for_lcur = NULL;
+	char **for_list = NULL;
+#endif
+	smallint last_followup;
+	smalluint rcode;
+#if ENABLE_HUSH_IF || ENABLE_HUSH_CASE
+	smalluint cond_code = 0;
+#else
+	enum { cond_code = 0 };
+#endif
+#if HAS_KEYWORDS
+	smallint rword;      /* RES_foo */
+	smallint last_rword; /* ditto */
+#endif
+
+	debug_printf_exec("run_list start lvl %d\n", G.run_list_level);
+	debug_enter();
+
+#if ENABLE_HUSH_LOOPS
+	/* Check syntax for "for" */
+	{
+		struct pipe *cpipe;
+		for (cpipe = pi; cpipe; cpipe = cpipe->next) {
+			if (cpipe->res_word != RES_FOR && cpipe->res_word != RES_IN)
+				continue;
+			/* current word is FOR or IN (BOLD in comments below) */
+			if (cpipe->next == NULL) {
+				syntax_error("malformed for");
+				debug_leave();
+				debug_printf_exec("run_list lvl %d return 1\n", G.run_list_level);
+				return 1;
+			}
+			/* "FOR v; do ..." and "for v IN a b; do..." are ok */
+			if (cpipe->next->res_word == RES_DO)
+				continue;
+			/* next word is not "do". It must be "in" then ("FOR v in ...") */
+			if (cpipe->res_word == RES_IN /* "for v IN a b; not_do..."? */
+			 || cpipe->next->res_word != RES_IN /* FOR v not_do_and_not_in..."? */
+			) {
+				syntax_error("malformed for");
+				debug_leave();
+				debug_printf_exec("run_list lvl %d return 1\n", G.run_list_level);
+				return 1;
+			}
+		}
+	}
+#endif
+
+	/* Past this point, all code paths should jump to ret: label
+	 * in order to return, no direct "return" statements please.
+	 * This helps to ensure that no memory is leaked. */
+
+#if ENABLE_HUSH_JOB
+	G.run_list_level++;
+#endif
+
+#if HAS_KEYWORDS
+	rword = RES_NONE;
+	last_rword = RES_XXXX;
+#endif
+	last_followup = PIPE_SEQ;
+	rcode = G.last_exitcode;
+
+	/* 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 (G_flag_return_in_progress == 1)
+			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 ENABLE_HUSH_IF
+		    rword == RES_IF || rword == RES_ELIF ||
+#endif
+		    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 */
+		) {
+			/* start of a loop: remember where loop starts */
+			loop_top = pi;
+			G.depth_of_loop++;
+		}
+#endif
+		/* Still in the same "if...", "then..." or "do..." branch? */
+		if (IF_HAS_KEYWORDS(rword == last_rword &&) 1) {
+			if ((rcode == 0 && last_followup == PIPE_OR)
+			 || (rcode != 0 && last_followup == PIPE_AND)
+			) {
+				/* It is "<true> || CMD" or "<false> && CMD"
+				 * and we should not execute CMD */
+				debug_printf_exec("skipped cmd because of || or &&\n");
+				last_followup = pi->followup;
+				goto dont_check_jobs_but_continue;
+			}
+		}
+		last_followup = pi->followup;
+		IF_HAS_KEYWORDS(last_rword = rword;)
+#if ENABLE_HUSH_IF
+		if (cond_code) {
+			if (rword == RES_THEN) {
+				/* if false; then ... fi has exitcode 0! */
+				G.last_exitcode = rcode = EXIT_SUCCESS;
+				/* "if <false> THEN cmd": skip cmd */
+				continue;
+			}
+		} else {
+			if (rword == RES_ELSE || rword == RES_ELIF) {
+				/* "if <true> then ... ELSE/ELIF cmd":
+				 * skip cmd and all following ones */
+				break;
+			}
+		}
+#endif
+#if ENABLE_HUSH_LOOPS
+		if (rword == RES_FOR) { /* && pi->num_cmds - always == 1 */
+			if (!for_lcur) {
+				/* first loop through for */
+
+				static const char encoded_dollar_at[] ALIGN1 = {
+					SPECIAL_VAR_SYMBOL, '@' | 0x80, SPECIAL_VAR_SYMBOL, '\0'
+				}; /* encoded representation of "$@" */
+				static const char *const encoded_dollar_at_argv[] = {
+					encoded_dollar_at, NULL
+				}; /* argv list with one element: "$@" */
+				char **vals;
+
+				G.last_exitcode = rcode = EXIT_SUCCESS;
+				vals = (char**)encoded_dollar_at_argv;
+				if (pi->next->res_word == RES_IN) {
+					/* if no variable values after "in" we skip "for" */
+					if (!pi->next->cmds[0].argv) {
+						debug_printf_exec(": null FOR: exitcode EXIT_SUCCESS\n");
+						break;
+					}
+					vals = pi->next->cmds[0].argv;
+				} /* else: "for var; do..." -> assume "$@" list */
+				/* create list of variable values */
+				debug_print_strings("for_list made from", vals);
+				for_list = expand_strvec_to_strvec(vals);
+				for_lcur = for_list;
+				debug_print_strings("for_list", for_list);
+			}
+			if (!*for_lcur) {
+				/* "for" loop is over, clean up */
+				free(for_list);
+				for_list = NULL;
+				for_lcur = NULL;
+				break;
+			}
+			/* 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++), /*flag:*/ 0);
+			continue;
+		}
+		if (rword == RES_IN) {
+			continue; /* "for v IN list;..." - "in" has no cmds anyway */
+		}
+		if (rword == RES_DONE) {
+			continue; /* "done" has no cmds too */
+		}
+#endif
+#if ENABLE_HUSH_CASE
+		if (rword == RES_CASE) {
+			debug_printf_exec("CASE cond_code:%d\n", cond_code);
+			case_word = expand_string_to_string(pi->cmds->argv[0],
+				EXP_FLAG_ESC_GLOB_CHARS, /*unbackslash:*/ 1);
+			debug_printf_exec("CASE word1:'%s'\n", case_word);
+			//unbackslash(case_word);
+			//debug_printf_exec("CASE word2:'%s'\n", case_word);
+			continue;
+		}
+		if (rword == RES_MATCH) {
+			char **argv;
+
+			debug_printf_exec("MATCH cond_code:%d\n", cond_code);
+			if (!case_word) /* "case ... matched_word) ... WORD)": we executed selected branch, stop */
+				break;
+			/* all prev words didn't match, does this one match? */
+			argv = pi->cmds->argv;
+			while (*argv) {
+				char *pattern;
+				debug_printf_exec("expand_string_to_string('%s')\n", *argv);
+				pattern = expand_string_to_string(*argv,
+						EXP_FLAG_ESC_GLOB_CHARS,
+						/*unbackslash:*/ 0
+				);
+				/* TODO: which FNM_xxx flags to use? */
+				cond_code = (fnmatch(pattern, case_word, /*flags:*/ 0) != 0);
+				debug_printf_exec("fnmatch(pattern:'%s',str:'%s'):%d\n",
+						pattern, case_word, cond_code);
+				free(pattern);
+				if (cond_code == 0) {
+					/* match! we will execute this branch */
+					free(case_word);
+					case_word = NULL; /* make future "word)" stop */
+					break;
+				}
+				argv++;
+			}
+			continue;
+		}
+		if (rword == RES_CASE_BODY) { /* inside of a case branch */
+			debug_printf_exec("CASE_BODY cond_code:%d\n", cond_code);
+			if (cond_code != 0)
+				continue; /* not matched yet, skip this pipe */
+		}
+		if (rword == RES_ESAC) {
+			debug_printf_exec("ESAC cond_code:%d\n", cond_code);
+			if (case_word) {
+				/* "case" did not match anything: still set $? (to 0) */
+				G.last_exitcode = rcode = EXIT_SUCCESS;
+			}
+		}
+#endif
+		/* Just pressing <enter> in shell should check for jobs.
+		 * OTOH, in non-interactive shell this is useless
+		 * and only leads to extra job checks */
+		if (pi->num_cmds == 0) {
+			if (G_interactive_fd)
+				goto check_jobs_and_continue;
+			continue;
+		}
+
+		/* After analyzing all keywords and conditions, we decided
+		 * to execute this pipe. NB: have to do checkjobs(NULL)
+		 * after run_pipe to collect any background children,
+		 * even if list execution is to be stopped. */
+		debug_printf_exec(": run_pipe with %d members\n", pi->num_cmds);
+#if ENABLE_HUSH_LOOPS
+		G.flag_break_continue = 0;
+#endif
+		rcode = r = run_pipe(pi); /* NB: rcode is a smalluint, r is int */
+		if (r != -1) {
+			/* We ran a builtin, function, or group.
+			 * rcode is already known
+			 * and we don't need to wait for anything. */
+			debug_printf_exec(": builtin/func exitcode %d\n", rcode);
+			G.last_exitcode = rcode;
+			check_and_run_traps();
+#if ENABLE_HUSH_TRAP && ENABLE_HUSH_FUNCTIONS
+			rcode = G.last_exitcode; /* "return" in trap can change it, read back */
+#endif
+#if ENABLE_HUSH_LOOPS
+			/* Was it "break" or "continue"? */
+			if (G.flag_break_continue) {
+				smallint fbc = G.flag_break_continue;
+				/* We might fall into outer *loop*,
+				 * don't want to break it too */
+				if (loop_top) {
+					G.depth_break_continue--;
+					if (G.depth_break_continue == 0)
+						G.flag_break_continue = 0;
+					/* else: e.g. "continue 2" should *break* once, *then* continue */
+				} /* else: "while... do... { we are here (innermost list is not a loop!) };...done" */
+				if (G.depth_break_continue != 0 || fbc == BC_BREAK) {
+					checkjobs(NULL, 0 /*(no pid to wait for)*/);
+					break;
+				}
+				/* "continue": simulate end of loop */
+				rword = RES_DONE;
+				continue;
+			}
+#endif
+			if (G_flag_return_in_progress == 1) {
+				checkjobs(NULL, 0 /*(no pid to wait for)*/);
+				break;
+			}
+		} 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 NOT treating inner &'s as jobs */
+#if ENABLE_HUSH_JOB
+			if (G.run_list_level == 1)
+				insert_job_into_table(pi);
+#endif
+			/* Last command's pid goes to $! */
+			G.last_bg_pid = pi->cmds[pi->num_cmds - 1].pid;
+			G.last_bg_pid_exitcode = 0;
+			debug_printf_exec(": cmd&: exitcode EXIT_SUCCESS\n");
+/* Check pi->pi_inverted? "! sleep 1 & echo $?": bash says 1. dash and ash say 0 */
+			rcode = EXIT_SUCCESS;
+			goto check_traps;
+		} else {
+#if ENABLE_HUSH_JOB
+			if (G.run_list_level == 1 && G_interactive_fd) {
+				/* Waits for completion, then fg's main shell */
+				rcode = checkjobs_and_fg_shell(pi);
+				debug_printf_exec(": checkjobs_and_fg_shell exitcode %d\n", rcode);
+				goto check_traps;
+			}
+#endif
+			/* This one just waits for completion */
+			rcode = checkjobs(pi, 0 /*(no pid to wait for)*/);
+			debug_printf_exec(": checkjobs exitcode %d\n", rcode);
+ check_traps:
+			G.last_exitcode = rcode;
+			check_and_run_traps();
+#if ENABLE_HUSH_TRAP && ENABLE_HUSH_FUNCTIONS
+			rcode = G.last_exitcode; /* "return" in trap can change it, read back */
+#endif
+		}
+
+		/* 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)
+			cond_code = rcode;
+#endif
+ check_jobs_and_continue:
+		checkjobs(NULL, 0 /*(no pid to wait for)*/);
+ dont_check_jobs_but_continue: ;
+#if ENABLE_HUSH_LOOPS
+		/* Beware of "while false; true; do ..."! */
+		if (pi->next
+		 && (pi->next->res_word == RES_DO || pi->next->res_word == RES_DONE)
+		 /* check for RES_DONE is needed for "while ...; do \n done" case */
+		) {
+			if (rword == RES_WHILE) {
+				if (rcode) {
+					/* "while false; do...done" - exitcode 0 */
+					G.last_exitcode = rcode = EXIT_SUCCESS;
+					debug_printf_exec(": while expr is false: breaking (exitcode:EXIT_SUCCESS)\n");
+					break;
+				}
+			}
+			if (rword == RES_UNTIL) {
+				if (!rcode) {
+					debug_printf_exec(": until expr is true: breaking\n");
+					break;
+				}
+			}
+		}
+#endif
+	} /* for (pi) */
+
+#if ENABLE_HUSH_JOB
+	G.run_list_level--;
+#endif
+#if ENABLE_HUSH_LOOPS
+	if (loop_top)
+		G.depth_of_loop--;
+	free(for_list);
+#endif
+#if ENABLE_HUSH_CASE
+	free(case_word);
+#endif
+	debug_leave();
+	debug_printf_exec("run_list lvl %d return %d\n", G.run_list_level + 1, rcode);
+	return rcode;
+}
+
+/* Select which version we will use */
+static int run_and_free_list(struct pipe *pi)
+{
+	int rcode = 0;
+	debug_printf_exec("run_and_free_list entered\n");
+	if (!G.o_opt[OPT_O_NOEXEC]) {
+		debug_printf_exec(": run_list: 1st pipe with %d cmds\n", pi->num_cmds);
+		rcode = run_list(pi);
+	}
+	/* free_pipe_list has the side effect of clearing memory.
+	 * In the long run that function can be merged with run_list,
+	 * but doing that now would hobble the debugging effort. */
+	free_pipe_list(pi);
+	debug_printf_exec("run_and_free_list return %d\n", rcode);
+	return rcode;
+}
+
+
+static void install_sighandlers(unsigned mask)
+{
+	sighandler_t old_handler;
+	unsigned sig = 0;
+	while ((mask >>= 1) != 0) {
+		sig++;
+		if (!(mask & 1))
+			continue;
+		old_handler = install_sighandler(sig, pick_sighandler(sig));
+		/* POSIX allows shell to re-enable SIGCHLD
+		 * even if it was SIG_IGN on entry.
+		 * Therefore we skip IGN check for it:
+		 */
+		if (sig == SIGCHLD)
+			continue;
+		/* Interactive bash re-enables SIGHUP which is SIG_IGNed on entry.
+		 * Try:
+		 * trap '' hup; bash; echo RET  # type "kill -hup $$", see SIGHUP having effect
+		 * trap '' hup; bash -c 'kill -hup $$; echo ALIVE'  # here SIGHUP is SIG_IGNed
+		 */
+		if (sig == SIGHUP && G_interactive_fd)
+			continue;
+		/* Unless one of the above signals, is it SIG_IGN? */
+		if (old_handler == SIG_IGN) {
+			/* oops... restore back to IGN, and record this fact */
+			install_sighandler(sig, old_handler);
+#if ENABLE_HUSH_TRAP
+			if (!G_traps)
+				G_traps = xzalloc(sizeof(G_traps[0]) * NSIG);
+			free(G_traps[sig]);
+			G_traps[sig] = xzalloc(1); /* == xstrdup(""); */
+#endif
+		}
+	}
+}
+
+/* Called a few times only (or even once if "sh -c") */
+static void install_special_sighandlers(void)
+{
+	unsigned mask;
+
+	/* Which signals are shell-special? */
+	mask = (1 << SIGQUIT) | (1 << SIGCHLD);
+	if (G_interactive_fd) {
+		mask |= SPECIAL_INTERACTIVE_SIGS;
+		if (G_saved_tty_pgrp) /* we have ctty, job control sigs work */
+			mask |= SPECIAL_JOBSTOP_SIGS;
+	}
+	/* Careful, do not re-install handlers we already installed */
+	if (G.special_sig_mask != mask) {
+		unsigned diff = mask & ~G.special_sig_mask;
+		G.special_sig_mask = mask;
+		install_sighandlers(diff);
+	}
+}
+
+#if ENABLE_HUSH_JOB
+/* helper */
+/* Set handlers to restore tty pgrp and exit */
+static void install_fatal_sighandlers(void)
+{
+	unsigned mask;
+
+	/* We will restore tty pgrp on these signals */
+	mask = 0
+		/*+ (1 << SIGILL ) * HUSH_DEBUG*/
+		/*+ (1 << SIGFPE ) * HUSH_DEBUG*/
+		+ (1 << SIGBUS ) * HUSH_DEBUG
+		+ (1 << SIGSEGV) * HUSH_DEBUG
+		/*+ (1 << SIGTRAP) * HUSH_DEBUG*/
+		+ (1 << SIGABRT)
+	/* bash 3.2 seems to handle these just like 'fatal' ones */
+		+ (1 << SIGPIPE)
+		+ (1 << SIGALRM)
+	/* if we are interactive, SIGHUP, SIGTERM and SIGINT are special sigs.
+	 * if we aren't interactive... but in this case
+	 * we never want to restore pgrp on exit, and this fn is not called
+	 */
+		/*+ (1 << SIGHUP )*/
+		/*+ (1 << SIGTERM)*/
+		/*+ (1 << SIGINT )*/
+	;
+	G_fatal_sig_mask = mask;
+
+	install_sighandlers(mask);
+}
+#endif
+
+static int set_mode(int state, char mode, const char *o_opt)
+{
+	int idx;
+	switch (mode) {
+	case 'n':
+		G.o_opt[OPT_O_NOEXEC] = state;
+		break;
+	case 'x':
+		IF_HUSH_MODE_X(G_x_mode = state;)
+		IF_HUSH_MODE_X(if (G.x_mode_fd <= 0) G.x_mode_fd = dup_CLOEXEC(2, 10);)
+		break;
+	case 'e':
+		G.o_opt[OPT_O_ERREXIT] = state;
+		break;
+	case 'o':
+		if (!o_opt) {
+			/* "set -o" or "set +o" without parameter.
+			 * in bash, set -o produces this output:
+			 *  pipefail        off
+			 * and set +o:
+			 *  set +o pipefail
+			 * We always use the second form.
+			 */
+			const char *p = o_opt_strings;
+			idx = 0;
+			while (*p) {
+				printf("set %co %s\n", (G.o_opt[idx] ? '-' : '+'), p);
+				idx++;
+				p += strlen(p) + 1;
+			}
+			break;
+		}
+		idx = index_in_strings(o_opt_strings, o_opt);
+		if (idx >= 0) {
+			G.o_opt[idx] = state;
+			break;
+		}
+		/* fall through to error */
+	default:
+		return EXIT_FAILURE;
+	}
+	return EXIT_SUCCESS;
+}
+
+int hush_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+int hush_main(int argc, char **argv)
+{
+	pid_t cached_getpid;
+	enum {
+		OPT_login = (1 << 0),
+	};
+	unsigned flags;
+#if !BB_MMU
+	unsigned builtin_argc = 0;
+#endif
+	char **e;
+	struct variable *cur_var;
+	struct variable *shell_ver;
+
+	INIT_G();
+	if (EXIT_SUCCESS != 0) /* if EXIT_SUCCESS == 0, it is already done */
+		G.last_exitcode = EXIT_SUCCESS;
+#if ENABLE_HUSH_TRAP
+# if ENABLE_HUSH_FUNCTIONS
+	G.return_exitcode = -1;
+# endif
+	G.pre_trap_exitcode = -1;
+#endif
+
+#if ENABLE_HUSH_FAST
+	G.count_SIGCHLD++; /* ensure it is != G.handled_SIGCHLD */
+#endif
+#if !BB_MMU
+	G.argv0_for_re_execing = argv[0];
+#endif
+
+	cached_getpid = getpid();   /* for tcsetpgrp() during init */
+	G.root_pid = cached_getpid; /* for $PID  (NOMMU can override via -$HEXPID:HEXPPID:...) */
+	G.root_ppid = getppid();    /* for $PPID (NOMMU can override)  */
+
+	/* Deal with HUSH_VERSION */
+	debug_printf_env("unsetenv '%s'\n", "HUSH_VERSION");
+	unsetenv("HUSH_VERSION"); /* in case it exists in initial env */
+	shell_ver = xzalloc(sizeof(*shell_ver));
+	shell_ver->flg_export = 1;
+	shell_ver->flg_read_only = 1;
+	/* Code which handles ${var<op>...} needs writable values for all variables,
+	 * therefore we xstrdup: */
+	shell_ver->varstr = xstrdup(hush_version_str);
+
+	/* Create shell local variables from the values
+	 * currently living in the environment */
+	G.top_var = shell_ver;
+	cur_var = G.top_var;
+	e = environ;
+	if (e) while (*e) {
+		char *value = strchr(*e, '=');
+		if (value) { /* paranoia */
+			cur_var->next = xzalloc(sizeof(*cur_var));
+			cur_var = cur_var->next;
+			cur_var->varstr = *e;
+			cur_var->max_len = strlen(*e);
+			cur_var->flg_export = 1;
+		}
+		e++;
+	}
+	/* (Re)insert HUSH_VERSION into env (AFTER we scanned the env!) */
+	debug_printf_env("putenv '%s'\n", shell_ver->varstr);
+	putenv(shell_ver->varstr);
+
+	/* Export PWD */
+	set_pwd_var(SETFLAG_EXPORT);
+
+#if BASH_HOSTNAME_VAR
+	/* Set (but not export) HOSTNAME unless already set */
+	if (!get_local_var_value("HOSTNAME")) {
+		struct utsname uts;
+		uname(&uts);
+		set_local_var_from_halves("HOSTNAME", uts.nodename);
+	}
+#endif
+	/* IFS is not inherited from the parent environment */
+	set_local_var_from_halves("IFS", defifs);
+
+	if (!get_local_var_value("PATH"))
+		set_local_var_from_halves("PATH", bb_default_root_path);
+
+	/* PS1/PS2 are set later, if we determine that we are interactive */
+
+	/* bash also exports SHLVL and _,
+	 * and sets (but doesn't export) the following variables:
+	 * BASH=/bin/bash
+	 * BASH_VERSINFO=([0]="3" [1]="2" [2]="0" [3]="1" [4]="release" [5]="i386-pc-linux-gnu")
+	 * BASH_VERSION='3.2.0(1)-release'
+	 * HOSTTYPE=i386
+	 * MACHTYPE=i386-pc-linux-gnu
+	 * OSTYPE=linux-gnu
+	 * PPID=<NNNNN> - we also do it elsewhere
+	 * EUID=<NNNNN>
+	 * UID=<NNNNN>
+	 * GROUPS=()
+	 * LINES=<NNN>
+	 * COLUMNS=<NNN>
+	 * BASH_ARGC=()
+	 * BASH_ARGV=()
+	 * BASH_LINENO=()
+	 * BASH_SOURCE=()
+	 * DIRSTACK=()
+	 * PIPESTATUS=([0]="0")
+	 * HISTFILE=/<xxx>/.bash_history
+	 * HISTFILESIZE=500
+	 * HISTSIZE=500
+	 * MAILCHECK=60
+	 * PATH=/usr/gnu/bin:/usr/local/bin:/bin:/usr/bin:.
+	 * SHELL=/bin/bash
+	 * SHELLOPTS=braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor
+	 * TERM=dumb
+	 * OPTERR=1
+	 * OPTIND=1
+	 * PS4='+ '
+	 */
+
+#if NUM_SCRIPTS > 0
+	if (argc < 0) {
+		char *script = get_script_content(-argc - 1);
+		G.global_argv = argv;
+		G.global_argc = string_array_len(argv);
+		//install_special_sighandlers(); - needed?
+		parse_and_run_string(script);
+		goto final_return;
+	}
+#endif
+
+	/* Initialize some more globals to non-zero values */
+	die_func = restore_ttypgrp_and__exit;
+
+	/* Shell is non-interactive at first. We need to call
+	 * install_special_sighandlers() if we are going to execute "sh <script>",
+	 * "sh -c <cmds>" or login shell's /etc/profile and friends.
+	 * If we later decide that we are interactive, we run install_special_sighandlers()
+	 * in order to intercept (more) signals.
+	 */
+
+	/* Parse options */
+	/* http://www.opengroup.org/onlinepubs/9699919799/utilities/sh.html */
+	flags = (argv[0] && argv[0][0] == '-') ? OPT_login : 0;
+	while (1) {
+		int opt = getopt(argc, argv, "+" /* stop at 1st non-option */
+				"cexinsl"
+#if !BB_MMU
+				"<:$:R:V:"
+# if ENABLE_HUSH_FUNCTIONS
+				"F:"
+# endif
+#endif
+		);
+		if (opt <= 0)
+			break;
+		switch (opt) {
+		case 'c':
+			/* Note: -c is not an option with param!
+			 * "hush -c -l SCRIPT" is valid. "hush -cSCRIPT" is not.
+			 */
+			G.opt_c = 1;
+			break;
+		case 'i':
+			/* Well, we cannot just declare interactiveness,
+			 * we have to have some stuff (ctty, etc) */
+			/* G_interactive_fd++; */
+			break;
+		case 's':
+			G.opt_s = 1;
+			break;
+		case 'l':
+			flags |= OPT_login;
+			break;
+#if !BB_MMU
+		case '<': /* "big heredoc" support */
+			full_write1_str(optarg);
+			_exit(0);
+		case '$': {
+			unsigned long long empty_trap_mask;
+
+			G.root_pid = bb_strtou(optarg, &optarg, 16);
+			optarg++;
+			G.root_ppid = bb_strtou(optarg, &optarg, 16);
+			optarg++;
+			G.last_bg_pid = bb_strtou(optarg, &optarg, 16);
+			optarg++;
+			G.last_exitcode = bb_strtou(optarg, &optarg, 16);
+			optarg++;
+			builtin_argc = bb_strtou(optarg, &optarg, 16);
+			optarg++;
+			empty_trap_mask = bb_strtoull(optarg, &optarg, 16);
+			if (empty_trap_mask != 0) {
+				IF_HUSH_TRAP(int sig;)
+				install_special_sighandlers();
+# if ENABLE_HUSH_TRAP
+				G_traps = xzalloc(sizeof(G_traps[0]) * NSIG);
+				for (sig = 1; sig < NSIG; sig++) {
+					if (empty_trap_mask & (1LL << sig)) {
+						G_traps[sig] = xzalloc(1); /* == xstrdup(""); */
+						install_sighandler(sig, SIG_IGN);
+					}
+				}
+# endif
+			}
+# if ENABLE_HUSH_LOOPS
+			optarg++;
+			G.depth_of_loop = bb_strtou(optarg, &optarg, 16);
+# endif
+			/* Suppress "killed by signal" message, -$ hack is used
+			 * for subshells: echo `sh -c 'kill -9 $$'`
+			 * should be silent.
+			 */
+			IF_HUSH_JOB(G.run_list_level = 1;)
+# if ENABLE_HUSH_FUNCTIONS
+			/* nommu uses re-exec trick for "... | func | ...",
+			 * should allow "return".
+			 * This accidentally allows returns in subshells.
+			 */
+			G_flag_return_in_progress = -1;
+# endif
+			break;
+		}
+		case 'R':
+		case 'V':
+			set_local_var(xstrdup(optarg), opt == 'R' ? SETFLAG_MAKE_RO : 0);
+			break;
+# if ENABLE_HUSH_FUNCTIONS
+		case 'F': {
+			struct function *funcp = new_function(optarg);
+			/* funcp->name is already set to optarg */
+			/* funcp->body is set to NULL. It's a special case. */
+			funcp->body_as_string = argv[optind];
+			optind++;
+			break;
+		}
+# endif
+#endif
+		/*case '?': invalid option encountered (set_mode('?') will fail) */
+		/*case 'n':*/
+		/*case 'x':*/
+		/*case 'e':*/
+		default:
+			if (set_mode(1, opt, NULL) == 0) /* no error */
+				break;
+			bb_show_usage();
+		}
+	} /* option parsing loop */
+
+	/* Skip options. Try "hush -l": $1 should not be "-l"! */
+	G.global_argc = argc - (optind - 1);
+	G.global_argv = argv + (optind - 1);
+	G.global_argv[0] = argv[0];
+
+	/* If we are login shell... */
+	if (flags & OPT_login) {
+		const char *hp = NULL;
+		HFILE *input;
+
+		debug_printf("sourcing /etc/profile\n");
+		input = hfopen("/etc/profile");
+ run_profile:
+		if (input != NULL) {
+			install_special_sighandlers();
+			parse_and_run_file(input);
+			hfclose(input);
+		}
+		/* bash: after sourcing /etc/profile,
+		 * tries to source (in the given order):
+		 * ~/.bash_profile, ~/.bash_login, ~/.profile,
+		 * stopping on first found. --noprofile turns this off.
+		 * bash also sources ~/.bash_logout on exit.
+		 * If called as sh, skips .bash_XXX files.
+		 */
+		if (!hp) { /* unless we looped on the "goto" already */
+			hp = get_local_var_value("HOME");
+			if (hp && hp[0]) {
+				debug_printf("sourcing ~/.profile\n");
+				hp = concat_path_file(hp, ".profile");
+				input = hfopen(hp);
+				free((char*)hp);
+				goto run_profile;
+			}
+		}
+	}
+
+	/* -c takes effect *after* -l */
+	if (G.opt_c) {
+		/* Possibilities:
+		 * sh ... -c 'script'
+		 * sh ... -c 'script' ARG0 [ARG1...]
+		 * On NOMMU, if builtin_argc != 0,
+		 * sh ... -c 'builtin' BARGV... "" ARG0 [ARG1...]
+		 * "" needs to be replaced with NULL
+		 * and BARGV vector fed to builtin function.
+		 * Note: the form without ARG0 never happens:
+		 * sh ... -c 'builtin' BARGV... ""
+		 */
+		char *script;
+
+		install_special_sighandlers();
+
+		G.global_argc--;
+		G.global_argv++;
+#if !BB_MMU
+		if (builtin_argc) {
+			/* -c 'builtin' [BARGV...] "" ARG0 [ARG1...] */
+			const struct built_in_command *x;
+			x = find_builtin(G.global_argv[0]);
+			if (x) { /* paranoia */
+				argv = G.global_argv;
+				G.global_argc -= builtin_argc + 1; /* skip [BARGV...] "" */
+				G.global_argv += builtin_argc + 1;
+				G.global_argv[-1] = NULL; /* replace "" */
+				G.last_exitcode = x->b_function(argv);
+			}
+			goto final_return;
+		}
+#endif
+
+		script = G.global_argv[0];
+		if (!script)
+			bb_error_msg_and_die(bb_msg_requires_arg, "-c");
+		if (!G.global_argv[1]) {
+			/* -c 'script' (no params): prevent empty $0 */
+			G.global_argv[0] = argv[0];
+		} else { /* else -c 'script' ARG0 [ARG1...]: $0 is ARG0 */
+			G.global_argc--;
+			G.global_argv++;
+		}
+		parse_and_run_string(script);
+		goto final_return;
+	}
+
+	/* -s is: hush -s ARGV1 ARGV2 (no SCRIPT) */
+	if (!G.opt_s && G.global_argv[1]) {
+		HFILE *input;
+		/*
+		 * "bash <script>" (which is never interactive (unless -i?))
+		 * sources $BASH_ENV here (without scanning $PATH).
+		 * If called as sh, does the same but with $ENV.
+		 * Also NB, per POSIX, $ENV should undergo parameter expansion.
+		 */
+		G.global_argc--;
+		G.global_argv++;
+		debug_printf("running script '%s'\n", G.global_argv[0]);
+		xfunc_error_retval = 127; /* for "hush /does/not/exist" case */
+		input = hfopen(G.global_argv[0]);
+		if (!input) {
+			bb_simple_perror_msg_and_die(G.global_argv[0]);
+		}
+		xfunc_error_retval = 1;
+		install_special_sighandlers();
+		parse_and_run_file(input);
+#if ENABLE_FEATURE_CLEAN_UP
+		hfclose(input);
+#endif
+		goto final_return;
+	}
+	/* "implicit" -s: bare interactive hush shows 's' in $- */
+	G.opt_s = 1;
+
+	/* Up to here, shell was non-interactive. Now it may become one.
+	 * NB: don't forget to (re)run install_special_sighandlers() as needed.
+	 */
+
+	/* A shell is interactive if the '-i' flag was given,
+	 * or if all of the following conditions are met:
+	 *    no -c command
+	 *    no arguments remaining or the -s flag given
+	 *    standard input is a terminal
+	 *    standard output is a terminal
+	 * Refer to Posix.2, the description of the 'sh' utility.
+	 */
+#if ENABLE_HUSH_JOB
+	if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
+		G_saved_tty_pgrp = tcgetpgrp(STDIN_FILENO);
+		debug_printf("saved_tty_pgrp:%d\n", G_saved_tty_pgrp);
+		if (G_saved_tty_pgrp < 0)
+			G_saved_tty_pgrp = 0;
+
+		/* try to dup stdin to high fd#, >= 255 */
+		G_interactive_fd = dup_CLOEXEC(STDIN_FILENO, 254);
+		if (G_interactive_fd < 0) {
+			/* try to dup to any fd */
+			G_interactive_fd = dup(STDIN_FILENO);
+			if (G_interactive_fd < 0) {
+				/* give up */
+				G_interactive_fd = 0;
+				G_saved_tty_pgrp = 0;
+			}
+		}
+	}
+	debug_printf("interactive_fd:%d\n", G_interactive_fd);
+	if (G_interactive_fd) {
+		close_on_exec_on(G_interactive_fd);
+
+		if (G_saved_tty_pgrp) {
+			/* If we were run as 'hush &', sleep until we are
+			 * in the foreground (tty pgrp == our pgrp).
+			 * If we get started under a job aware app (like bash),
+			 * make sure we are now in charge so we don't fight over
+			 * who gets the foreground */
+			while (1) {
+				pid_t shell_pgrp = getpgrp();
+				G_saved_tty_pgrp = tcgetpgrp(G_interactive_fd);
+				if (G_saved_tty_pgrp == shell_pgrp)
+					break;
+				/* send TTIN to ourself (should stop us) */
+				kill(- shell_pgrp, SIGTTIN);
+			}
+		}
+
+		/* Install more signal handlers */
+		install_special_sighandlers();
+
+		if (G_saved_tty_pgrp) {
+			/* Set other signals to restore saved_tty_pgrp */
+			install_fatal_sighandlers();
+			/* Put ourselves in our own process group
+			 * (bash, too, does this only if ctty is available) */
+			bb_setpgrp(); /* is the same as setpgid(our_pid, our_pid); */
+			/* Grab control of the terminal */
+			tcsetpgrp(G_interactive_fd, cached_getpid);
+		}
+		enable_restore_tty_pgrp_on_exit();
+
+# if ENABLE_FEATURE_EDITING
+		G.line_input_state = new_line_input_t(FOR_SHELL);
+#  if EDITING_HAS_get_exe_name
+		G.line_input_state->get_exe_name = get_builtin_name;
+#  endif
+# endif
+# if ENABLE_HUSH_SAVEHISTORY && MAX_HISTORY > 0
+		{
+			const char *hp = get_local_var_value("HISTFILE");
+			if (!hp) {
+				hp = get_local_var_value("HOME");
+				if (hp)
+					hp = concat_path_file(hp, ".hush_history");
+			} else {
+				hp = xstrdup(hp);
+			}
+			if (hp) {
+				G.line_input_state->hist_file = hp;
+				//set_local_var(xasprintf("HISTFILE=%s", ...));
+			}
+#  if ENABLE_FEATURE_SH_HISTFILESIZE
+			hp = get_local_var_value("HISTFILESIZE");
+			G.line_input_state->max_history = size_from_HISTFILESIZE(hp);
+#  endif
+		}
+# endif
+	} else {
+		install_special_sighandlers();
+	}
+#elif ENABLE_HUSH_INTERACTIVE
+	/* No job control compiled in, only prompt/line editing */
+	if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
+		G_interactive_fd = dup_CLOEXEC(STDIN_FILENO, 254);
+		if (G_interactive_fd < 0) {
+			/* try to dup to any fd */
+			G_interactive_fd = dup_CLOEXEC(STDIN_FILENO, -1);
+			if (G_interactive_fd < 0)
+				/* give up */
+				G_interactive_fd = 0;
+		}
+	}
+	if (G_interactive_fd) {
+		close_on_exec_on(G_interactive_fd);
+	}
+	install_special_sighandlers();
+#else
+	/* We have interactiveness code disabled */
+	install_special_sighandlers();
+#endif
+	/* bash:
+	 * if interactive but not a login shell, sources ~/.bashrc
+	 * (--norc turns this off, --rcfile <file> overrides)
+	 */
+
+	if (G_interactive_fd) {
+#if ENABLE_HUSH_INTERACTIVE && ENABLE_FEATURE_EDITING_FANCY_PROMPT
+		/* Set (but not export) PS1/2 unless already set */
+		if (!get_local_var_value("PS1"))
+			set_local_var_from_halves("PS1", "\\w \\$ ");
+		if (!get_local_var_value("PS2"))
+			set_local_var_from_halves("PS2", "> ");
+#endif
+		if (!ENABLE_FEATURE_SH_EXTRA_QUIET) {
+			/* note: ash and hush share this string */
+			printf("\n\n%s %s\n"
+				IF_HUSH_HELP("Enter 'help' for a list of built-in commands.\n")
+				"\n",
+				bb_banner,
+				"hush - the humble shell"
+			);
+		}
+	}
+
+	parse_and_run_file(hfopen(NULL)); /* stdin */
+
+ final_return:
+	hush_exit(G.last_exitcode);
+}
+
+
+/*
+ * Built-ins
+ */
+static int FAST_FUNC builtin_true(char **argv UNUSED_PARAM)
+{
+	return 0;
+}
+
+#if ENABLE_HUSH_TEST || ENABLE_HUSH_ECHO || ENABLE_HUSH_PRINTF || ENABLE_HUSH_KILL
+static NOINLINE int run_applet_main(char **argv, int (*applet_main_func)(int argc, char **argv))
+{
+	int argc = string_array_len(argv);
+	return applet_main_func(argc, argv);
+}
+#endif
+#if ENABLE_HUSH_TEST || BASH_TEST2
+static int FAST_FUNC builtin_test(char **argv)
+{
+	return run_applet_main(argv, test_main);
+}
+#endif
+#if ENABLE_HUSH_ECHO
+static int FAST_FUNC builtin_echo(char **argv)
+{
+	return run_applet_main(argv, echo_main);
+}
+#endif
+#if ENABLE_HUSH_PRINTF
+static int FAST_FUNC builtin_printf(char **argv)
+{
+	return run_applet_main(argv, printf_main);
+}
+#endif
+
+#if ENABLE_HUSH_HELP
+static int FAST_FUNC builtin_help(char **argv UNUSED_PARAM)
+{
+	const struct built_in_command *x;
+
+	printf(
+		"Built-in commands:\n"
+		"------------------\n");
+	for (x = bltins1; x != &bltins1[ARRAY_SIZE(bltins1)]; x++) {
+		if (x->b_descr)
+			printf("%-10s%s\n", x->b_cmd, x->b_descr);
+	}
+	return EXIT_SUCCESS;
+}
+#endif
+
+#if MAX_HISTORY && ENABLE_FEATURE_EDITING
+static int FAST_FUNC builtin_history(char **argv UNUSED_PARAM)
+{
+	show_history(G.line_input_state);
+	return EXIT_SUCCESS;
+}
+#endif
+
+static char **skip_dash_dash(char **argv)
+{
+	argv++;
+	if (argv[0] && argv[0][0] == '-' && argv[0][1] == '-' && argv[0][2] == '\0')
+		argv++;
+	return argv;
+}
+
+static int FAST_FUNC builtin_cd(char **argv)
+{
+	const char *newdir;
+
+	argv = skip_dash_dash(argv);
+	newdir = argv[0];
+	if (newdir == NULL) {
+		/* bash does nothing (exitcode 0) if HOME is ""; if it's unset,
+		 * bash says "bash: cd: HOME not set" and does nothing
+		 * (exitcode 1)
+		 */
+		const char *home = get_local_var_value("HOME");
+		newdir = home ? home : "/";
+	}
+	if (chdir(newdir)) {
+		/* Mimic bash message exactly */
+		bb_perror_msg("cd: %s", newdir);
+		return EXIT_FAILURE;
+	}
+	/* Read current dir (get_cwd(1) is inside) and set PWD.
+	 * 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(/*flag:*/ 0);
+	return EXIT_SUCCESS;
+}
+
+static int FAST_FUNC builtin_pwd(char **argv UNUSED_PARAM)
+{
+	puts(get_cwd(0));
+	return EXIT_SUCCESS;
+}
+
+static int FAST_FUNC builtin_eval(char **argv)
+{
+	argv = skip_dash_dash(argv);
+
+	if (!argv[0])
+		return EXIT_SUCCESS;
+
+	IF_HUSH_MODE_X(G.x_mode_depth++;)
+	//bb_error_msg("%s: ++x_mode_depth=%d", __func__, G.x_mode_depth);
+	if (!argv[1]) {
+		/* bash:
+		 * eval "echo Hi; done" ("done" is syntax error):
+		 * "echo Hi" will not execute too.
+		 */
+		parse_and_run_string(argv[0]);
+	} else {
+		/* "The eval utility shall construct a command by
+		 * concatenating arguments together, separating
+		 * each with a <space> character."
+		 */
+		char *str, *p;
+		unsigned len = 0;
+		char **pp = argv;
+		do
+			len += strlen(*pp) + 1;
+		while (*++pp);
+		str = p = xmalloc(len);
+		pp = argv;
+		for (;;) {
+			p = stpcpy(p, *pp);
+			pp++;
+			if (!*pp)
+				break;
+			*p++ = ' ';
+		}
+		parse_and_run_string(str);
+		free(str);
+	}
+	IF_HUSH_MODE_X(G.x_mode_depth--;)
+	//bb_error_msg("%s: --x_mode_depth=%d", __func__, G.x_mode_depth);
+	return G.last_exitcode;
+}
+
+static int FAST_FUNC builtin_exec(char **argv)
+{
+	argv = skip_dash_dash(argv);
+	if (argv[0] == NULL)
+		return EXIT_SUCCESS; /* bash does this */
+
+	/* Careful: we can end up here after [v]fork. Do not restore
+	 * tty pgrp then, only top-level shell process does that */
+	if (G_saved_tty_pgrp && getpid() == G.root_pid)
+		tcsetpgrp(G_interactive_fd, G_saved_tty_pgrp);
+
+	/* Saved-redirect fds, script fds and G_interactive_fd are still
+	 * open here. However, they are all CLOEXEC, and execv below
+	 * closes them. Try interactive "exec ls -l /proc/self/fd",
+	 * it should show no extra open fds in the "ls" process.
+	 * If we'd try to run builtins/NOEXECs, this would need improving.
+	 */
+	//close_saved_fds_and_FILE_fds();
+
+	/* TODO: if exec fails, bash does NOT exit! We do.
+	 * We'll need to undo trap cleanup (it's inside execvp_or_die)
+	 * and tcsetpgrp, and this is inherently racy.
+	 */
+	execvp_or_die(argv);
+}
+
+static int FAST_FUNC builtin_exit(char **argv)
+{
+	debug_printf_exec("%s()\n", __func__);
+
+	/* interactive bash:
+	 * # trap "echo EEE" EXIT
+	 * # exit
+	 * exit
+	 * There are stopped jobs.
+	 * (if there are _stopped_ jobs, running ones don't count)
+	 * # exit
+	 * exit
+	 * EEE (then bash exits)
+	 *
+	 * TODO: we can use G.exiting = -1 as indicator "last cmd was exit"
+	 */
+
+	/* note: EXIT trap is run by hush_exit */
+	argv = skip_dash_dash(argv);
+	if (argv[0] == NULL) {
+#if ENABLE_HUSH_TRAP
+		if (G.pre_trap_exitcode >= 0) /* "exit" in trap uses $? from before the trap */
+			hush_exit(G.pre_trap_exitcode);
+#endif
+		hush_exit(G.last_exitcode);
+	}
+	/* mimic bash: exit 123abc == exit 255 + error msg */
+	xfunc_error_retval = 255;
+	/* bash: exit -2 == exit 254, no error msg */
+	hush_exit(xatoi(argv[0]) & 0xff);
+}
+
+#if ENABLE_HUSH_TYPE
+/* http://www.opengroup.org/onlinepubs/9699919799/utilities/type.html */
+static int FAST_FUNC builtin_type(char **argv)
+{
+	int ret = EXIT_SUCCESS;
+
+	while (*++argv) {
+		const char *type;
+		char *path = NULL;
+
+		if (0) {} /* make conditional compile easier below */
+		/*else if (find_alias(*argv))
+			type = "an alias";*/
+# if ENABLE_HUSH_FUNCTIONS
+		else if (find_function(*argv))
+			type = "a function";
+# endif
+		else if (find_builtin(*argv))
+			type = "a shell builtin";
+		else if ((path = find_in_path(*argv)) != NULL)
+			type = path;
+		else {
+			bb_error_msg("type: %s: not found", *argv);
+			ret = EXIT_FAILURE;
+			continue;
+		}
+
+		printf("%s is %s\n", *argv, type);
+		free(path);
+	}
+
+	return ret;
+}
+#endif
+
+#if ENABLE_HUSH_READ
+/* Interruptibility of read builtin in bash
+ * (tested on bash-4.2.8 by sending signals (not by ^C)):
+ *
+ * Empty trap makes read ignore corresponding signal, for any signal.
+ *
+ * SIGINT:
+ * - terminates non-interactive shell;
+ * - interrupts read in interactive shell;
+ * if it has non-empty trap:
+ * - executes trap and returns to command prompt in interactive shell;
+ * - executes trap and returns to read in non-interactive shell;
+ * SIGTERM:
+ * - is ignored (does not interrupt) read in interactive shell;
+ * - terminates non-interactive shell;
+ * if it has non-empty trap:
+ * - executes trap and returns to read;
+ * SIGHUP:
+ * - terminates shell (regardless of interactivity);
+ * if it has non-empty trap:
+ * - executes trap and returns to read;
+ * SIGCHLD from children:
+ * - does not interrupt read regardless of interactivity:
+ *   try: sleep 1 & read x; echo $x
+ */
+static int FAST_FUNC builtin_read(char **argv)
+{
+	const char *r;
+	struct builtin_read_params params;
+
+	memset(&params, 0, sizeof(params));
+
+	/* "!": do not abort on errors.
+	 * Option string must start with "sr" to match BUILTIN_READ_xxx
+	 */
+	params.read_flags = getopt32(argv,
+# if BASH_READ_D
+		IF_NOT_HUSH_BASH_COMPAT("^")
+		"!srn:p:t:u:d:" IF_NOT_HUSH_BASH_COMPAT("\0" "-1"/*min 1 arg*/),
+		&params.opt_n, &params.opt_p, &params.opt_t, &params.opt_u, &params.opt_d
+# else
+		IF_NOT_HUSH_BASH_COMPAT("^")
+		"!srn:p:t:u:" IF_NOT_HUSH_BASH_COMPAT("\0" "-1"/*min 1 arg*/),
+		&params.opt_n, &params.opt_p, &params.opt_t, &params.opt_u
+# endif
+//TODO: print "read: need variable name"
+//for the case of !BASH "read" with no args (now it fails silently)
+//(or maybe extend getopt32() to emit a message if "-1" fails)
+	);
+	if ((uint32_t)params.read_flags == (uint32_t)-1)
+		return EXIT_FAILURE;
+	argv += optind;
+	params.argv = argv;
+	params.setvar = set_local_var_from_halves;
+	params.ifs = get_local_var_value("IFS"); /* can be NULL */
+
+ again:
+	r = shell_builtin_read(&params);
+
+	if ((uintptr_t)r == 1 && errno == EINTR) {
+		unsigned sig = check_and_run_traps();
+		if (sig != SIGINT)
+			goto again;
+	}
+
+	if ((uintptr_t)r > 1) {
+		bb_simple_error_msg(r);
+		r = (char*)(uintptr_t)1;
+	}
+
+	return (uintptr_t)r;
+}
+#endif
+
+#if ENABLE_HUSH_UMASK
+static int FAST_FUNC builtin_umask(char **argv)
+{
+	int rc;
+	mode_t mask;
+
+	rc = 1;
+	mask = umask(0);
+	argv = skip_dash_dash(argv);
+	if (argv[0]) {
+		mode_t old_mask = mask;
+
+		/* numeric umasks are taken as-is */
+		/* symbolic umasks are inverted: "umask a=rx" calls umask(222) */
+		if (!isdigit(argv[0][0]))
+			mask ^= 0777;
+		mask = bb_parse_mode(argv[0], mask);
+		if (!isdigit(argv[0][0]))
+			mask ^= 0777;
+		if ((unsigned)mask > 0777) {
+			mask = old_mask;
+			/* bash messages:
+			 * bash: umask: 'q': invalid symbolic mode operator
+			 * bash: umask: 999: octal number out of range
+			 */
+			bb_error_msg("%s: invalid mode '%s'", "umask", argv[0]);
+			rc = 0;
+		}
+	} else {
+		/* Mimic bash */
+		printf("%04o\n", (unsigned) mask);
+		/* fall through and restore mask which we set to 0 */
+	}
+	umask(mask);
+
+	return !rc; /* rc != 0 - success */
+}
+#endif
+
+#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_TRAP
+static void print_escaped(const char *s)
+{
+	if (*s == '\'')
+		goto squote;
+	do {
+		const char *p = strchrnul(s, '\'');
+		/* print 'xxxx', possibly just '' */
+		printf("'%.*s'", (int)(p - s), s);
+		if (*p == '\0')
+			break;
+		s = p;
+ squote:
+		/* s points to '; print "'''...'''" */
+		putchar('"');
+		do putchar('\''); while (*++s == '\'');
+		putchar('"');
+	} while (*s);
+}
+#endif
+
+#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_LOCAL || ENABLE_HUSH_READONLY
+static int helper_export_local(char **argv, unsigned flags)
+{
+	do {
+		char *name = *argv;
+		const char *name_end = endofname(name);
+
+		if (*name_end == '\0') {
+			struct variable *var, **vpp;
+
+			vpp = get_ptr_to_local_var(name, name_end - name);
+			var = vpp ? *vpp : NULL;
+
+			if (flags & SETFLAG_UNEXPORT) {
+				/* export -n NAME (without =VALUE) */
+				if (var) {
+					var->flg_export = 0;
+					debug_printf_env("%s: unsetenv '%s'\n", __func__, name);
+					unsetenv(name);
+				} /* else: export -n NOT_EXISTING_VAR: no-op */
+				continue;
+			}
+			if (flags & SETFLAG_EXPORT) {
+				/* export NAME (without =VALUE) */
+				if (var) {
+					var->flg_export = 1;
+					debug_printf_env("%s: putenv '%s'\n", __func__, var->varstr);
+					putenv(var->varstr);
+					continue;
+				}
+			}
+			if (flags & SETFLAG_MAKE_RO) {
+				/* readonly NAME (without =VALUE) */
+				if (var) {
+					var->flg_read_only = 1;
+					continue;
+				}
+			}
+# if ENABLE_HUSH_LOCAL
+			/* Is this "local" bltin? */
+			if (!(flags & (SETFLAG_EXPORT|SETFLAG_UNEXPORT|SETFLAG_MAKE_RO))) {
+				unsigned lvl = flags >> SETFLAG_VARLVL_SHIFT;
+				if (var && var->var_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.
+			 */
+			/* Or, it's "local NAME" (without =VALUE).
+			 * 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 {
+			if (*name_end != '=') {
+				bb_error_msg("'%s': bad variable name", name);
+				/* do not parse following argv[]s: */
+				return 1;
+			}
+			/* (Un)exporting/making local NAME=VALUE */
+			name = xstrdup(name);
+			/* Testcase: export PS1='\w \$ ' */
+			unbackslash(name);
+		}
+		debug_printf_env("%s: set_local_var('%s')\n", __func__, name);
+		if (set_local_var(name, flags))
+			return EXIT_FAILURE;
+	} while (*++argv);
+	return EXIT_SUCCESS;
+}
+#endif
+
+#if ENABLE_HUSH_EXPORT
+static int FAST_FUNC builtin_export(char **argv)
+{
+	unsigned opt_unexport;
+
+# if ENABLE_HUSH_EXPORT_N
+	/* "!": do not abort on errors */
+	opt_unexport = getopt32(argv, "!n");
+	if (opt_unexport == (uint32_t)-1)
+		return EXIT_FAILURE;
+	argv += optind;
+# else
+	opt_unexport = 0;
+	argv++;
+# endif
+
+	if (argv[0] == NULL) {
+		char **e = environ;
+		if (e) {
+			while (*e) {
+# if 0
+				puts(*e++);
+# else
+				/* ash emits: export VAR='VAL'
+				 * bash: declare -x VAR="VAL"
+				 * we follow ash example */
+				const char *s = *e++;
+				const char *p = strchr(s, '=');
+
+				if (!p) /* wtf? take next variable */
+					continue;
+				/* export var= */
+				printf("export %.*s", (int)(p - s) + 1, s);
+				print_escaped(p + 1);
+				putchar('\n');
+# endif
+			}
+			/*fflush_all(); - done after each builtin anyway */
+		}
+		return EXIT_SUCCESS;
+	}
+
+	return helper_export_local(argv, opt_unexport ? SETFLAG_UNEXPORT : SETFLAG_EXPORT);
+}
+#endif
+
+#if ENABLE_HUSH_LOCAL
+static int FAST_FUNC builtin_local(char **argv)
+{
+	if (G.func_nest_level == 0) {
+		bb_error_msg("%s: not in a function", argv[0]);
+		return EXIT_FAILURE; /* bash compat */
+	}
+	argv++;
+	/* Since all builtins run in a nested variable level,
+	 * need to use level - 1 here. Or else the variable will be removed at once
+	 * after builtin returns.
+	 */
+	return helper_export_local(argv, (G.var_nest_level - 1) << SETFLAG_VARLVL_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
+
+#if ENABLE_HUSH_UNSET
+/* http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#unset */
+static int FAST_FUNC builtin_unset(char **argv)
+{
+	int ret;
+	unsigned opts;
+
+	/* "!": do not abort on errors */
+	/* "+": stop at 1st non-option */
+	opts = getopt32(argv, "!+vf");
+	if (opts == (unsigned)-1)
+		return EXIT_FAILURE;
+	if (opts == 3) {
+		bb_simple_error_msg("unset: -v and -f are exclusive");
+		return EXIT_FAILURE;
+	}
+	argv += optind;
+
+	ret = EXIT_SUCCESS;
+	while (*argv) {
+		if (!(opts & 2)) { /* not -f */
+			if (unset_local_var(*argv)) {
+				/* unset <nonexistent_var> doesn't fail.
+				 * Error is when one tries to unset RO var.
+				 * Message was printed by unset_local_var. */
+				ret = EXIT_FAILURE;
+			}
+		}
+# if ENABLE_HUSH_FUNCTIONS
+		else {
+			unset_func(*argv);
+		}
+# endif
+		argv++;
+	}
+	return ret;
+}
+#endif
+
+#if ENABLE_HUSH_SET
+/* http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#set
+ * built-in 'set' handler
+ * SUSv3 says:
+ * set [-abCefhmnuvx] [-o option] [argument...]
+ * set [+abCefhmnuvx] [+o option] [argument...]
+ * set -- [argument...]
+ * set -o
+ * set +o
+ * Implementations shall support the options in both their hyphen and
+ * plus-sign forms. These options can also be specified as options to sh.
+ * Examples:
+ * Write out all variables and their values: set
+ * Set $1, $2, and $3 and set "$#" to 3: set c a b
+ * Turn on the -x and -v options: set -xv
+ * Unset all positional parameters: set --
+ * Set $1 to the value of x, even if it begins with '-' or '+': set -- "$x"
+ * Set the positional parameters to the expansion of x, even if x expands
+ * with a leading '-' or '+': set -- $x
+ *
+ * So far, we only support "set -- [argument...]" and some of the short names.
+ */
+static int FAST_FUNC builtin_set(char **argv)
+{
+	int n;
+	char **pp, **g_argv;
+	char *arg = *++argv;
+
+	if (arg == NULL) {
+		struct variable *e;
+		for (e = G.top_var; e; e = e->next)
+			puts(e->varstr);
+		return EXIT_SUCCESS;
+	}
+
+	do {
+		if (strcmp(arg, "--") == 0) {
+			++argv;
+			goto set_argv;
+		}
+		if (arg[0] != '+' && arg[0] != '-')
+			break;
+		for (n = 1; arg[n]; ++n) {
+			if (set_mode((arg[0] == '-'), arg[n], argv[1])) {
+				bb_error_msg("%s: %s: invalid option", "set", arg);
+				return EXIT_FAILURE;
+			}
+			if (arg[n] == 'o' && argv[1])
+				argv++;
+		}
+	} while ((arg = *++argv) != NULL);
+	/* Now argv[0] is 1st argument */
+
+	if (arg == NULL)
+		return EXIT_SUCCESS;
+ set_argv:
+
+	/* NB: G.global_argv[0] ($0) is never freed/changed */
+	g_argv = G.global_argv;
+	if (G.global_args_malloced) {
+		pp = g_argv;
+		while (*++pp)
+			free(*pp);
+		g_argv[1] = NULL;
+	} else {
+		G.global_args_malloced = 1;
+		pp = xzalloc(sizeof(pp[0]) * 2);
+		pp[0] = g_argv[0]; /* retain $0 */
+		g_argv = pp;
+	}
+	/* This realloc's G.global_argv */
+	G.global_argv = pp = add_strings_to_strings(g_argv, argv, /*dup:*/ 1);
+
+	G.global_argc = 1 + string_array_len(pp + 1);
+
+	return EXIT_SUCCESS;
+}
+#endif
+
+static int FAST_FUNC builtin_shift(char **argv)
+{
+	int n = 1;
+	argv = skip_dash_dash(argv);
+	if (argv[0]) {
+		n = bb_strtou(argv[0], NULL, 10);
+		if (errno || n < 0) {
+			/* shared string with ash.c */
+			bb_error_msg("Illegal number: %s", argv[0]);
+			/*
+			 * ash aborts in this case.
+			 * bash prints error message and set $? to 1.
+			 * Interestingly, for "shift 99999" bash does not
+			 * print error message, but does set $? to 1
+			 * (and does no shifting at all).
+			 */
+		}
+	}
+	if (n >= 0 && n < G.global_argc) {
+		if (G_global_args_malloced) {
+			int m = 1;
+			while (m <= n)
+				free(G.global_argv[m++]);
+		}
+		G.global_argc -= n;
+		memmove(&G.global_argv[1], &G.global_argv[n+1],
+				G.global_argc * sizeof(G.global_argv[0]));
+		return EXIT_SUCCESS;
+	}
+	return EXIT_FAILURE;
+}
+
+#if ENABLE_HUSH_GETOPTS
+static int FAST_FUNC builtin_getopts(char **argv)
+{
+/* http://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html
+
+TODO:
+If a required argument is not found, and getopts is not silent,
+a question mark (?) is placed in VAR, OPTARG is unset, and a
+diagnostic message is printed.  If getopts is silent, then a
+colon (:) is placed in VAR and OPTARG is set to the option
+character found.
+
+Test that VAR is a valid variable name?
+
+"Whenever the shell is invoked, OPTIND shall be initialized to 1"
+*/
+	char cbuf[2];
+	const char *cp, *optstring, *var;
+	int c, n, exitcode, my_opterr;
+	unsigned count;
+
+	optstring = *++argv;
+	if (!optstring || !(var = *++argv)) {
+		bb_simple_error_msg("usage: getopts OPTSTRING VAR [ARGS]");
+		return EXIT_FAILURE;
+	}
+
+	if (argv[1])
+		argv[0] = G.global_argv[0]; /* for error messages in getopt() */
+	else
+		argv = G.global_argv;
+	cbuf[1] = '\0';
+
+	my_opterr = 0;
+	if (optstring[0] != ':') {
+		cp = get_local_var_value("OPTERR");
+		/* 0 if "OPTERR=0", 1 otherwise */
+		my_opterr = (!cp || NOT_LONE_CHAR(cp, '0'));
+	}
+
+	/* getopts stops on first non-option. Add "+" to force that */
+	/*if (optstring[0] != '+')*/ {
+		char *s = alloca(strlen(optstring) + 2);
+		sprintf(s, "+%s", optstring);
+		optstring = s;
+	}
+
+	/* Naively, now we should just
+	 *	cp = get_local_var_value("OPTIND");
+	 *	optind = cp ? atoi(cp) : 0;
+	 *	optarg = NULL;
+	 *	opterr = my_opterr;
+	 *	c = getopt(string_array_len(argv), argv, optstring);
+	 * and be done? Not so fast...
+	 * Unlike normal getopt() usage in C programs, here
+	 * each successive call will (usually) have the same argv[] CONTENTS,
+	 * but not the ADDRESSES. Worse yet, it's possible that between
+	 * invocations of "getopts", there will be calls to shell builtins
+	 * which use getopt() internally. Example:
+	 *	while getopts "abc" RES -a -bc -abc de; do
+	 *		unset -ff func
+	 *	done
+	 * This would not work correctly: getopt() call inside "unset"
+	 * modifies internal libc state which is tracking position in
+	 * multi-option strings ("-abc"). At best, it can skip options
+	 * or return the same option infinitely. With glibc implementation
+	 * of getopt(), it would use outright invalid pointers and return
+	 * garbage even _without_ "unset" mangling internal state.
+	 *
+	 * We resort to resetting getopt() state and calling it N times,
+	 * until we get Nth result (or failure).
+	 * (N == G.getopt_count is reset to 0 whenever OPTIND is [un]set).
+	 */
+	GETOPT_RESET();
+	count = 0;
+	n = string_array_len(argv);
+	do {
+		optarg = NULL;
+		opterr = (count < G.getopt_count) ? 0 : my_opterr;
+		c = getopt(n, argv, optstring);
+		if (c < 0)
+			break;
+		count++;
+	} while (count <= G.getopt_count);
+
+	/* Set OPTIND. Prevent resetting of the magic counter! */
+	set_local_var_from_halves("OPTIND", utoa(optind));
+	G.getopt_count = count; /* "next time, give me N+1'th result" */
+	GETOPT_RESET(); /* just in case */
+
+	/* Set OPTARG */
+	/* Always set or unset, never left as-is, even on exit/error:
+	 * "If no option was found, or if the option that was found
+	 * does not have an option-argument, OPTARG shall be unset."
+	 */
+	cp = optarg;
+	if (c == '?') {
+		/* If ":optstring" and unknown option is seen,
+		 * it is stored to OPTARG.
+		 */
+		if (optstring[1] == ':') {
+			cbuf[0] = optopt;
+			cp = cbuf;
+		}
+	}
+	if (cp)
+		set_local_var_from_halves("OPTARG", cp);
+	else
+		unset_local_var("OPTARG");
+
+	/* Convert -1 to "?" */
+	exitcode = EXIT_SUCCESS;
+	if (c < 0) { /* -1: end of options */
+		exitcode = EXIT_FAILURE;
+		c = '?';
+	}
+
+	/* Set VAR */
+	cbuf[0] = c;
+	set_local_var_from_halves(var, cbuf);
+
+	return exitcode;
+}
+#endif
+
+static int FAST_FUNC builtin_source(char **argv)
+{
+	char *arg_path, *filename;
+	HFILE *input;
+	save_arg_t sv;
+	char *args_need_save;
+#if ENABLE_HUSH_FUNCTIONS
+	smallint sv_flg;
+#endif
+
+	argv = skip_dash_dash(argv);
+	filename = argv[0];
+	if (!filename) {
+		/* bash says: "bash: .: filename argument required" */
+		return 2; /* bash compat */
+	}
+	arg_path = NULL;
+	if (!strchr(filename, '/')) {
+		arg_path = find_in_path(filename);
+		if (arg_path)
+			filename = arg_path;
+		else if (!ENABLE_HUSH_BASH_SOURCE_CURDIR) {
+			errno = ENOENT;
+			bb_simple_perror_msg(filename);
+			return EXIT_FAILURE;
+		}
+	}
+	input = hfopen(filename);
+	free(arg_path);
+	if (!input) {
+		bb_perror_msg("%s", filename);
+		/* POSIX: non-interactive shell should abort here,
+		 * not merely fail. So far no one complained :)
+		 */
+		return EXIT_FAILURE;
+	}
+
+#if ENABLE_HUSH_FUNCTIONS
+	sv_flg = G_flag_return_in_progress;
+	/* "we are inside sourced file, ok to use return" */
+	G_flag_return_in_progress = -1;
+#endif
+	args_need_save = argv[1]; /* used as a boolean variable */
+	if (args_need_save)
+		save_and_replace_G_args(&sv, argv);
+
+	/* "false; . ./empty_line; echo Zero:$?" should print 0 */
+	G.last_exitcode = 0;
+	parse_and_run_file(input);
+	hfclose(input);
+
+	if (args_need_save) /* can't use argv[1] instead: "shift" can mangle it */
+		restore_G_args(&sv, argv);
+#if ENABLE_HUSH_FUNCTIONS
+	G_flag_return_in_progress = sv_flg;
+#endif
+
+	return G.last_exitcode;
+}
+
+#if ENABLE_HUSH_TRAP
+static int FAST_FUNC builtin_trap(char **argv)
+{
+	int sig;
+	char *new_cmd;
+
+	if (!G_traps)
+		G_traps = xzalloc(sizeof(G_traps[0]) * NSIG);
+
+	argv++;
+	if (!*argv) {
+		int i;
+		/* No args: print all trapped */
+		for (i = 0; i < NSIG; ++i) {
+			if (G_traps[i]) {
+				printf("trap -- ");
+				print_escaped(G_traps[i]);
+				/* note: bash adds "SIG", but only if invoked
+				 * as "bash". If called as "sh", or if set -o posix,
+				 * then it prints short signal names.
+				 * We are printing short names: */
+				printf(" %s\n", get_signame(i));
+			}
+		}
+		/*fflush_all(); - done after each builtin anyway */
+		return EXIT_SUCCESS;
+	}
+
+	new_cmd = NULL;
+	/* If first arg is a number: reset all specified signals */
+	sig = bb_strtou(*argv, NULL, 10);
+	if (errno == 0) {
+		int ret;
+ process_sig_list:
+		ret = EXIT_SUCCESS;
+		while (*argv) {
+			sighandler_t handler;
+
+			sig = get_signum(*argv++);
+			if (sig < 0) {
+				ret = EXIT_FAILURE;
+				/* Mimic bash message exactly */
+				bb_error_msg("trap: %s: invalid signal specification", argv[-1]);
+				continue;
+			}
+
+			free(G_traps[sig]);
+			G_traps[sig] = xstrdup(new_cmd);
+
+			debug_printf("trap: setting SIG%s (%i) to '%s'\n",
+				get_signame(sig), sig, G_traps[sig]);
+
+			/* There is no signal for 0 (EXIT) */
+			if (sig == 0)
+				continue;
+
+			if (new_cmd)
+				handler = (new_cmd[0] ? record_pending_signo : SIG_IGN);
+			else
+				/* We are removing trap handler */
+				handler = pick_sighandler(sig);
+			install_sighandler(sig, handler);
+		}
+		return ret;
+	}
+
+	if (!argv[1]) { /* no second arg */
+		bb_simple_error_msg("trap: invalid arguments");
+		return EXIT_FAILURE;
+	}
+
+	/* First arg is "-": reset all specified to default */
+	/* First arg is "--": skip it, the rest is "handler SIGs..." */
+	/* Everything else: set arg as signal handler
+	 * (includes "" case, which ignores signal) */
+	if (argv[0][0] == '-') {
+		if (argv[0][1] == '\0') { /* "-" */
+			/* new_cmd remains NULL: "reset these sigs" */
+			goto reset_traps;
+		}
+		if (argv[0][1] == '-' && argv[0][2] == '\0') { /* "--" */
+			argv++;
+		}
+		/* else: "-something", no special meaning */
+	}
+	new_cmd = *argv;
+ reset_traps:
+	argv++;
+	goto process_sig_list;
+}
+#endif
+
+#if ENABLE_HUSH_JOB
+static struct pipe *parse_jobspec(const char *str)
+{
+	struct pipe *pi;
+	unsigned jobnum;
+
+	if (sscanf(str, "%%%u", &jobnum) != 1) {
+		if (str[0] != '%'
+		 || (str[1] != '%' && str[1] != '+' && str[1] != '\0')
+		) {
+			bb_error_msg("bad argument '%s'", str);
+			return NULL;
+		}
+		/* It is "%%", "%+" or "%" - current job */
+		jobnum = G.last_jobid;
+		if (jobnum == 0) {
+			bb_simple_error_msg("no current job");
+			return NULL;
+		}
+	}
+	for (pi = G.job_list; pi; pi = pi->next) {
+		if (pi->jobid == jobnum) {
+			return pi;
+		}
+	}
+	bb_error_msg("%u: no such job", jobnum);
+	return NULL;
+}
+
+static int FAST_FUNC builtin_jobs(char **argv UNUSED_PARAM)
+{
+	struct pipe *job;
+	const char *status_string;
+
+	checkjobs(NULL, 0 /*(no pid to wait for)*/);
+	for (job = G.job_list; job; job = job->next) {
+		if (job->alive_cmds == job->stopped_cmds)
+			status_string = "Stopped";
+		else
+			status_string = "Running";
+
+		printf(JOB_STATUS_FORMAT, job->jobid, status_string, job->cmdtext);
+	}
+
+	clean_up_last_dead_job();
+
+	return EXIT_SUCCESS;
+}
+
+/* built-in 'fg' and 'bg' handler */
+static int FAST_FUNC builtin_fg_bg(char **argv)
+{
+	int i;
+	struct pipe *pi;
+
+	if (!G_interactive_fd)
+		return EXIT_FAILURE;
+
+	/* If they gave us no args, assume they want the last backgrounded task */
+	if (!argv[1]) {
+		for (pi = G.job_list; pi; pi = pi->next) {
+			if (pi->jobid == G.last_jobid) {
+				goto found;
+			}
+		}
+		bb_error_msg("%s: no current job", argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	pi = parse_jobspec(argv[1]);
+	if (!pi)
+		return EXIT_FAILURE;
+ found:
+	/* TODO: bash prints a string representation
+	 * of job being foregrounded (like "sleep 1 | cat") */
+	if (argv[0][0] == 'f' && G_saved_tty_pgrp) {
+		/* Put the job into the foreground.  */
+		tcsetpgrp(G_interactive_fd, pi->pgrp);
+	}
+
+	/* Restart the processes in the job */
+	debug_printf_jobs("reviving %d procs, pgrp %d\n", pi->num_cmds, pi->pgrp);
+	for (i = 0; i < pi->num_cmds; i++) {
+		debug_printf_jobs("reviving pid %d\n", pi->cmds[i].pid);
+	}
+	pi->stopped_cmds = 0;
+
+	i = kill(- pi->pgrp, SIGCONT);
+	if (i < 0) {
+		if (errno == ESRCH) {
+			delete_finished_job(pi);
+			return EXIT_SUCCESS;
+		}
+		bb_simple_perror_msg("kill (SIGCONT)");
+	}
+
+	if (argv[0][0] == 'f') {
+		remove_job_from_table(pi); /* FG job shouldn't be in job table */
+		return checkjobs_and_fg_shell(pi);
+	}
+	return EXIT_SUCCESS;
+}
+#endif
+
+#if ENABLE_HUSH_KILL
+static int FAST_FUNC builtin_kill(char **argv)
+{
+	int ret = 0;
+
+# if ENABLE_HUSH_JOB
+	if (argv[1] && strcmp(argv[1], "-l") != 0) {
+		int i = 1;
+
+		do {
+			struct pipe *pi;
+			char *dst;
+			int j, n;
+
+			if (argv[i][0] != '%')
+				continue;
+			/*
+			 * "kill %N" - job kill
+			 * Converting to pgrp / pid kill
+			 */
+			pi = parse_jobspec(argv[i]);
+			if (!pi) {
+				/* Eat bad jobspec */
+				j = i;
+				do {
+					j++;
+					argv[j - 1] = argv[j];
+				} while (argv[j]);
+				ret = 1;
+				i--;
+				continue;
+			}
+			/*
+			 * In jobs started under job control, we signal
+			 * entire process group by kill -PGRP_ID.
+			 * This happens, f.e., in interactive shell.
+			 *
+			 * Otherwise, we signal each child via
+			 * kill PID1 PID2 PID3.
+			 * Testcases:
+			 * sh -c 'sleep 1|sleep 1 & kill %1'
+			 * sh -c 'true|sleep 2 & sleep 1; kill %1'
+			 * sh -c 'true|sleep 1 & sleep 2; kill %1'
+			 */
+			n = G_interactive_fd ? 1 : pi->num_cmds;
+			dst = alloca(n * sizeof(int)*4);
+			argv[i] = dst;
+			if (G_interactive_fd)
+				dst += sprintf(dst, " -%u", (int)pi->pgrp);
+			else for (j = 0; j < n; j++) {
+				struct command *cmd = &pi->cmds[j];
+				/* Skip exited members of the job */
+				if (cmd->pid == 0)
+					continue;
+				/*
+				 * kill_main has matching code to expect
+				 * leading space. Needed to not confuse
+				 * negative pids with "kill -SIGNAL_NO" syntax
+				 */
+				dst += sprintf(dst, " %u", (int)cmd->pid);
+			}
+			*dst = '\0';
+		} while (argv[++i]);
+	}
+# endif
+
+	if (argv[1] || ret == 0) {
+		ret = run_applet_main(argv, kill_main);
+	}
+	/* else: ret = 1, "kill %bad_jobspec" case */
+	return ret;
+}
+#endif
+
+#if ENABLE_HUSH_WAIT
+/* http://www.opengroup.org/onlinepubs/9699919799/utilities/wait.html */
+# if !ENABLE_HUSH_JOB
+#  define wait_for_child_or_signal(pipe,pid) wait_for_child_or_signal(pid)
+# endif
+static int wait_for_child_or_signal(struct pipe *waitfor_pipe, pid_t waitfor_pid)
+{
+	int ret = 0;
+	for (;;) {
+		int sig;
+		sigset_t oldset;
+
+		if (!sigisemptyset(&G.pending_set))
+			goto check_sig;
+
+		/* waitpid is not interruptible by SA_RESTARTed
+		 * signals which we use. Thus, this ugly dance:
+		 */
+
+		/* Make sure possible SIGCHLD is stored in kernel's
+		 * pending signal mask before we call waitpid.
+		 * Or else we may race with SIGCHLD, lose it,
+		 * and get stuck in sigsuspend...
+		 */
+		sigfillset(&oldset); /* block all signals, remember old set */
+		sigprocmask2(SIG_SETMASK, &oldset);
+
+		if (!sigisemptyset(&G.pending_set)) {
+			/* Crap! we raced with some signal! */
+			goto restore;
+		}
+
+		/*errno = 0; - checkjobs does this */
+/* Can't pass waitfor_pipe into checkjobs(): it won't be interruptible */
+		ret = checkjobs(NULL, waitfor_pid); /* waitpid(WNOHANG) inside */
+		debug_printf_exec("checkjobs:%d\n", ret);
+# if ENABLE_HUSH_JOB
+		if (waitfor_pipe) {
+			int rcode = job_exited_or_stopped(waitfor_pipe);
+			debug_printf_exec("job_exited_or_stopped:%d\n", rcode);
+			if (rcode >= 0) {
+				ret = rcode;
+				sigprocmask(SIG_SETMASK, &oldset, NULL);
+				break;
+			}
+		}
+# endif
+		/* if ECHILD, there are no children (ret is -1 or 0) */
+		/* if ret == 0, no children changed state */
+		/* if ret != 0, it's exitcode+1 of exited waitfor_pid child */
+		if (errno == ECHILD || ret) {
+			ret--;
+			if (ret < 0) /* if ECHILD, may need to fix "ret" */
+				ret = 0;
+# if ENABLE_HUSH_BASH_COMPAT
+			if (waitfor_pid == -1 && errno == ECHILD) {
+				/* exitcode of "wait -n" with no children is 127, not 0 */
+				ret = 127;
+			}
+# endif
+			sigprocmask(SIG_SETMASK, &oldset, NULL);
+			break;
+		}
+		/* Wait for SIGCHLD or any other signal */
+		/* It is vitally important for sigsuspend that SIGCHLD has non-DFL handler! */
+		/* Note: sigsuspend invokes signal handler */
+		sigsuspend(&oldset);
+		/* ^^^ add "sigdelset(&oldset, SIGCHLD)" before sigsuspend
+		 * to make sure SIGCHLD is not masked off?
+		 * It was reported that this:
+		 *	fn() { : | return; }
+		 *	shopt -s lastpipe
+		 *	fn
+		 *	exec hush SCRIPT
+		 * under bash 4.4.23 runs SCRIPT with SIGCHLD masked,
+		 * making "wait" commands in SCRIPT block forever.
+		 */
+ restore:
+		sigprocmask(SIG_SETMASK, &oldset, NULL);
+ check_sig:
+		/* So, did we get a signal? */
+		sig = check_and_run_traps();
+		if (sig /*&& sig != SIGCHLD - always true */) {
+			/* Do this for any (non-ignored) signal, not only for ^C */
+			ret = 128 | sig;
+			break;
+		}
+		/* SIGCHLD, or no signal, or ignored one, such as SIGQUIT. Repeat */
+	}
+	return ret;
+}
+
+static int FAST_FUNC builtin_wait(char **argv)
+{
+	int ret;
+	int status;
+
+	argv = skip_dash_dash(argv);
+# if ENABLE_HUSH_BASH_COMPAT
+	if (argv[0] && strcmp(argv[0], "-n") == 0) {
+		/* wait -n */
+		/* (bash accepts "wait -n PID" too and ignores PID) */
+		G.dead_job_exitcode = -1;
+		return wait_for_child_or_signal(NULL, -1 /*no job, wait for one job*/);
+	}
+# endif
+	if (argv[0] == NULL) {
+		/* Don't care about wait results */
+		/* Note 1: must wait until there are no more children */
+		/* Note 2: must be interruptible */
+		/* Examples:
+		 * $ sleep 3 & sleep 6 & wait
+		 * [1] 30934 sleep 3
+		 * [2] 30935 sleep 6
+		 * [1] Done                   sleep 3
+		 * [2] Done                   sleep 6
+		 * $ sleep 3 & sleep 6 & wait
+		 * [1] 30936 sleep 3
+		 * [2] 30937 sleep 6
+		 * [1] Done                   sleep 3
+		 * ^C <-- after ~4 sec from keyboard
+		 * $
+		 */
+		return wait_for_child_or_signal(NULL, 0 /*no job and no pid to wait for*/);
+	}
+
+	do {
+		pid_t pid = bb_strtou(*argv, NULL, 10);
+		if (errno || pid <= 0) {
+# if ENABLE_HUSH_JOB
+			if (argv[0][0] == '%') {
+				struct pipe *wait_pipe;
+				ret = 127; /* bash compat for bad jobspecs */
+				wait_pipe = parse_jobspec(*argv);
+				if (wait_pipe) {
+					ret = job_exited_or_stopped(wait_pipe);
+					if (ret < 0) {
+						ret = wait_for_child_or_signal(wait_pipe, 0);
+					} else {
+						/* waiting on "last dead job" removes it */
+						clean_up_last_dead_job();
+					}
+				}
+				/* else: parse_jobspec() already emitted error msg */
+				continue;
+			}
+# endif
+			/* mimic bash message */
+			bb_error_msg("wait: '%s': not a pid or valid job spec", *argv);
+			ret = EXIT_FAILURE;
+			continue; /* bash checks all argv[] */
+		}
+
+		/* Do we have such child? */
+		ret = waitpid(pid, &status, WNOHANG);
+		if (ret < 0) {
+			/* No */
+			ret = 127;
+			if (errno == ECHILD) {
+				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.
+					 * In dash, it is 127.
+					 */
+					ret = G.last_bg_pid_exitcode;
+				} else {
+					/* Example: "wait 1". mimic bash message */
+					bb_error_msg("wait: pid %u is not a child of this shell", (unsigned)pid);
+				}
+			} else {
+				/* ??? */
+				bb_perror_msg("wait %s", *argv);
+			}
+			continue; /* bash checks all argv[] */
+		}
+		if (ret == 0) {
+			/* Yes, and it still runs */
+			ret = wait_for_child_or_signal(NULL, pid);
+		} else {
+			/* Yes, and it just exited */
+			process_wait_result(NULL, pid, status);
+			ret = WEXITSTATUS(status);
+			if (WIFSIGNALED(status))
+				ret = 128 | WTERMSIG(status);
+		}
+	} while (*++argv);
+
+	return ret;
+}
+#endif
+
+#if ENABLE_HUSH_LOOPS || ENABLE_HUSH_FUNCTIONS
+static unsigned parse_numeric_argv1(char **argv, unsigned def, unsigned def_min)
+{
+	if (argv[1]) {
+		def = bb_strtou(argv[1], NULL, 10);
+		if (errno || def < def_min || argv[2]) {
+			bb_error_msg("%s: bad arguments", argv[0]);
+			def = UINT_MAX;
+		}
+	}
+	return def;
+}
+#endif
+
+#if ENABLE_HUSH_LOOPS
+static int FAST_FUNC builtin_break(char **argv)
+{
+	unsigned depth;
+	if (G.depth_of_loop == 0) {
+		bb_error_msg("%s: only meaningful in a loop", argv[0]);
+		/* if we came from builtin_continue(), need to undo "= 1" */
+		G.flag_break_continue = 0;
+		return EXIT_SUCCESS; /* bash compat */
+	}
+	G.flag_break_continue++; /* BC_BREAK = 1, or BC_CONTINUE = 2 */
+
+	G.depth_break_continue = depth = parse_numeric_argv1(argv, 1, 1);
+	if (depth == UINT_MAX)
+		G.flag_break_continue = BC_BREAK;
+	if (G.depth_of_loop < depth)
+		G.depth_break_continue = G.depth_of_loop;
+
+	return EXIT_SUCCESS;
+}
+
+static int FAST_FUNC builtin_continue(char **argv)
+{
+	G.flag_break_continue = 1; /* BC_CONTINUE = 2 = 1+1 */
+	return builtin_break(argv);
+}
+#endif
+
+#if ENABLE_HUSH_FUNCTIONS
+static int FAST_FUNC builtin_return(char **argv)
+{
+	int rc;
+
+	if (G_flag_return_in_progress != -1) {
+		bb_error_msg("%s: not in a function or sourced script", argv[0]);
+		return EXIT_FAILURE; /* bash compat */
+	}
+
+	G_flag_return_in_progress = 1;
+
+	/* bash:
+	 * out of range: wraps around at 256, does not error out
+	 * non-numeric param:
+	 * f() { false; return qwe; }; f; echo $?
+	 * bash: return: qwe: numeric argument required  <== we do this
+	 * 255  <== we also do this
+	 */
+	rc = parse_numeric_argv1(argv, G.last_exitcode, 0);
+# if ENABLE_HUSH_TRAP
+	if (argv[1]) { /* "return ARG" inside a running trap sets $? */
+		debug_printf_exec("G.return_exitcode=%d\n", rc);
+		G.return_exitcode = rc;
+	}
+# endif
+	return rc;
+}
+#endif
+
+#if ENABLE_HUSH_TIMES
+static int FAST_FUNC builtin_times(char **argv UNUSED_PARAM)
+{
+	static const uint8_t times_tbl[] ALIGN1 = {
+		' ',  offsetof(struct tms, tms_utime),
+		'\n', offsetof(struct tms, tms_stime),
+		' ',  offsetof(struct tms, tms_cutime),
+		'\n', offsetof(struct tms, tms_cstime),
+		0
+	};
+	const uint8_t *p;
+	unsigned clk_tck;
+	struct tms buf;
+
+	clk_tck = bb_clk_tck();
+
+	times(&buf);
+	p = times_tbl;
+	do {
+		unsigned sec, frac;
+		unsigned long t;
+		t = *(clock_t *)(((char *) &buf) + p[1]);
+		sec = t / clk_tck;
+		frac = t % clk_tck;
+		printf("%um%u.%03us%c",
+			sec / 60, sec % 60,
+			(frac * 1000) / clk_tck,
+			p[0]);
+		p += 2;
+	} while (*p);
+
+	return EXIT_SUCCESS;
+}
+#endif
+
+#if ENABLE_HUSH_MEMLEAK
+static int FAST_FUNC builtin_memleak(char **argv UNUSED_PARAM)
+{
+	void *p;
+	unsigned long l;
+
+# ifdef M_TRIM_THRESHOLD
+	/* Optional. Reduces probability of false positives */
+	malloc_trim(0);
+# endif
+	/* Crude attempt to find where "free memory" starts,
+	 * sans fragmentation. */
+	p = malloc(240);
+	l = (unsigned long)p;
+	free(p);
+	p = malloc(3400);
+	if (l < (unsigned long)p) l = (unsigned long)p;
+	free(p);
+
+
+# if 0  /* debug */
+	{
+		struct mallinfo mi = mallinfo();
+		printf("top alloc:0x%lx malloced:%d+%d=%d\n", l,
+			mi.arena, mi.hblkhd, mi.arena + mi.hblkhd);
+	}
+# endif
+
+	if (!G.memleak_value)
+		G.memleak_value = l;
+
+	l -= G.memleak_value;
+	if ((long)l < 0)
+		l = 0;
+	l /= 1024;
+	if (l > 127)
+		l = 127;
+
+	/* Exitcode is "how many kilobytes we leaked since 1st call" */
+	return l;
+}
+#endif
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 08/21] cli: Port Busybox 2021 hush to U-Boot.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (6 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 07/21] cli: Add Busybox upstream hush.c file Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 09/21] cli: Add choice for hush parser Francis Laniel
                   ` (13 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit adds new file cli_hush_2021.c, it is a copy of Busybox hush file as
it was of time to commit 37460f5da.
This commit modifies Busybox hush to not compile some part specific to Busybox
and adds some code
needed by U-Boot.
The modifications consists mainly on adding code #if(n)def guards.

For the moment, this refurbished flavor of hush only permits running command
without any keywords (i.e., if and for are not recognized) or variable expansion
(i.e., echo $foo prints foo and not value stored in variable foo).

A new file was also added to define some functions specific to U-Boot.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
Signed-off-by: Harald Seiler <hws@denx.de>
---
 common/cli_hush_2021.c          | 292 ++++++++++++++++++++
 common/cli_hush_2021_upstream.c | 470 +++++++++++++++++++++++++++++++-
 2 files changed, 761 insertions(+), 1 deletion(-)
 create mode 100644 common/cli_hush_2021.c

diff --git a/common/cli_hush_2021.c b/common/cli_hush_2021.c
new file mode 100644
index 0000000000..52e249d8be
--- /dev/null
+++ b/common/cli_hush_2021.c
@@ -0,0 +1,292 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * This file defines the compilation unit for the new hush shell version.  The
+ * actual implementation from upstream BusyBox can be found in
+ * `cli_hush_2021_upstream.c` which is included at the end of this file.
+ *
+ * This "wrapper" technique is used to keep the changes to the upstream version
+ * as minmal as possible.  Instead, all defines and redefines necessary are done
+ * here, outside the upstream sources.  This will hopefully make upgrades to
+ * newer revisions much easier.
+ *
+ * Copyright (c) 2021, Harald Seiler, DENX Software Engineering, hws@denx.de
+ */
+
+#include <common.h>         /* readline */
+#include <env.h>
+#include <malloc.h>         /* malloc, free, realloc*/
+#include <linux/ctype.h>    /* isalpha, isdigit */
+#include <console.h>
+#include <bootretry.h>
+#include <cli.h>
+#include <cli_hush.h>
+#include <command.h>        /* find_cmd */
+#include <asm/global_data.h>
+
+/*
+ * BusyBox Version: UPDATE THIS WHEN PULLING NEW UPSTREAM REVISION!
+ */
+#define BB_VER			"1.34.0.git37460f5daff9"
+
+/*
+ * Define hush features by the names used upstream.
+ */
+#define ENABLE_HUSH_INTERACTIVE	1
+#define ENABLE_FEATURE_EDITING	1
+#define ENABLE_HUSH_IF		1
+#define ENABLE_HUSH_LOOPS	1
+/* No MMU in U-Boot */
+#define BB_MMU			0
+#define USE_FOR_NOMMU(...)	__VA_ARGS__
+#define USE_FOR_MMU(...)
+
+/* TODO: Drop this before upstreaming */
+#ifdef CONFIG_SYS_PROMPT
+#undef CONFIG_SYS_PROMPT
+#define CONFIG_SYS_PROMPT	"2021> "
+#endif /* CONFIG_SYS_PROMPT */
+
+/*
+ * Size-saving "small" ints (arch-dependent)
+ */
+#if CONFIG_IS_ENABLED(X86) || CONFIG_IS_ENABLED(X86_64) || CONFIG_IS_ENABLED(MIPS)
+/* add other arches which benefit from this... */
+typedef signed char smallint;
+typedef unsigned char smalluint;
+#else
+/* for arches where byte accesses generate larger code: */
+typedef int smallint;
+typedef unsigned smalluint;
+#endif
+
+/*
+ * Alignment defines used by BusyBox.
+ */
+#define ALIGN1			__attribute__((aligned(1)))
+#define ALIGN2			__attribute__((aligned(2)))
+#define ALIGN4			__attribute__((aligned(4)))
+#define ALIGN8			__attribute__((aligned(8)))
+#define ALIGN_PTR		__attribute__((aligned(sizeof(void*))))
+
+/*
+ * Miscellaneous compiler/platform defines.
+ */
+#define FAST_FUNC /* not used in U-Boot */
+#define UNUSED_PARAM		__always_unused
+#define ALWAYS_INLINE		__always_inline
+#define NOINLINE		noinline
+
+/*
+ * Defines to provide equivalents to what libc/BusyBox defines.
+ */
+#define EOF			(-1)
+#define EXIT_SUCCESS		0
+#define EXIT_FAILURE		1
+
+/*
+ * Stubs to provide libc/BusyBox functions based on U-Boot equivalents where it
+ * makes sense.
+ */
+#define utoa			simple_itoa
+
+static void __noreturn xfunc_die(void)
+{
+	panic("HUSH died!");
+}
+
+#define bb_error_msg_and_die(format, ...) do { \
+panic("HUSH: " format, __VA_ARGS__); \
+} while (0);
+
+#define bb_simple_error_msg_and_die(msg) do { \
+panic_str("HUSH: " msg); \
+} while (0);
+
+/* fdprintf() is used for debug output. */
+static int __maybe_unused fdprintf(int fd, const char *format, ...)
+{
+	va_list args;
+	uint i;
+
+	assert(fd == 2);
+
+	va_start(args, format);
+	i = vprintf(format, args);
+	va_end(args);
+
+	return i;
+}
+
+static void bb_verror_msg(const char *s, va_list p, const char* strerr)
+{
+	/* TODO: what to do with strerr arg? */
+	vprintf(s, p);
+}
+
+static void bb_error_msg(const char *s, ...)
+{
+	va_list p;
+
+	va_start(p, s);
+	bb_verror_msg(s, p, NULL);
+	va_end(p);
+}
+
+static void bb_simple_error_msg(const char *s)
+{
+	bb_error_msg("%s", s);
+}
+
+static void *xmalloc(size_t size)
+{
+	void *p = NULL;
+	if (!(p = malloc(size)))
+		panic("out of memory");
+	return p;
+}
+
+static void *xzalloc(size_t size)
+{
+	void *p = xmalloc(size);
+	memset(p, 0, size);
+	return p;
+}
+
+static void *xrealloc(void *ptr, size_t size)
+{
+	void *p = NULL;
+	if (!(p = realloc(ptr, size)))
+		panic("out of memory");
+	return p;
+}
+
+static void* xmemdup(const void *s, int n)
+{
+	return memcpy(xmalloc(n), s, n);
+}
+
+#define xstrdup		strdup
+#define xstrndup	strndup
+
+static void *mempcpy(void *dest, const void *src, size_t len)
+{
+	return memcpy(dest, src, len) + len;
+}
+
+/* Like strcpy but can copy overlapping strings. */
+static void overlapping_strcpy(char *dst, const char *src)
+{
+	/*
+	 * Cheap optimization for dst == src case -
+	 * better to have it here than in many callers.
+	 */
+	if (dst != src) {
+		while ((*dst = *src) != '\0') {
+			dst++;
+			src++;
+		}
+	}
+}
+
+static char* skip_whitespace(const char *s)
+{
+	/*
+	 * In POSIX/C locale (the only locale we care about: do we REALLY want
+	 * to allow Unicode whitespace in, say, .conf files? nuts!)
+	 * isspace is only these chars: "\t\n\v\f\r" and space.
+	 * "\t\n\v\f\r" happen to have ASCII codes 9,10,11,12,13.
+	 * Use that.
+	 */
+	while (*s == ' ' || (unsigned char)(*s - 9) <= (13 - 9))
+		s++;
+
+	return (char *) s;
+}
+
+static char* skip_non_whitespace(const char *s)
+{
+	while (*s != '\0' && *s != ' ' && (unsigned char)(*s - 9) > (13 - 9))
+		s++;
+
+	return (char *) s;
+}
+
+#define is_name(c)	((c) == '_' || isalpha((unsigned char)(c)))
+#define is_in_name(c)	((c) == '_' || isalnum((unsigned char)(c)))
+
+static const char* endofname(const char *name)
+{
+	if (!is_name(*name))
+		return name;
+	while (*++name) {
+		if (!is_in_name(*name))
+			break;
+	}
+	return name;
+}
+
+struct in_str;
+static int u_boot_cli_readline(struct in_str *i);
+
+struct in_str;
+static int u_boot_cli_readline(struct in_str *i);
+
+/*
+ * BusyBox globals which are needed for hush.
+ */
+static uint8_t xfunc_error_retval;
+
+static const char defifsvar[] __aligned(1) = "IFS= \t\n";
+#define defifs (defifsvar + 4)
+
+/*
+ * This define is used for changes that need be done directly in the upstream
+ * sources still. Ideally, its use should be minimized as much as possible.
+ */
+#define __U_BOOT__
+
+/*
+ *
+ * +-- Include of the upstream sources --+ *
+ * V                                     V
+ */
+#include "cli_hush_2021_upstream.c"
+/*
+ * A                                     A
+ * +-- Include of the upstream sources --+ *
+ *
+ */
+
+int u_boot_hush_start(void)
+{
+	INIT_G();
+	return 0;
+}
+
+static int u_boot_cli_readline(struct in_str *i)
+{
+	char *prompt;
+	char __maybe_unused *ps_prompt = NULL;
+
+	if (!G.promptmode)
+		prompt = CONFIG_SYS_PROMPT;
+#ifdef CONFIG_SYS_PROMPT_HUSH_PS2
+	else
+		prompt = CONFIG_SYS_PROMPT_HUSH_PS2;
+#else
+	/* TODO: default value? */
+	#error "SYS_PROMPT_HUSH_PS2 is not defined!"
+#endif
+
+	if (CONFIG_IS_ENABLED(CMDLINE_PS_SUPPORT)) {
+		if (!G.promptmode)
+			ps_prompt = env_get("PS1");
+		else
+			ps_prompt = env_get("PS2");
+
+		if (ps_prompt)
+			prompt = ps_prompt;
+	}
+
+	return cli_readline(prompt);
+}
diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
index c970d9097e..06649a6011 100644
--- a/common/cli_hush_2021_upstream.c
+++ b/common/cli_hush_2021_upstream.c
@@ -343,6 +343,7 @@
 //usage:#define hush_full_usage "\n\n"
 //usage:	"Unix shell interpreter"
 
+#ifndef __U_BOOT__
 #if !(defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
 	|| defined(__APPLE__) \
     )
@@ -378,6 +379,7 @@
 #else
 # define NUM_SCRIPTS 0
 #endif
+#endif /* !__U_BOOT__ */
 
 /* So far, all bash compat is controlled by one config option */
 /* Separate defines document which part of code implements what */
@@ -421,6 +423,7 @@
 # define USE_FOR_MMU(...)
 #endif
 
+#ifndef __U_BOOT__
 #include "NUM_APPLETS.h"
 #if NUM_APPLETS == 1
 /* STANDALONE does not make sense, and won't compile */
@@ -432,6 +435,7 @@
 # define IF_FEATURE_SH_STANDALONE(...)
 # define IF_NOT_FEATURE_SH_STANDALONE(...) __VA_ARGS__
 #endif
+#endif /* __U_BOOT__ */
 
 #if !ENABLE_HUSH_INTERACTIVE
 # undef ENABLE_FEATURE_EDITING
@@ -511,6 +515,7 @@ typedef struct nommu_save_t {
 } nommu_save_t;
 #endif
 
+
 enum {
 	RES_NONE  = 0,
 #if ENABLE_HUSH_IF
@@ -594,6 +599,7 @@ typedef struct in_str {
 	HFILE *file;
 } in_str;
 
+#ifndef __U_BOOT__
 /* The descrip member of this structure is only used to make
  * debugging output pretty */
 static const struct {
@@ -640,9 +646,12 @@ typedef enum redir_type {
 	HEREDOC_QUOTED   = 2,
 } redir_type;
 
+#endif /* !__U_BOOT__ */
 
 struct command {
+#ifndef __U_BOOT__
 	pid_t pid;                  /* 0 if exited */
+#endif /* !__U_BOOT__ */
 	unsigned assignment_cnt;    /* how many argv[i] are assignments? */
 #if ENABLE_HUSH_LINENO_VAR
 	unsigned lineno;
@@ -684,6 +693,9 @@ struct command {
  * When command is freed, it severs the link
  * (sets ->child_func->parent_cmd to NULL).
  */
+#endif
+#ifdef __U_BOOT__
+	int argc; /* number of program arguments */
 #endif
 	char **argv;                /* command name and arguments */
 /* argv vector may contain variable references (^Cvar^C, ^C0^C etc)
@@ -692,15 +704,23 @@ struct command {
  * Example: argv[0]=='.^C*^C.' here: echo .$*.
  * References of the form ^C`cmd arg^C are `cmd arg` substitutions.
  */
+#ifndef __U_BOOT__
 	struct redir_struct *redirects; /* I/O redirections */
+#endif /* !__U_BOOT__ */
 };
 /* Is there anything in this command at all? */
+#ifndef __U_BOOT__
 #define IS_NULL_CMD(cmd) \
 	(!(cmd)->group && !(cmd)->argv && !(cmd)->redirects)
 
+#else /* __U_BOOT__ */
+#define IS_NULL_CMD(cmd) \
+	(!(cmd)->group && !(cmd)->argv)
+#endif /* __U_BOOT__ */
 struct pipe {
 	struct pipe *next;
 	int num_cmds;               /* total number of commands in pipe */
+#ifndef __U_BOOT__
 	int alive_cmds;             /* number of commands running (not exited) */
 	int stopped_cmds;           /* number of commands alive, but stopped */
 #if ENABLE_HUSH_JOB
@@ -708,6 +728,7 @@ struct pipe {
 	pid_t pgrp;                 /* process group ID for the job */
 	char *cmdtext;              /* name of job */
 #endif
+#endif /* !__U_BOOT__ */
 	struct command *cmds;       /* array of commands in pipe */
 	smallint followup;          /* PIPE_BG, PIPE_SEQ, PIPE_OR, PIPE_AND */
 	IF_HAS_KEYWORDS(smallint pi_inverted;) /* "! cmd | cmd" */
@@ -731,8 +752,10 @@ struct parse_context {
 	struct pipe *pipe;
 	/* last command in pipe (being constructed right now) */
 	struct command *command;
+#ifndef __U_BOOT__
 	/* last redirect in command->redirects list */
 	struct redir_struct *pending_redirect;
+#endif /* !__U_BOOT__ */
 	o_string word;
 #if !BB_MMU
 	o_string as_string;
@@ -766,19 +789,23 @@ enum {
 	WORD_IS_KEYWORD       = 3,
 };
 
+#ifndef __U_BOOT__
 /* On program start, environ points to initial environment.
  * putenv adds new pointers into it, unsetenv removes them.
  * Neither of these (de)allocates the strings.
  * setenv allocates new strings in malloc space and does putenv,
  * and thus setenv is unusable (leaky) for shell's purposes */
 #define setenv(...) setenv_is_leaky_dont_use()
+#endif /* !__U_BOOT__ */
 struct variable {
 	struct variable *next;
 	char *varstr;        /* points to "name=" portion */
 	int max_len;         /* if > 0, name is part of initial env; else name is malloced */
 	uint16_t var_nest_level;
+#ifndef __U_BOOT__
 	smallint flg_export; /* putenv should be done on this var */
 	smallint flg_read_only;
+#endif /* !__U_BOOT__ */
 };
 
 enum {
@@ -850,6 +877,7 @@ enum {
 /* "Globals" within this file */
 /* Sorted roughly by size (smaller offsets == smaller code) */
 struct globals {
+#ifndef __U_BOOT__
 	/* interactive_fd != 0 means we are an interactive shell.
 	 * If we are, then saved_tty_pgrp can also be != 0, meaning
 	 * that controlling tty is available. With saved_tty_pgrp == 0,
@@ -870,6 +898,10 @@ struct globals {
 #else
 # define G_interactive_fd 0
 #endif
+#else /* __U_BOOT__ */
+# define G_interactive_fd 0
+#endif /* __U_BOOT__ */
+#ifndef __U_BOOT__
 #if ENABLE_FEATURE_EDITING
 	line_input_t *line_input_state;
 #endif
@@ -888,8 +920,10 @@ struct globals {
 #else
 # define G_saved_tty_pgrp 0
 #endif
+#endif /* !__U_BOOT__ */
 	/* How deeply are we in context where "set -e" is ignored */
 	int errexit_depth;
+#ifndef __U_BOOT__
 	/* "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
@@ -915,13 +949,16 @@ struct globals {
 #endif
 	char opt_s;
 	char opt_c;
+#endif /* !__U_BOOT__ */
 #if ENABLE_HUSH_INTERACTIVE
 	smallint promptmode; /* 0: PS1, 1: PS2 */
 #endif
 	smallint flag_SIGINT;
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_LOOPS
 	smallint flag_break_continue;
 #endif
+#endif /* !__U_BOOT__ */
 #if ENABLE_HUSH_FUNCTIONS
 	/* 0: outside of a function (or sourced file)
 	 * -1: inside of a function, ok to use return builtin
@@ -937,6 +974,7 @@ struct globals {
 	smalluint last_exitcode;
 	smalluint expand_exitcode;
 	smalluint last_bg_pid_exitcode;
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_SET
 	/* are global_argv and global_argv[1..n] malloced? (note: not [0]) */
 	smalluint global_args_malloced;
@@ -947,6 +985,7 @@ struct globals {
 #if ENABLE_HUSH_BASH_COMPAT
 	int dead_job_exitcode; /* for "wait -n" */
 #endif
+#endif /* !__U_BOOT__ */
 	/* how many non-NULL argv's we have. NB: $# + 1 */
 	int global_argc;
 	char **global_argv;
@@ -954,19 +993,30 @@ struct globals {
 	char *argv0_for_re_execing;
 #endif
 #if ENABLE_HUSH_LOOPS
+#ifndef __U_BOOT__
 	unsigned depth_break_continue;
+#endif /* !__U_BOOT__ */
 	unsigned depth_of_loop;
 #endif
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_GETOPTS
 	unsigned getopt_count;
 #endif
+#endif /* !__U_BOOT__ */
 	const char *ifs;
+#ifdef __U_BOOT__
+	int flag_repeat;
+	int do_repeat;
+#endif /* __U_BOOT__ */
 	char *ifs_whitespace; /* = G.ifs or malloced */
+#ifndef __U_BOOT__
 	const char *cwd;
+#endif /* !__U_BOOT__ */
 	struct variable *top_var;
 	char **expanded_assignments;
 	struct variable **shadowed_vars_pp;
 	unsigned var_nest_level;
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_FUNCTIONS
 # if ENABLE_HUSH_LOCAL
 	unsigned func_nest_level; /* solely to prevent "local v" in non-functions */
@@ -1022,9 +1072,11 @@ struct globals {
 	int x_mode_fd;
 	o_string x_mode_buf;
 #endif
+#endif /* !__U_BOOT__ */
 #if HUSH_DEBUG >= 2
 	int debug_indent;
 #endif
+#ifndef __U_BOOT__
 	struct sigaction sa;
 	char optstring_buf[sizeof("eixcs")];
 #if BASH_EPOCH_VARS
@@ -1033,19 +1085,34 @@ struct globals {
 #if ENABLE_FEATURE_EDITING
 	char user_input_buf[CONFIG_FEATURE_EDITING_MAX_LEN];
 #endif
+#endif /* !__U_BOOT__ */
 };
+#ifdef __U_BOOT__
+struct globals *ptr_to_globals;
+#endif /* __U_BOOT__ */
 #define G (*ptr_to_globals)
 /* Not #defining name to G.name - this quickly gets unwieldy
  * (too many defines). Also, I actually prefer to see when a variable
  * is global, thus "G." prefix is a useful hint */
+#ifdef __U_BOOT__
+#define SET_PTR_TO_GLOBALS(x) do { \
+	(*(struct globals**)&ptr_to_globals) = (void*)(x); \
+	barrier(); \
+} while (0)
+#define INIT_G() do { \
+	SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
+} while (0)
+#else /* !__U_BOOT__ */
 #define INIT_G() do { \
 	SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
 	/* memset(&G.sa, 0, sizeof(G.sa)); */  \
 	sigfillset(&G.sa.sa_mask); \
 	G.sa.sa_flags = SA_RESTART; \
 } while (0)
+#endif /* !__U_BOOT__ */
 
 
+#ifndef __U_BOOT__
 /* Function prototypes for builtins */
 static int builtin_cd(char **argv) FAST_FUNC;
 #if ENABLE_HUSH_ECHO
@@ -1245,6 +1312,7 @@ static const struct built_in_command bltins2[] ALIGN_PTR = {
 #endif
 };
 
+#endif /* !__U_BOOT__ */
 
 /* Debug printouts.
  */
@@ -1465,7 +1533,6 @@ static void syntax_error_unexpected_ch(unsigned lineno UNUSED_PARAM, int ch)
 # define syntax_error_unexpected_ch(ch) syntax_error_unexpected_ch(__LINE__, ch)
 #endif
 
-
 /* Utility functions
  */
 /* Replace each \x with x in place, return ptr past NUL. */
@@ -1539,6 +1606,7 @@ static char **add_string_to_strings(char **strings, char *add)
 	v[1] = NULL;
 	return add_strings_to_strings(strings, v, /*dup:*/ 0);
 }
+
 #if LEAK_HUNTING
 static char **xx_add_string_to_strings(int lineno, char **strings, char *add)
 {
@@ -1564,6 +1632,7 @@ static void free_strings(char **strings)
 	free(strings);
 }
 
+#ifndef __U_BOOT__
 static int dup_CLOEXEC(int fd, int avoid_fd)
 {
 	int newfd;
@@ -1749,6 +1818,7 @@ static int fd_in_HFILEs(int fd)
 	return 0;
 }
 
+#endif /* !__U_BOOT__ */
 
 /* Helpers for setting new $n and restoring them back
  */
@@ -1756,9 +1826,12 @@ typedef struct save_arg_t {
 	char *sv_argv0;
 	char **sv_g_argv;
 	int sv_g_argc;
+#ifndef __U_BOOT__
 	IF_HUSH_SET(smallint sv_g_malloced;)
+#endif /* !__U_BOOT__ */
 } save_arg_t;
 
+#ifndef __U_BOOT__
 static void save_and_replace_G_args(save_arg_t *sv, char **argv)
 {
 	sv->sv_argv0 = argv[0];
@@ -1789,8 +1862,10 @@ static void restore_G_args(save_arg_t *sv, char **argv)
 	G.global_argc = sv->sv_g_argc;
 	IF_HUSH_SET(G.global_args_malloced = sv->sv_g_malloced;)
 }
+#endif /* !__U_BOOT__ */
 
 
+#ifndef __U_BOOT__
 /* Basic theory of signal handling in shell
  * ========================================
  * This does not describe what hush does, rather, it is current understanding
@@ -1969,7 +2044,9 @@ static sighandler_t install_sighandler(int sig, sighandler_t handler)
 	sigaction(sig, &G.sa, &old_sa);
 	return old_sa.sa_handler;
 }
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 static void hush_exit(int exitcode) NORETURN;
 
 static void restore_ttypgrp_and__exit(void) NORETURN;
@@ -2039,7 +2116,9 @@ static void sigexit(int sig)
 # define enable_restore_tty_pgrp_on_exit()  ((void)0)
 
 #endif
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 static sighandler_t pick_sighandler(unsigned sig)
 {
 	sighandler_t handler = SIG_DFL;
@@ -2066,7 +2145,9 @@ static sighandler_t pick_sighandler(unsigned sig)
 	}
 	return handler;
 }
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 /* Restores tty foreground process group, and exits. */
 static void hush_exit(int exitcode)
 {
@@ -2226,6 +2307,7 @@ static const char *get_cwd(int force)
 	return G.cwd;
 }
 
+#endif /* !__U_BOOT__ */
 
 /*
  * Shell and environment variable support
@@ -2263,8 +2345,10 @@ static const char* FAST_FUNC get_local_var_value(const char *name)
 	if (vpp)
 		return (*vpp)->varstr + len + 1;
 
+#ifndef __U_BOOT__
 	if (strcmp(name, "PPID") == 0)
 		return utoa(G.root_ppid);
+#endif /* !__U_BOOT__ */
 	// bash compat: UID? EUID?
 #if ENABLE_HUSH_RANDOM_SUPPORT
 	if (strcmp(name, "RANDOM") == 0)
@@ -2293,6 +2377,7 @@ static const char* FAST_FUNC get_local_var_value(const char *name)
 	return NULL;
 }
 
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_GETOPTS
 static void handle_changed_special_names(const char *name, unsigned name_len)
 {
@@ -2307,15 +2392,25 @@ static void handle_changed_special_names(const char *name, unsigned name_len)
 /* Do not even bother evaluating arguments */
 # define handle_changed_special_names(...) ((void)0)
 #endif
+#else /* __U_BOOT__ */
+/* Do not even bother evaluating arguments */
+# define handle_changed_special_names(...) ((void)0)
+#endif /* __U_BOOT__ */
 
 /* str holds "NAME=VAL" and is expected to be malloced.
  * We take ownership of it.
  */
+#ifndef __U_BOOT__
 #define SETFLAG_EXPORT   (1 << 0)
 #define SETFLAG_UNEXPORT (1 << 1)
 #define SETFLAG_MAKE_RO  (1 << 2)
+#endif /* !__U_BOOT__ */
 #define SETFLAG_VARLVL_SHIFT   3
+#ifndef __U_BOOT__
 static int set_local_var(char *str, unsigned flags)
+#else /* __U_BOOT__ */
+int set_local_var(char *str, int flags)
+#endif /* __U_BOOT__ */
 {
 	struct variable **cur_pp;
 	struct variable *cur;
@@ -2337,6 +2432,7 @@ static int set_local_var(char *str, unsigned flags)
 			continue;
 		}
 
+#ifndef __U_BOOT__
 		/* We found an existing var with this name */
 		if (cur->flg_read_only) {
 			bb_error_msg("%s: readonly variable", str);
@@ -2351,6 +2447,7 @@ static int set_local_var(char *str, unsigned flags)
 			unsetenv(str);
 			*eq_sign = '=';
 		}
+#endif /* !__U_BOOT__ */
 		if (cur->var_nest_level < local_lvl) {
 			/* bash 3.2.33(1) and exported vars:
 			 * # export z=z
@@ -2360,8 +2457,10 @@ static int set_local_var(char *str, unsigned flags)
 			 * # env | grep ^z
 			 * z=z
 			 */
+#ifndef __U_BOOT__
 			if (cur->flg_export)
 				flags |= SETFLAG_EXPORT;
+#endif /* !__U_BOOT__ */
 			/* New variable is local ("local VAR=VAL" or
 			 * "VAR=VAL cmd")
 			 * and existing one is global, or local
@@ -2372,7 +2471,11 @@ static int set_local_var(char *str, unsigned flags)
 			if (G.shadowed_vars_pp) {
 				/* Save in "shadowed" list */
 				debug_printf_env("shadowing %s'%s'/%u by '%s'/%u\n",
+#ifndef __U_BOOT__
 					cur->flg_export ? "exported " : "",
+#else /* __U_BOOT__ */
+					"",
+#endif /* __U_BOOT__ */
 					cur->varstr, cur->var_nest_level, str, local_lvl
 				);
 				cur->next = *G.shadowed_vars_pp;
@@ -2380,7 +2483,11 @@ static int set_local_var(char *str, unsigned flags)
 			} else {
 				/* Came from pseudo_exec_argv(), no need to save: delete it */
 				debug_printf_env("shadow-deleting %s'%s'/%u by '%s'/%u\n",
+#ifndef __U_BOOT__
 					cur->flg_export ? "exported " : "",
+#else /* __U_BOOT__ */
+					"",
+#endif /* __U_BOOT__ */
 					cur->varstr, cur->var_nest_level, str, local_lvl
 				);
 				if (cur->max_len == 0) /* allocated "VAR=VAL"? */
@@ -2422,13 +2529,16 @@ static int set_local_var(char *str, unsigned flags)
 	/* Not found or shadowed - create new variable struct */
 	debug_printf_env("%s: alloc new var '%s'/%u\n", __func__, str, local_lvl);
 	cur = xzalloc(sizeof(*cur));
+#ifndef __U_BOOT__
 	cur->var_nest_level = local_lvl;
+#endif /* !__U_BOOT__ */
 	cur->next = *cur_pp;
 	*cur_pp = cur;
 
  set_str_and_exp:
 	cur->varstr = str;
  exp:
+#ifndef __U_BOOT__
 #if !BB_MMU || ENABLE_HUSH_READONLY
 	if (flags & SETFLAG_MAKE_RO) {
 		cur->flg_read_only = 1;
@@ -2436,7 +2546,9 @@ static int set_local_var(char *str, unsigned flags)
 #endif
 	if (flags & SETFLAG_EXPORT)
 		cur->flg_export = 1;
+#endif /* !__U_BOOT__ */
 	retval = 0;
+#ifndef __U_BOOT__
 	if (cur->flg_export) {
 		if (flags & SETFLAG_UNEXPORT) {
 			cur->flg_export = 0;
@@ -2449,6 +2561,7 @@ static int set_local_var(char *str, unsigned flags)
 			 */
 		}
 	}
+#endif /* !__U_BOOT__ */
 	free(free_me);
 
 	handle_changed_special_names(cur->varstr, name_len - 1);
@@ -2456,9 +2569,17 @@ static int set_local_var(char *str, unsigned flags)
 	return retval;
 }
 
+#ifndef __U_BOOT__
 static void FAST_FUNC set_local_var_from_halves(const char *name, const char *val)
 {
+#ifndef __U_BOOT__
 	char *var = xasprintf("%s=%s", name, val);
+#else /* __U_BOOT__ */
+	char *var;
+
+	// TODO: Secure it.
+	sprintf(var, "%s=%s", name, val);
+#endif /* __U_BOOT__ */
 	set_local_var(var, /*flag:*/ 0);
 }
 
@@ -2467,6 +2588,7 @@ static void set_pwd_var(unsigned flag)
 {
 	set_local_var(xasprintf("PWD=%s", get_cwd(/*force:*/ 1)), flag);
 }
+#endif /* !__U_BOOT__ */
 
 #if ENABLE_HUSH_UNSET || ENABLE_HUSH_GETOPTS
 static int unset_local_var_len(const char *name, int name_len)
@@ -2509,6 +2631,7 @@ static int unset_local_var(const char *name)
 #endif
 
 
+#ifndef __U_BOOT__
 /*
  * Helpers for "var1=val1 var2=val2 cmd" feature
  */
@@ -2602,11 +2725,13 @@ static void reinit_unicode_for_hush(void)
 	}
 }
 
+#endif /* !__U_BOOT__ */
 /*
  * in_str support (strings, and "strings" read from files).
  */
 
 #if ENABLE_HUSH_INTERACTIVE
+#ifndef __U_BOOT__
 /* To test correct lineedit/interactive behavior, type from command line:
  *	echo $P\
  *	\
@@ -2639,8 +2764,15 @@ static const char *setup_prompt_string(void)
 	debug_printf("prompt_str '%s'\n", prompt_str);
 	return prompt_str;
 }
+#endif /* !__U_BOOT__ */
+
+#ifndef __U_BOOT__
 static int get_user_input(struct in_str *i)
+#else /* __U_BOOT__ */
+static void get_user_input(struct in_str *i)
+#endif /* __U_BOOT__ */
 {
+#ifndef __U_BOOT__
 # if ENABLE_FEATURE_EDITING
 	/* In EDITING case, this function reads next input line,
 	 * saves it in i->p, then returns 1st char of it.
@@ -2716,37 +2848,133 @@ static int get_user_input(struct in_str *i)
 	}
 	return r;
 # endif
+#else /* __U_BOOT__ */
+	int n;
+	int promptme;
+	static char the_command[CONFIG_SYS_CBSIZE + 1];
+
+	bootretry_reset_cmd_timeout();
+	promptme = 1;
+	n = u_boot_cli_readline(i);
+
+# ifdef CONFIG_BOOT_RETRY_TIME
+	if (n == -2) {
+		puts("\nTimeout waiting for command\n");
+#  ifdef CONFIG_RESET_TO_RETRY
+		do_reset(NULL, 0, 0, NULL);
+#  else
+#	error "This currently only works with CONFIG_RESET_TO_RETRY enabled"
+#  endif
+	}
+# endif
+	if (n == -1 ) {
+		G.flag_repeat = 0;
+		promptme = 0;
+	}
+	n = strlen(console_buffer);
+	console_buffer[n] = '\n';
+	console_buffer[n+1]= '\0';
+	if (had_ctrlc())
+		G.flag_repeat = 0;
+	clear_ctrlc();
+	G.do_repeat = 0;
+	if (G.promptmode == 1) {
+		if (console_buffer[0] == '\n'&& G.flag_repeat == 0) {
+			strcpy(the_command, console_buffer);
+		}
+		else {
+			if (console_buffer[0] != '\n') {
+				strcpy(the_command, console_buffer);
+				G.flag_repeat = 1;
+			}
+			else {
+				G.do_repeat = 1;
+			}
+		}
+		i->p = the_command;
+	}
+	else {
+		if (console_buffer[0] != '\n') {
+			if (strlen(the_command) + strlen(console_buffer)
+				< CONFIG_SYS_CBSIZE) {
+				n = strlen(the_command);
+#ifdef __U_BOOT__
+				/*
+				 * To avoid writing to bad places, we check if
+				 * n is greater than 0.
+				 * This bug was found by Harald Seiler.
+				 */
+				if (n > 0)
+					the_command[n-1] = ' ';
+				strcpy(&the_command[n], console_buffer);
+#else /* !__U_BOOT__ */
+			the_command[n-1] = ' ';
+			strcpy(&the_command[n], console_buffer);
+#endif /* !__U_BOOT__ */
+				}
+				else {
+					the_command[0] = '\n';
+					the_command[1] = '\0';
+					G.flag_repeat = 0;
+				}
+		}
+		if (promptme == 0) {
+			the_command[0] = '\n';
+			the_command[1] = '\0';
+		}
+		i->p = console_buffer;
+	}
+#endif /* __U_BOOT__ */
 }
 /* This is the magic location that prints prompts
  * and gets data back from the user */
 static int fgetc_interactive(struct in_str *i)
 {
 	int ch;
+#ifndef __U_BOOT__
 	/* If it's interactive stdin, get new line. */
 	if (G_interactive_fd && i->file == G.HFILE_stdin) {
+#endif /* !__U_BOOT__ */
+#ifndef __U_BOOT__
 		/* Returns first char (or EOF), the rest is in i->p[] */
 		ch = get_user_input(i);
+#else /* __U_BOOT__ */
+		/*
+		 * get_user_input() does not return anything when used in
+		 * U-Boot.
+		 * So, we need to take the read character from i->p[].
+		 */
+		get_user_input(i);
+		if (i->p && *i->p) {
+			ch = *i->p++;
+		}
+#endif /* __U_BOOT__ */
 		G.promptmode = 1; /* PS2 */
 		debug_printf_prompt("%s promptmode=%d\n", __func__, G.promptmode);
+#ifndef __U_BOOT__
 	} else {
 		/* Not stdin: script file, sourced file, etc */
 		do ch = hfgetc(i->file); while (ch == '\0');
 	}
+#endif /* !__U_BOOT__ */
 	return ch;
 }
 #else  /* !INTERACTIVE */
+#ifndef __U_BOOT__
 static ALWAYS_INLINE int fgetc_interactive(struct in_str *i)
 {
 	int ch;
 	do ch = hfgetc(i->file); while (ch == '\0');
 	return ch;
 }
+#endif /* !__U_BOOT__ */
 #endif  /* !INTERACTIVE */
 
 static int i_getch(struct in_str *i)
 {
 	int ch;
 
+#ifndef __U_BOOT__
 	if (!i->file) {
 		/* string-based in_str */
 		ch = (unsigned char)*i->p;
@@ -2758,6 +2986,7 @@ static int i_getch(struct in_str *i)
 		return EOF;
 	}
 
+#endif /* !__U_BOOT__ */
 	/* FILE-based in_str */
 
 #if ENABLE_FEATURE_EDITING
@@ -2767,6 +2996,7 @@ static int i_getch(struct in_str *i)
 		goto out;
 	}
 #endif
+#ifndef __U_BOOT__
 	/* peek_buf[] is an int array, not char. Can contain EOF. */
 	ch = i->peek_buf[0];
 	if (ch != 0) {
@@ -2778,6 +3008,7 @@ static int i_getch(struct in_str *i)
 		goto out;
 	}
 
+#endif /* !__U_BOOT__ */
 	ch = fgetc_interactive(i);
  out:
 	debug_printf("file_get: got '%c' %d\n", ch, ch);
@@ -2793,6 +3024,7 @@ static int i_getch(struct in_str *i)
 
 static int i_peek(struct in_str *i)
 {
+#ifndef __U_BOOT__
 	int ch;
 
 	if (!i->file) {
@@ -2827,6 +3059,11 @@ static int i_peek(struct in_str *i)
 	i->peek_buf[0] = ch;
 	/*i->peek_buf[1] = 0; - already is */
 	return ch;
+#else /* __U_BOOT__ */
+	/* string-based in_str */
+	/* Doesn't report EOF on NUL. None of the callers care. */
+	return (unsigned char)*i->p;
+#endif /* __U_BOOT__ */
 }
 
 /* Only ever called if i_peek() was called, and did not return EOF.
@@ -2835,7 +3072,9 @@ static int i_peek(struct in_str *i)
  */
 static int i_peek2(struct in_str *i)
 {
+#ifndef __U_BOOT__
 	int ch;
+#endif /* !__U_BOOT__ */
 
 	/* There are two cases when i->p[] buffer exists.
 	 * (1) it's a string in_str.
@@ -2846,6 +3085,7 @@ static int i_peek2(struct in_str *i)
 	if (i->p)
 		return (unsigned char)i->p[1];
 
+#ifndef __U_BOOT__
 	/* Now we know it is a file-based in_str. */
 
 	/* peek_buf[] is an int array, not char. Can contain EOF. */
@@ -2859,6 +3099,9 @@ static int i_peek2(struct in_str *i)
 
 	debug_printf("file_peek2: got '%c' %d\n", ch, ch);
 	return ch;
+#else
+	return 0;
+#endif /* __U_BOOT__ */
 }
 
 static int i_getch_and_eat_bkslash_nl(struct in_str *input)
@@ -2897,13 +3140,20 @@ static int i_peek_and_eat_bkslash_nl(struct in_str *input)
 	}
 }
 
+#ifndef __U_BOOT__
 static void setup_file_in_str(struct in_str *i, HFILE *fp)
+#else /* __U_BOOT__ */
+static void setup_file_in_str(struct in_str *i)
+#endif /* __U_BOOT__ */
 {
 	memset(i, 0, sizeof(*i));
+#ifndef __U_BOOT__
 	i->file = fp;
 	/* i->p = NULL; */
+#endif /* !__U_BOOT__ */
 }
 
+#ifndef __U_BOOT__
 static void setup_string_in_str(struct in_str *i, const char *s)
 {
 	memset(i, 0, sizeof(*i));
@@ -2911,6 +3161,7 @@ static void setup_string_in_str(struct in_str *i, const char *s)
 	i->p = s;
 }
 
+#endif /* !__U_BOOT__ */
 
 /*
  * o_string support
@@ -2980,10 +3231,12 @@ static void o_addstr(o_string *o, const char *str)
 	o_addblock(o, str, strlen(str));
 }
 
+#ifndef __U_BOOT__
 static void o_addstr_with_NUL(o_string *o, const char *str)
 {
 	o_addblock(o, str, strlen(str) + 1);
 }
+#endif /* !__U_BOOT__ */
 
 #if !BB_MMU
 static void nommu_addchr(o_string *o, int ch)
@@ -2995,6 +3248,7 @@ static void nommu_addchr(o_string *o, int ch)
 # define nommu_addchr(o, str) ((void)0)
 #endif
 
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_MODE_X
 static void x_mode_addchr(int ch)
 {
@@ -3025,6 +3279,7 @@ static void x_mode_flush(void)
 	G.x_mode_buf.length = 0;
 }
 #endif
+#endif /* !__U_BOOT__ */
 
 /*
  * HUSH_BRACE_EXPANSION code needs corresponding quoting on variable expansion side.
@@ -3156,7 +3411,11 @@ static void debug_print_list(const char *prefix, o_string *o, int n)
 	if (n) {
 		const char *p = o->data + (int)(uintptr_t)list[n - 1] + string_start;
 		indent();
+#ifndef __U_BOOT__
 		fdprintf(2, " total_sz:%ld\n", (long)((p + strlen(p) + 1) - o->data));
+#else /* __U_BOOT__ */
+		printf(" total_sz:%ld\n", (long)((p + strlen(p) + 1) - o->data));
+#endif /* __U_BOOT__ */
 	}
 }
 #else
@@ -3216,6 +3475,7 @@ static int o_get_last_ptr(o_string *o, int n)
 	return ((int)(uintptr_t)list[n-1]) + string_start;
 }
 
+#ifndef __U_BOOT__
 /*
  * Globbing routines.
  *
@@ -3524,12 +3784,14 @@ static int perform_glob(o_string *o, int n)
 	return n;
 }
 
+#endif /* !__U_BOOT__ */
 #endif /* !HUSH_BRACE_EXPANSION */
 
 /* If o->o_expflags & EXP_FLAG_GLOB, glob the string so far remembered.
  * Otherwise, just finish current list[] and start new */
 static int o_save_ptr(o_string *o, int n)
 {
+#ifndef __U_BOOT__
 	if (o->o_expflags & EXP_FLAG_GLOB) {
 		/* If o->has_empty_slot, list[n] was already globbed
 		 * (if it was requested back then when it was filled)
@@ -3537,6 +3799,7 @@ static int o_save_ptr(o_string *o, int n)
 		if (!o->has_empty_slot)
 			return perform_glob(o, n); /* o_save_ptr_helper is inside */
 	}
+#endif /* !__U_BOOT__ */
 	return o_save_ptr_helper(o, n);
 }
 
@@ -3567,10 +3830,14 @@ static struct pipe *free_pipe(struct pipe *pi)
 	struct pipe *next;
 	int i;
 
+#ifndef __U_BOOT__
 	debug_printf_clean("free_pipe (pid %d)\n", getpid());
+#endif /* !__U_BOOT__ */
 	for (i = 0; i < pi->num_cmds; i++) {
 		struct command *command;
+#ifndef __U_BOOT__
 		struct redir_struct *r, *rnext;
+#endif /* !__U_BOOT__ */
 
 		command = &pi->cmds[i];
 		debug_printf_clean("  command %d:\n", i);
@@ -3605,6 +3872,7 @@ static struct pipe *free_pipe(struct pipe *pi)
 		free(command->group_as_string);
 		//command->group_as_string = NULL;
 #endif
+#ifndef __U_BOOT__
 		for (r = command->redirects; r; r = rnext) {
 			debug_printf_clean("   redirect %d%s",
 					r->rd_fd, redir_table[r->rd_type].descrip);
@@ -3619,13 +3887,16 @@ static struct pipe *free_pipe(struct pipe *pi)
 			free(r);
 		}
 		//command->redirects = NULL;
+#endif /* !__U_BOOT__ */
 	}
 	free(pi->cmds);   /* children are an array, they get freed all at once */
 	//pi->cmds = NULL;
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_JOB
 	free(pi->cmdtext);
 	//pi->cmdtext = NULL;
 #endif
+#endif /* !__U_BOOT__ */
 
 	next = pi->next;
 	free(pi);
@@ -3655,6 +3926,7 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 		[PIPE_OR ] = "OR" ,
 		[PIPE_BG ] = "BG" ,
 	};
+#ifndef __U_BOOT__
 	static const char *RES[] = {
 		[RES_NONE ] = "NONE" ,
 # if ENABLE_HUSH_IF
@@ -3684,6 +3956,7 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 		[RES_XXXX ] = "XXXX" ,
 		[RES_SNTX ] = "SNTX" ,
 	};
+#endif /* !__U_BOOT__ */
 	static const char *const CMDTYPE[] = {
 		"{}",
 		"()",
@@ -3701,8 +3974,10 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 				lvl*2, "",
 				pin,
 				pi->num_cmds,
+#ifdef __U_BOOT__
 				(IF_HAS_KEYWORDS(pi->pi_inverted ? "! " :) ""),
 				RES[pi->res_word],
+#endif /* !__U_BOOT__ */
 				pi->followup, PIPE[pi->followup]
 		);
 		prn = 0;
@@ -3734,8 +4009,10 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 				fdprintf(2, " '%s'", *argv);
 				argv++;
 			}
+#ifndef __U_BOOT__
 			if (command->redirects)
 				fdprintf(2, " {redir}");
+#endif /* __U_BOOT__ */
 			fdprintf(2, "\n");
 			prn++;
 		}
@@ -3869,8 +4146,13 @@ static void done_pipe(struct parse_context *ctx, pipe_style type)
 	) {
 		struct pipe *new_p;
 		debug_printf_parse("done_pipe: adding new pipe: "
+#ifndef __U_BOOT__
 				"not_null:%d ctx->ctx_res_w:%d\n",
 				not_null, ctx->ctx_res_w);
+#else /* __U_BOOT__ */
+				"not_null:%d\n",
+				not_null);
+#endif /* __U_BOOT__ */
 		new_p = new_pipe();
 		ctx->pipe->next = new_p;
 		ctx->pipe = new_p;
@@ -4096,6 +4378,7 @@ static int done_word(struct parse_context *ctx)
 		return 0;
 	}
 
+#ifndef __U_BOOT__
 	if (ctx->pending_redirect) {
 		/* We do not glob in e.g. >*.tmp case. bash seems to glob here
 		 * only if run as "bash", not "sh" */
@@ -4137,6 +4420,7 @@ static int done_word(struct parse_context *ctx)
 		debug_printf_parse("word stored in rd_filename: '%s'\n", ctx->word.data);
 		ctx->pending_redirect = NULL;
 	} else {
+#endif /* !__U_BOOT__ */
 #if HAS_KEYWORDS
 # if ENABLE_HUSH_CASE
 		if (ctx->ctx_dsemicolon
@@ -4247,8 +4531,14 @@ static int done_word(struct parse_context *ctx)
 		}
 		debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]);
 		command->argv = add_string_to_strings(command->argv, xstrdup(ctx->word.data));
+#ifdef __U_BOOT__
+		command->argc++;
+#endif /* __U_BOOT__ */
 		debug_print_strings("word appended to argv", command->argv);
+
+#ifndef __U_BOOT__
 	}
+#endif /* !__U_BOOT__ */
 
 #if ENABLE_HUSH_LOOPS
 	if (ctx->ctx_res_w == RES_FOR) {
@@ -4280,6 +4570,7 @@ static int done_word(struct parse_context *ctx)
 }
 
 
+#ifndef __U_BOOT__
 /* Peek ahead in the input to find out if we have a "&n" construct,
  * as in "2>&1", that represents duplicating a file descriptor.
  * Return:
@@ -4463,7 +4754,11 @@ static char *fetch_till_str(o_string *as_string,
 			nommu_addchr(as_string, ch);
 		if (ch == '\n' || ch == EOF) {
  check_heredoc_end:
+#ifndef __U_BOOT__
 			if ((heredoc_flags & HEREDOC_QUOTED) || prev != '\\') {
+#else /* __U_BOOT__ */
+			if (prev != '\\') {
+#endif
 				/* End-of-line, and not a line continuation */
 				if (strcmp(heredoc.data + past_EOL, word) == 0) {
 					heredoc.data[past_EOL] = '\0';
@@ -4483,7 +4778,11 @@ static char *fetch_till_str(o_string *as_string,
 						ch = i_getch(input);
 						if (ch != EOF)
 							nommu_addchr(as_string, ch);
+#ifndef __U_BOOT__
 					} while ((heredoc_flags & HEREDOC_SKIPTABS) && ch == '\t');
+#else /* __U_BOOT__ */
+				} while (ch == '\t');
+#endif
 					/* If this immediately ended the line,
 					 * go back to end-of-line checks.
 					 */
@@ -4520,6 +4819,7 @@ static char *fetch_till_str(o_string *as_string,
 			prev = ch;
 	}
 }
+#endif /* !__U_BOOT__ */
 
 /* Look at entire parse tree for not-yet-loaded REDIRECT_HEREDOCs
  * and load them all. There should be exactly heredoc_cnt of them.
@@ -4539,10 +4839,13 @@ static int fetch_heredocs(o_string *as_string, struct pipe *pi, int heredoc_cnt,
 				cmd->argv ? cmd->argv[0] : "NONE"
 		);
 		for (i = 0; i < pi->num_cmds; i++) {
+#ifndef __U_BOOT__
 			struct redir_struct *redir = cmd->redirects;
 
+#endif /* !__U_BOOT__ */
 			debug_printf_heredoc("fetch_heredocs: %d cmd argv0:'%s'\n",
 					i, cmd->argv ? cmd->argv[0] : "NONE");
+#ifndef __U_BOOT__
 			while (redir) {
 				if (redir->rd_type == REDIRECT_HEREDOC) {
 					char *p;
@@ -4561,6 +4864,7 @@ static int fetch_heredocs(o_string *as_string, struct pipe *pi, int heredoc_cnt,
 				}
 				redir = redir->next;
 			}
+#endif /* !__U_BOOT__ */
 			if (cmd->group) {
 				//bb_error_msg("%s:%u heredoc_cnt:%d", __func__, __LINE__, heredoc_cnt);
 				heredoc_cnt = fetch_heredocs(as_string, cmd->group, heredoc_cnt, input);
@@ -4651,13 +4955,17 @@ static int parse_group(struct parse_context *ctx,
 	}
 #endif
 
+#ifndef __U_BOOT__
  IF_HUSH_FUNCTIONS(skip:)
+#endif /* !__U_BOOT__ */
 
 	endch = '}';
 	if (ch == '(') {
 		endch = ')';
+#ifndef __U_BOOT__
 		IF_HUSH_FUNCTIONS(if (command->cmd_type != CMD_FUNCDEF))
 			command->cmd_type = CMD_SUBSHELL;
+#endif /* !__U_BOOT__ */
 	} else {
 		/* bash does not allow "{echo...", requires whitespace */
 		ch = i_peek(input);
@@ -4725,6 +5033,7 @@ static int parse_group(struct parse_context *ctx,
 #undef as_string
 }
 
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_TICK || ENABLE_FEATURE_SH_MATH || ENABLE_HUSH_DOLLAR_OPS
 /* Subroutines for copying $(...) and `...` things */
 /* '...' */
@@ -4754,6 +5063,7 @@ static int add_till_single_quote_dquoted(o_string *dest, struct in_str *input)
 		o_addqchr(dest, ch);
 	}
 }
+
 /* "...\"...`..`...." - do we need to handle "...$(..)..." too? */
 static int add_till_backquote(o_string *dest, struct in_str *input, int in_dquote);
 static int add_till_double_quote(o_string *dest, struct in_str *input)
@@ -4780,6 +5090,8 @@ static int add_till_double_quote(o_string *dest, struct in_str *input)
 		//if (ch == '$') ...
 	}
 }
+
+
 /* Process `cmd` - copy contents until "`" is seen. Complicated by
  * \` quoting.
  * "Within the backquoted style of command substitution, backslash
@@ -5014,6 +5326,7 @@ static int parse_dollar_squote(o_string *as_string, o_string *dest, struct in_st
 #else
 # #define parse_dollar_squote(as_string, dest, input) 0
 #endif /* BASH_DOLLAR_SQUOTE */
+#endif /* !__U_BOOT__ */
 
 /* Return code: 0 for OK, 1 for syntax error */
 #if BB_MMU
@@ -5056,8 +5369,10 @@ static int parse_dollar(o_string *as_string,
 		o_addchr(dest, ch | quote_mask);
 		o_addchr(dest, SPECIAL_VAR_SYMBOL);
 	} else switch (ch) {
+#ifndef __U_BOOT__
 	case '$': /* pid */
 	case '!': /* last bg pid */
+#endif /* !__U_BOOT__ */
 	case '?': /* last exit code */
 	case '#': /* number of args */
 	case '*': /* args */
@@ -5151,6 +5466,7 @@ static int parse_dollar(o_string *as_string,
  eat_until_closing:
 				/* Eat everything until closing '}' (or ':') */
 				end_ch = '}';
+#ifndef __U_BOOT__
 				if (BASH_SUBSTR
 				 && ch == ':'
 				 && !strchr(MINUS_PLUS_EQUAL_QUESTION, i_peek(input))
@@ -5169,6 +5485,7 @@ static int parse_dollar(o_string *as_string,
 					}
 					end_ch = '}' * 0x100 + '/';
 				}
+#endif /* !__U_BOOT__ */
 				o_addchr(dest, ch);
 				/* The pattern can't be empty.
 				 * IOW: if the first char after "${v//" is a slash,
@@ -5179,13 +5496,17 @@ static int parse_dollar(o_string *as_string,
 				if (i_peek(input) == '/') {
 					o_addchr(dest, i_getch(input));
 				}
+#ifndef __U_BOOT__
  again:
+#endif /* !__U_BOOT__ */
 				if (!BB_MMU)
 					pos = dest->length;
 #if ENABLE_HUSH_DOLLAR_OPS
+#ifndef __U_BOOT__
 				last_ch = add_till_closing_bracket(dest, input, end_ch);
 				if (last_ch == 0) /* error? */
 					return 0;
+#endif /* !__U_BOOT__ */
 #else
 # error Simple code to only allow ${var} is not implemented
 #endif
@@ -5194,6 +5515,7 @@ static int parse_dollar(o_string *as_string,
 					o_addchr(as_string, last_ch);
 				}
 
+#ifndef __U_BOOT__
 				if ((BASH_SUBSTR || BASH_PATTERN_SUBST)
 					 && (end_ch & 0xff00)
 				) {
@@ -5212,6 +5534,7 @@ static int parse_dollar(o_string *as_string,
 						o_addstr(dest, "999999999");
 					} /* else: it's ${var/[/]pattern} */
 				}
+#endif /* !__U_BOOT__ */
 				break;
 			}
 			len_single_ch = 0; /* it can't be ${#C} op */
@@ -5415,8 +5738,10 @@ static struct pipe *parse_stream(char **pstring,
 		const char *is_special;
 		int ch;
 		int next;
+#ifndef __U_BOOT__
 		int redir_fd;
 		redir_type redir_style;
+#endif /* !__U_BOOT__ */
 
 		ch = i_getch(input);
 		debug_printf_parse(": ch=%c (%d) escape=%d\n",
@@ -5506,8 +5831,10 @@ static struct pipe *parse_stream(char **pstring,
 		if (ch == '\'') {
 			ctx.word.has_quoted_part = 1;
 			next = i_getch(input);
+#ifndef __U_BOOT__
 			if (next == '\'' && !ctx.pending_redirect)
 				goto insert_empty_quoted_str_marker;
+#endif /* !__U_BOOT__ */
 
 			ch = next;
 			while (1) {
@@ -5534,7 +5861,11 @@ static struct pipe *parse_stream(char **pstring,
 			next = i_peek_and_eat_bkslash_nl(input);
 
 		is_special = "{}<>&|();#" /* special outside of "str" */
+#ifndef __U_BOOT__
 				"$\"" IF_HUSH_TICK("`") /* always special */
+#else /* __U_BOOT__ */
+				"$\""
+#endif /* __U_BOOT__ */
 				SPECIAL_VAR_SYMBOL_STR;
 #if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
 		if (ctx.command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB) {
@@ -5725,6 +6056,7 @@ static struct pipe *parse_stream(char **pstring,
 		/* Catch <, > before deciding whether this word is
 		 * an assignment. a=1 2>z b=2: b=2 is still assignment */
 		switch (ch) {
+#ifndef __U_BOOT__
 		case '>':
 			redir_fd = redirect_opt_num(&ctx.word);
 			if (done_word(&ctx)) {
@@ -5771,6 +6103,7 @@ static struct pipe *parse_stream(char **pstring,
 			if (parse_redirect(&ctx, redir_fd, redir_style, input))
 				goto parse_error_exitcode1;
 			continue; /* get next char */
+#endif /* !__U_BOOT__ */
 		case '#':
 			if (ctx.word.length == 0 && !ctx.word.has_quoted_part) {
 				/* skip "#comment" */
@@ -5798,8 +6131,10 @@ static struct pipe *parse_stream(char **pstring,
  skip_end_trigger:
 
 		if (ctx.is_assignment == MAYBE_ASSIGNMENT
+#ifndef __U_BOOT__
 		 /* check that we are not in word in "a=1 2>word b=1": */
 		 && !ctx.pending_redirect
+#endif /* !__U_BOOT__ */
 		) {
 			/* ch is a special char and thus this word
 			 * cannot be an assignment */
@@ -5821,8 +6156,10 @@ static struct pipe *parse_stream(char **pstring,
 			o_addchr(&ctx.word, ch);
 			continue; /* get next char */
 		case '$':
+#ifndef __U_BOOT__
 			if (parse_dollar_squote(&ctx.as_string, &ctx.word, input))
 				continue; /* get next char */
+#endif /* !__U_BOOT__ */
 			if (!parse_dollar(&ctx.as_string, &ctx.word, input, /*quote_mask:*/ 0)) {
 				debug_printf_parse("parse_stream parse error: "
 					"parse_dollar returned 0 (error)\n");
@@ -5831,9 +6168,15 @@ static struct pipe *parse_stream(char **pstring,
 			continue; /* get next char */
 		case '"':
 			ctx.word.has_quoted_part = 1;
+#ifndef __U_BOOT__
 			if (next == '"' && !ctx.pending_redirect) {
+#else /* __U_BOOT__ */
+			if (next == '"') {
+#endif /* __U_BOOT__ */
 				i_getch(input); /* eat second " */
+#ifndef __U_BOOT__
  insert_empty_quoted_str_marker:
+#endif /* !__U_BOOT__ */
 				nommu_addchr(&ctx.as_string, next);
 				o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
 				o_addchr(&ctx.word, SPECIAL_VAR_SYMBOL);
@@ -6140,6 +6483,7 @@ static int expand_on_ifs(o_string *output, int n, const char *str)
 	return n;
 }
 
+#ifndef __U_BOOT__
 /* Helper to expand $((...)) and heredoc body. These act as if
  * they are in double quotes, with the exception that they are not :).
  * Just the rules are similar: "expand only $var and `cmd`"
@@ -6440,7 +6784,9 @@ static int encode_then_append_var_plusminus(o_string *output, int n,
 	o_free(&dest);
 	return n;
 }
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 #if ENABLE_FEATURE_SH_MATH
 static arith_t expand_and_evaluate_arith(const char *arg, const char **errmsg_p)
 {
@@ -6461,7 +6807,9 @@ static arith_t expand_and_evaluate_arith(const char *arg, const char **errmsg_p)
 	return res;
 }
 #endif
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 #if BASH_PATTERN_SUBST
 /* ${var/[/]pattern[/repl]} helpers */
 static char *strstr_pattern(char *val, const char *pattern, int *size)
@@ -6529,6 +6877,7 @@ static char *replace_pattern(char *val, const char *pattern, const char *repl, c
 	return result;
 }
 #endif /* BASH_PATTERN_SUBST */
+#endif /* !__U_BOOT__ */
 
 static int append_str_maybe_ifs_split(o_string *output, int n,
 		int first_ch, const char *val)
@@ -6593,6 +6942,7 @@ static NOINLINE int expand_one_var(o_string *output, int n,
 			exp_saveptr = var+1 + strcspn(var+1, VAR_ENCODED_SUBST_OPS);
 		}
 		exp_op = exp_save = *exp_saveptr;
+#ifndef __U_BOOT__
 		if (exp_op) {
 			exp_word = exp_saveptr + 1;
 			if (exp_op == ':') {
@@ -6608,29 +6958,37 @@ static NOINLINE int expand_one_var(o_string *output, int n,
 			}
 			*exp_saveptr = '\0';
 		} /* else: it's not an expansion op, but bare ${var} */
+#endif /* !__U_BOOT__ */
 	}
 
 	/* Look up the variable in question */
 	if (isdigit(var[0])) {
 		/* parse_dollar should have vetted var for us */
+#ifndef __U_BOOT__
 		int nn = xatoi_positive(var);
+#else /* __U_BOOT__ */
+		int nn = simple_strtoul(var, NULL, 10);
+#endif /* __U_BOOT__ */
 		if (nn < G.global_argc)
 			val = G.global_argv[nn];
 		/* else val remains NULL: $N with too big N */
 	} else {
 		switch (var[0]) {
+#ifndef __U_BOOT__
 		case '$': /* pid */
 			val = utoa(G.root_pid);
 			break;
 		case '!': /* bg pid */
 			val = G.last_bg_pid ? utoa(G.last_bg_pid) : "";
 			break;
+#endif /* !__U_BOOT__ */
 		case '?': /* exitcode */
 			val = utoa(G.last_exitcode);
 			break;
 		case '#': /* argc */
 			val = utoa(G.global_argc ? G.global_argc-1 : 0);
 			break;
+#ifndef __U_BOOT__
 		case '-': { /* active options */
 			/* Check set_mode() to see what option chars we support */
 			char *cp;
@@ -6652,11 +7010,13 @@ static NOINLINE int expand_one_var(o_string *output, int n,
 			*cp = '\0';
 			break;
 		}
+#endif /* !__U_BOOT__ */
 		default:
 			val = get_local_var_value(var);
 		}
 	}
 
+#ifndef __U_BOOT__
 	/* Handle any expansions */
 	if (exp_op == 'L') {
 		reinit_unicode_for_hush();
@@ -6936,6 +7296,7 @@ static NOINLINE int expand_one_var(o_string *output, int n,
 		*exp_saveptr = exp_save;
 	} /* if (exp_op) */
 
+#endif /* !__U_BOOT__ */
 	arg[0] = arg0;
 	*pp = p;
 
@@ -7248,6 +7609,7 @@ static char* expand_strvec_to_string(char **argv)
 }
 #endif
 
+#ifndef __U_BOOT__
 static char **expand_assignments(char **argv, int count)
 {
 	int i;
@@ -7289,7 +7651,9 @@ static void switch_off_special_sigs(unsigned mask)
 		install_sighandler(sig, SIG_DFL);
 	}
 }
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 #if BB_MMU
 /* never called */
 void re_execute_shell(char ***to_free, const char *s,
@@ -7479,6 +7843,7 @@ static void re_execute_shell(char ***to_free, const char *s,
 }
 #endif  /* !BB_MMU */
 
+#endif /* !__U_BOOT__ */
 
 static int run_and_free_list(struct pipe *pi);
 
@@ -7537,6 +7902,7 @@ static void parse_and_run_stream(struct in_str *inp, int end_trigger)
 	}
 }
 
+#ifndef __U_BOOT__
 static void parse_and_run_string(const char *s)
 {
 	struct in_str input;
@@ -7546,18 +7912,30 @@ static void parse_and_run_string(const char *s)
 	parse_and_run_stream(&input, '\0');
 	//IF_HUSH_LINENO_VAR(G.parse_lineno = sv;)
 }
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 static void parse_and_run_file(HFILE *fp)
+#else /* __U_BOOT__ */
+void parse_and_run_file(void)
+#endif /* __U_BOOT__ */
 {
 	struct in_str input;
+#ifndef __U_BOOT__
 	IF_HUSH_LINENO_VAR(unsigned sv = G.parse_lineno;)
 
 	IF_HUSH_LINENO_VAR(G.parse_lineno = 1;)
 	setup_file_in_str(&input, fp);
+#else /* __U_BOOT__ */
+	setup_file_in_str(&input);
+#endif /* __U_BOOT__ */
 	parse_and_run_stream(&input, ';');
+#ifndef __U_BOOT__
 	IF_HUSH_LINENO_VAR(G.parse_lineno = sv;)
+#endif /* !__U_BOOT__ */
 }
 
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_TICK
 static int generate_stream_from_string(const char *s, pid_t *pid_p)
 {
@@ -8163,7 +8541,9 @@ static const char * FAST_FUNC get_builtin_name(int i)
 	return NULL;
 }
 #endif
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 static void remove_nested_vars(void)
 {
 	struct variable *cur;
@@ -8215,6 +8595,7 @@ static void leave_var_nest_level(void)
 
 	remove_nested_vars();
 }
+#endif /* __U_BOOT__ */
 
 #if ENABLE_HUSH_FUNCTIONS
 static struct function **find_function_slot(const char *name)
@@ -8399,6 +8780,7 @@ static int run_function(const struct function *funcp, char **argv)
 #endif /* ENABLE_HUSH_FUNCTIONS */
 
 
+#ifndef __U_BOOT__
 #if BB_MMU
 #define exec_builtin(to_free, x, argv) \
 	exec_builtin(x, argv)
@@ -8431,8 +8813,10 @@ static void exec_builtin(char ***to_free,
 			argv);
 #endif
 }
+#endif /* !__U_BOOT__ */
 
 
+#ifndef __U_BOOT__
 static void execvp_or_die(char **argv) NORETURN;
 static void execvp_or_die(char **argv)
 {
@@ -8514,7 +8898,9 @@ static void dump_cmd_in_x_mode(char **argv)
 #else
 # define dump_cmd_in_x_mode(argv) ((void)0)
 #endif
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_COMMAND
 static void if_command_vV_print_and_exit(char opt_vV, char *cmd, const char *explanation)
 {
@@ -8540,6 +8926,7 @@ static void if_command_vV_print_and_exit(char opt_vV, char *cmd, const char *exp
 #else
 # define if_command_vV_print_and_exit(a,b,c) ((void)0)
 #endif
+#endif /* !__U_BOOT__ */
 
 #if BB_MMU
 #define pseudo_exec_argv(nommu_save, argv, assignment_cnt, argv_expanded) \
@@ -8548,6 +8935,7 @@ static void if_command_vV_print_and_exit(char opt_vV, char *cmd, const char *exp
 	pseudo_exec(command, argv_expanded)
 #endif
 
+#ifndef __U_BOOT__
 /* Called after [v]fork() in run_pipe, or from builtin_exec.
  * Never returns.
  * Don't exit() here.  If you don't exec, use _exit instead.
@@ -9186,18 +9574,34 @@ static int redirect_and_varexp_helper(
 
 	return setup_redirects(command, sqp);
 }
+#endif /* !__U_BOOT__ */
+
 static NOINLINE int run_pipe(struct pipe *pi)
 {
 	static const char *const null_ptr = NULL;
 
 	int cmd_no;
+#ifndef __U_BOOT__
 	int next_infd;
+#endif /* !__U_BOOT__ */
 	struct command *command;
 	char **argv_expanded;
 	char **argv;
+#ifndef __U_BOOT__
 	struct squirrel *squirrel = NULL;
+#endif /* !__U_BOOT__ */
 	int rcode;
 
+#ifdef __U_BOOT__
+	/*
+	 * Set rcode here to avoid returning a garbage value in the middle of
+	 * the function.
+	 * Also, if an error occurs, rcode value would be changed and last
+	 * return will signal the error.
+	 */
+	rcode = 0;
+#endif /* __U_BOOT__ */
+
 	debug_printf_exec("run_pipe start: members:%d\n", pi->num_cmds);
 	debug_enter();
 
@@ -9230,11 +9634,14 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		G.ifs_whitespace = (char*)G.ifs;
 	}
 
+#ifndef __U_BOOT__
 	IF_HUSH_JOB(pi->pgrp = -1;)
 	pi->stopped_cmds = 0;
+#endif /* !__U_BOOT__ */
 	command = &pi->cmds[0];
 	argv_expanded = NULL;
 
+#ifndef __U_BOOT__
 	if (pi->num_cmds != 1
 	 || pi->followup == PIPE_BG
 	 || command->cmd_type == CMD_SUBSHELL
@@ -9243,6 +9650,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 	}
 
 	pi->alive_cmds = 1;
+#endif /* !__U_BOOT__ */
 
 	debug_printf_exec(": group:%p argv:'%s'\n",
 		command->group, command->argv ? command->argv[0] : "NONE");
@@ -9274,6 +9682,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		/* { list } */
 		debug_printf_exec("non-subshell group\n");
 		rcode = 1; /* exitcode if redir failed */
+#ifndef __U_BOOT__
 		if (setup_redirects(command, &squirrel) == 0) {
 			debug_printf_exec(": run_list\n");
 //FIXME: we need to pass squirrel down into run_list()
@@ -9286,6 +9695,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		}
 		restore_redirects(squirrel);
 		IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+#endif /* !__U_BOOT__ */
 		debug_leave();
 		debug_printf_exec("run_pipe: return %d\n", rcode);
 		return rcode;
@@ -9293,10 +9703,12 @@ static NOINLINE int run_pipe(struct pipe *pi)
 
 	argv = command->argv ? command->argv : (char **) &null_ptr;
 	{
+#ifndef __U_BOOT__
 		const struct built_in_command *x;
 		IF_HUSH_FUNCTIONS(const struct function *funcp;)
 		IF_NOT_HUSH_FUNCTIONS(enum { funcp = 0 };)
 		struct variable **sv_shadowed;
+#endif /* !__U_BOOT__ */
 		struct variable *old_vars;
 
 #if ENABLE_HUSH_LINENO_VAR
@@ -9311,8 +9723,10 @@ static NOINLINE int run_pipe(struct pipe *pi)
 			unsigned i;
 			G.expand_exitcode = 0;
  only_assignments:
+#ifndef __U_BOOT__
 			rcode = setup_redirects(command, &squirrel);
 			restore_redirects(squirrel);
+#endif /* !__U_BOOT__ */
 
 			/* Set shell variables */
 			i = 0;
@@ -9378,6 +9792,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		}
 
 		old_vars = NULL;
+#ifndef __U_BOOT__
 		sv_shadowed = G.shadowed_vars_pp;
 
 		/* Check if argv[0] matches any functions (this goes before bltins) */
@@ -9501,8 +9916,10 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		debug_leave();
 		debug_printf_exec("run_pipe return %d\n", rcode);
 		return rcode;
+#endif /* !__U_BOOT__ */
 	}
 
+#ifndef __U_BOOT__
  must_fork:
 	/* NB: argv_expanded may already be created, and that
 	 * might include `cmd` runs! Do not rerun it! We *must*
@@ -9511,9 +9928,11 @@ static NOINLINE int run_pipe(struct pipe *pi)
 	/* Going to fork a child per each pipe member */
 	pi->alive_cmds = 0;
 	next_infd = 0;
+#endif /* !__U_BOOT__ */
 
 	cmd_no = 0;
 	while (cmd_no < pi->num_cmds) {
+#ifndef __U_BOOT__
 		struct fd_pair pipefds;
 #if !BB_MMU
 		int sv_var_nest_level = G.var_nest_level;
@@ -9522,6 +9941,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		nommu_save.argv = NULL;
 		nommu_save.argv_from_re_execing = NULL;
 #endif
+#endif /* !__U_BOOT__ */
 		command = &pi->cmds[cmd_no];
 		cmd_no++;
 		if (command->argv) {
@@ -9531,6 +9951,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 			debug_printf_exec(": pipe member with no argv\n");
 		}
 
+#ifndef __U_BOOT__
 		/* pipes are inserted between pairs of commands */
 		pipefds.rd = 0;
 		pipefds.wr = 1;
@@ -9633,17 +10054,30 @@ static NOINLINE int run_pipe(struct pipe *pi)
 			close(pipefds.wr);
 		/* Pass read (output) pipe end to next iteration */
 		next_infd = pipefds.rd;
+#else /* __U_BOOT__ */
+		/* Process the command */
+		rcode = cmd_process(G.do_repeat ? CMD_FLAG_REPEAT : 0,
+				    command->argc, command->argv,
+				    &(G.flag_repeat), NULL);
+#endif /* __U_BOOT__ */
 	}
 
+#ifndef __U_BOOT__
 	if (!pi->alive_cmds) {
 		debug_leave();
 		debug_printf_exec("run_pipe return 1 (all forks failed, no children)\n");
 		return 1;
 	}
+#endif /* __U_BOOT__ */
 
 	debug_leave();
+#ifndef __U_BOOT__
 	debug_printf_exec("run_pipe return -1 (%u children started)\n", pi->alive_cmds);
 	return -1;
+#else /* __U_BOOT__ */
+	debug_printf_exec("run_pipe return %d\n", rcode);
+	return rcode;
+#endif /* __U_BOOT__ */
 }
 
 /* NB: called by pseudo_exec, and therefore must not modify any
@@ -9670,8 +10104,10 @@ static int run_list(struct pipe *pi)
 	smallint last_rword; /* ditto */
 #endif
 
+#ifndef __U_BOOT__
 	debug_printf_exec("run_list start lvl %d\n", G.run_list_level);
 	debug_enter();
+#endif /* !__U_BOOT__ */
 
 #if ENABLE_HUSH_LOOPS
 	/* Check syntax for "for" */
@@ -9719,10 +10155,15 @@ static int run_list(struct pipe *pi)
 	rcode = G.last_exitcode;
 
 	/* Go through list of pipes, (maybe) executing them. */
+#ifndef __U_BOOT__
 	for (; pi; pi = IF_HUSH_LOOPS(rword == RES_DONE ? loop_top : ) pi->next) {
+#else /* __U_BOOT__ */
+	for (; pi; pi = pi->next) {
+#endif /* __U_BOOT__ */
 		int r;
 		int sv_errexit_depth;
 
+#ifndef __U_BOOT__
 		if (G.flag_SIGINT)
 			break;
 		if (G_flag_return_in_progress == 1)
@@ -9732,6 +10173,7 @@ static int run_list(struct pipe *pi)
 		debug_printf_exec(": rword=%d cond_code=%d last_rword=%d\n",
 				rword, cond_code, last_rword);
 
+#endif /* !__U_BOOT__ */
 		sv_errexit_depth = G.errexit_depth;
 		if (
 #if ENABLE_HUSH_IF
@@ -9885,8 +10327,10 @@ static int run_list(struct pipe *pi)
 		 * OTOH, in non-interactive shell this is useless
 		 * and only leads to extra job checks */
 		if (pi->num_cmds == 0) {
+#ifndef __U_BOOT__
 			if (G_interactive_fd)
 				goto check_jobs_and_continue;
+#endif /* !__U_BOOT__ */
 			continue;
 		}
 
@@ -9895,9 +10339,11 @@ static int run_list(struct pipe *pi)
 		 * after run_pipe to collect any background children,
 		 * even if list execution is to be stopped. */
 		debug_printf_exec(": run_pipe with %d members\n", pi->num_cmds);
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_LOOPS
 		G.flag_break_continue = 0;
 #endif
+#endif /* !__U_BOOT__ */
 		rcode = r = run_pipe(pi); /* NB: rcode is a smalluint, r is int */
 		if (r != -1) {
 			/* We ran a builtin, function, or group.
@@ -9905,10 +10351,13 @@ static int run_list(struct pipe *pi)
 			 * and we don't need to wait for anything. */
 			debug_printf_exec(": builtin/func exitcode %d\n", rcode);
 			G.last_exitcode = rcode;
+#ifndef __U_BOOT__
 			check_and_run_traps();
+#endif /* !__U_BOOT__ */
 #if ENABLE_HUSH_TRAP && ENABLE_HUSH_FUNCTIONS
 			rcode = G.last_exitcode; /* "return" in trap can change it, read back */
 #endif
+#ifndef __U_BOOT__
 #if ENABLE_HUSH_LOOPS
 			/* Was it "break" or "continue"? */
 			if (G.flag_break_continue) {
@@ -9934,6 +10383,7 @@ static int run_list(struct pipe *pi)
 				checkjobs(NULL, 0 /*(no pid to wait for)*/);
 				break;
 			}
+
 		} 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:
@@ -9969,13 +10419,18 @@ static int run_list(struct pipe *pi)
 			rcode = G.last_exitcode; /* "return" in trap can change it, read back */
 #endif
 		}
+#endif /* !__U_BOOT__ */
 
+#ifndef __U_BOOT__
 		/* 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);
 		}
+#else /* __U_BOOT__ */
+	} /* if (r != -1) */
+#endif /* __U_BOOT__ */
 		G.errexit_depth = sv_errexit_depth;
 
 		/* Analyze how result affects subsequent commands */
@@ -9983,8 +10438,10 @@ static int run_list(struct pipe *pi)
 		if (rword == RES_IF || rword == RES_ELIF)
 			cond_code = rcode;
 #endif
+#ifndef __U_BOOT__
  check_jobs_and_continue:
 		checkjobs(NULL, 0 /*(no pid to wait for)*/);
+#endif /* !__U_BOOT__ */
  dont_check_jobs_but_continue: ;
 #if ENABLE_HUSH_LOOPS
 		/* Beware of "while false; true; do ..."! */
@@ -10021,8 +10478,10 @@ static int run_list(struct pipe *pi)
 #if ENABLE_HUSH_CASE
 	free(case_word);
 #endif
+#ifndef __U_BOOT__
 	debug_leave();
 	debug_printf_exec("run_list lvl %d return %d\n", G.run_list_level + 1, rcode);
+#endif /* !__U_BOOT__ */
 	return rcode;
 }
 
@@ -10031,10 +10490,14 @@ static int run_and_free_list(struct pipe *pi)
 {
 	int rcode = 0;
 	debug_printf_exec("run_and_free_list entered\n");
+#ifndef __U_BOOT__
 	if (!G.o_opt[OPT_O_NOEXEC]) {
+#endif /* !__U_BOOT__ */
 		debug_printf_exec(": run_list: 1st pipe with %d cmds\n", pi->num_cmds);
 		rcode = run_list(pi);
+#ifndef __U_BOOT__
 	}
+#endif /* !__U_BOOT__ */
 	/* free_pipe_list has the side effect of clearing memory.
 	 * In the long run that function can be merged with run_list,
 	 * but doing that now would hobble the debugging effort. */
@@ -10044,6 +10507,7 @@ static int run_and_free_list(struct pipe *pi)
 }
 
 
+#ifndef __U_BOOT__
 static void install_sighandlers(unsigned mask)
 {
 	sighandler_t old_handler;
@@ -10460,6 +10924,7 @@ int hush_main(int argc, char **argv)
 		}
 	}
 
+#ifndef __U_BOOT__
 	/* -c takes effect *after* -l */
 	if (G.opt_c) {
 		/* Possibilities:
@@ -10536,6 +11001,7 @@ int hush_main(int argc, char **argv)
 	/* "implicit" -s: bare interactive hush shows 's' in $- */
 	G.opt_s = 1;
 
+#endif /* __U_BOOT__ */
 	/* Up to here, shell was non-interactive. Now it may become one.
 	 * NB: don't forget to (re)run install_special_sighandlers() as needed.
 	 */
@@ -10681,6 +11147,7 @@ int hush_main(int argc, char **argv)
 }
 
 
+
 /*
  * Built-ins
  */
@@ -12160,3 +12627,4 @@ static int FAST_FUNC builtin_memleak(char **argv UNUSED_PARAM)
 	return l;
 }
 #endif
+#endif /* !__U_BOOT__ */
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 09/21] cli: Add choice for hush parser.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (7 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 08/21] cli: Port Busybox 2021 hush to U-Boot Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 10/21] cli: Add HUSH_2021_PARSER to hush parser choice Francis Laniel
                   ` (12 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

For the moment, the choice contains only entry: HUSH_OLD_PARSER which is the
default.
The goal of this commit is to prepare the field to add a new hush parser and it
does not change anything to actual behavior.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 cmd/Kconfig     | 14 ++++++++++++++
 common/Makefile |  2 +-
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/cmd/Kconfig b/cmd/Kconfig
index 02c298fdbe..be24346c56 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -23,6 +23,20 @@ config HUSH_PARSER
 	  If disabled, you get the old, much simpler behaviour with a somewhat
 	  smaller memory footprint.
 
+choice HUSH_FLAVOR
+	bool "Hush flavor to use"
+	depends on HUSH_PARSER
+	default HUSH_OLD_PARSER
+
+	config HUSH_OLD_PARSER
+		bool "Use hush old parser"
+		help
+		  This option enables the old flavor of hush based on hush Busybox from
+		  2005.
+
+		  It is actually the default U-Boot shell when decided to use hush as shell.
+endchoice
+
 config CMDLINE_EDITING
 	bool "Enable command line editing"
 	depends on CMDLINE
diff --git a/common/Makefile b/common/Makefile
index c500bcd7d8..8b4627fccf 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -8,7 +8,7 @@ ifndef CONFIG_SPL_BUILD
 obj-y += init/
 obj-y += main.o
 obj-y += exports.o
-obj-$(CONFIG_HUSH_PARSER) += cli_hush.o
+obj-$(CONFIG_HUSH_OLD_PARSER) += cli_hush.o
 obj-$(CONFIG_AUTOBOOT) += autoboot.o
 
 # # boards
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 10/21] cli: Add HUSH_2021_PARSER to hush parser choice .
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (8 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 09/21] cli: Add choice for hush parser Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 11/21] cli: Enables using hush 2021 parser as command line parser Francis Laniel
                   ` (11 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

To use hush 2021 as U-Boot shell, one should tick "Use hush 2021 parser" under
"Hush flavor to use" choice.
Since this is a choice, you cannot use at the same time old and new flavor of
hush.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 cmd/Kconfig     | 8 ++++++++
 common/Makefile | 1 +
 2 files changed, 9 insertions(+)

diff --git a/cmd/Kconfig b/cmd/Kconfig
index be24346c56..13d5ceb517 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -35,6 +35,14 @@ choice HUSH_FLAVOR
 		  2005.
 
 		  It is actually the default U-Boot shell when decided to use hush as shell.
+
+	config HUSH_2021_PARSER
+		bool "Use hush 2021 parser"
+		help
+		  This option enables the new flavor of hush based on hush Busybox from
+		  2021.
+
+		  For the moment, it is highly experimental and should be used at own risks.
 endchoice
 
 config CMDLINE_EDITING
diff --git a/common/Makefile b/common/Makefile
index 8b4627fccf..8b97be8e7a 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -9,6 +9,7 @@ obj-y += init/
 obj-y += main.o
 obj-y += exports.o
 obj-$(CONFIG_HUSH_OLD_PARSER) += cli_hush.o
+obj-$(CONFIG_HUSH_2021_PARSER) += cli_hush_2021.o
 obj-$(CONFIG_AUTOBOOT) += autoboot.o
 
 # # boards
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 11/21] cli: Enables using hush 2021 parser as command line parser.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (9 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 10/21] cli: Add HUSH_2021_PARSER to hush parser choice Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 12/21] cli: hush_2021: Enable variables expansion for hush 2021 Francis Laniel
                   ` (10 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

If one defines HUSH_2021_PARSER, cli_loop() will jump on hush 2021 parser code.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli.c       | 14 +++++++++++---
 include/cli_hush.h |  8 ++++++++
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/common/cli.c b/common/cli.c
index d86046a728..090677f7a0 100644
--- a/common/cli.c
+++ b/common/cli.c
@@ -40,12 +40,15 @@ int run_command(const char *cmd, int flag)
 		return 1;
 
 	return 0;
-#else
+#elif CONFIG_IS_ENABLED(HUSH_OLD_PARSER)
 	int hush_flags = FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP;
 
 	if (flag & CMD_FLAG_ENV)
 		hush_flags |= FLAG_CONT_ON_NEWLINE;
 	return parse_string_outer(cmd, hush_flags);
+#else /* HUSH_2021_PARSER */
+	/* Not yet implemented. */
+	return 0;
 #endif
 }
 
@@ -104,7 +107,7 @@ int run_command_list(const char *cmd, int len, int flag)
 		memcpy(buff, cmd, len);
 		buff[len] = '\0';
 	}
-#ifdef CONFIG_HUSH_PARSER
+#if CONFIG_IS_ENABLED(HUSH_OLD_PARSER)
 	rcode = parse_string_outer(buff, FLAG_PARSE_SEMICOLON);
 #else
 	/*
@@ -225,7 +228,12 @@ err:
 void cli_loop(void)
 {
 	bootstage_mark(BOOTSTAGE_ID_ENTER_CLI_LOOP);
-#ifdef CONFIG_HUSH_PARSER
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	parse_and_run_file();
+	printf("Problem\n");
+	/* This point is never reached */
+	for (;;);
+#elif CONFIG_IS_ENABLED(CONFIG_HUSH_OLD_PARSER)
 	parse_file_outer();
 	/* This point is never reached */
 	for (;;);
diff --git a/include/cli_hush.h b/include/cli_hush.h
index 2bd35670c7..f6c04929b9 100644
--- a/include/cli_hush.h
+++ b/include/cli_hush.h
@@ -14,9 +14,17 @@
 
 extern int u_boot_hush_start(void);
 extern int parse_string_outer(const char *, int);
+#if CONFIG_IS_ENABLED(HUSH_OLD_PARSER)
 extern int parse_file_outer(void);
+#elif CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+extern void parse_and_run_file(void);
+#endif
 
+#if CONFIG_IS_ENABLED(HUSH_OLD_PARSER)
 int set_local_var(const char *s, int flg_export);
+#elif CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+int set_local_var(char *s, int flg_export);
+#endif
 void unset_local_var(const char *name);
 char *get_local_var(const char *s);
 
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 12/21] cli: hush_2021: Enable variables expansion for hush 2021.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (10 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 11/21] cli: Enables using hush 2021 parser as command line parser Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 13/21] cli: hush_2021: Add functions to be called from run_command() Francis Laniel
                   ` (9 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

The following commit enables variables expansion for hush 2021, both for local
and environment variables.
So the following commands:
foo=bar
echo $foo
setenv bar foo
echo $bar
leads to "bar" and "foo" being printed on console output.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli_hush_2021.c          | 17 ++++++++
 common/cli_hush_2021_upstream.c | 74 ++++++++++++++++++++++++++++++---
 2 files changed, 86 insertions(+), 5 deletions(-)

diff --git a/common/cli_hush_2021.c b/common/cli_hush_2021.c
index 52e249d8be..342be3beb8 100644
--- a/common/cli_hush_2021.c
+++ b/common/cli_hush_2021.c
@@ -225,6 +225,23 @@ static const char* endofname(const char *name)
 	return name;
 }
 
+/**
+ * list_size() - returns the number of elements in char ** before NULL.
+ *
+ * Argument must contain NULL to signalize its end.
+ *
+ * @list The list to count the number of element.
+ * @return The number of element in list.
+ */
+static size_t list_size(char **list)
+{
+	size_t size;
+
+	for(size = 0; list[size] != NULL; size++);
+
+	return size;
+}
+
 struct in_str;
 static int u_boot_cli_readline(struct in_str *i);
 
diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
index 06649a6011..51e351cdc6 100644
--- a/common/cli_hush_2021_upstream.c
+++ b/common/cli_hush_2021_upstream.c
@@ -3475,7 +3475,6 @@ static int o_get_last_ptr(o_string *o, int n)
 	return ((int)(uintptr_t)list[n-1]) + string_start;
 }
 
-#ifndef __U_BOOT__
 /*
  * Globbing routines.
  *
@@ -3730,8 +3729,10 @@ static int glob_needed(const char *s)
  */
 static int perform_glob(o_string *o, int n)
 {
+#ifndef __U_BOOT__
 	glob_t globdata;
 	int gr;
+#endif /* __U_BOOT__ */
 	char *pattern;
 
 	debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
@@ -3740,13 +3741,16 @@ static int perform_glob(o_string *o, int n)
 	pattern = o->data + o_get_last_ptr(o, n);
 	debug_printf_glob("glob pattern '%s'\n", pattern);
 	if (!glob_needed(pattern)) {
+#ifndef __U_BOOT__
  literal:
+#endif /* __U_BOOT__ */
 		/* unbackslash last string in o in place, fix length */
 		o->length = unbackslash(pattern) - o->data;
 		debug_printf_glob("glob pattern '%s' is literal\n", pattern);
 		return o_save_ptr_helper(o, n);
 	}
 
+#ifndef __U_BOOT__
 	memset(&globdata, 0, sizeof(globdata));
 	/* Can't use GLOB_NOCHECK: it does not unescape the string.
 	 * If we glob "*.\*" and don't find anything, we need
@@ -3782,16 +3786,22 @@ static int perform_glob(o_string *o, int n)
 	if (DEBUG_GLOB)
 		debug_print_list("perform_glob returning", o, n);
 	return n;
+#else /* __U_BOOT__ */
+	/*
+	 * NOTE We only use perform glob to call unbackslash to remove backslash
+	 * from string once expanded.
+	 * So, it seems OK to return this if no previous return was done.
+	 */
+	return o_save_ptr_helper(o, n);
+#endif /* __U_BOOT__ */
 }
 
-#endif /* !__U_BOOT__ */
 #endif /* !HUSH_BRACE_EXPANSION */
 
 /* If o->o_expflags & EXP_FLAG_GLOB, glob the string so far remembered.
  * Otherwise, just finish current list[] and start new */
 static int o_save_ptr(o_string *o, int n)
 {
-#ifndef __U_BOOT__
 	if (o->o_expflags & EXP_FLAG_GLOB) {
 		/* If o->has_empty_slot, list[n] was already globbed
 		 * (if it was requested back then when it was filled)
@@ -3799,7 +3809,6 @@ static int o_save_ptr(o_string *o, int n)
 		if (!o->has_empty_slot)
 			return perform_glob(o, n); /* o_save_ptr_helper is inside */
 	}
-#endif /* !__U_BOOT__ */
 	return o_save_ptr_helper(o, n);
 }
 
@@ -7012,7 +7021,20 @@ static NOINLINE int expand_one_var(o_string *output, int n,
 		}
 #endif /* !__U_BOOT__ */
 		default:
+#ifndef __U_BOOT__
 			val = get_local_var_value(var);
+#else /* __U_BOOT__ */
+			/*
+			 * Environment variable set with setenv* have to be
+			 * expanded.
+			 * So, we first search if the variable exists in
+			 * environment, if this is not the case, we default to
+			 * local value.
+			 */
+			val = env_get(var);
+			if (!val)
+				val = get_local_var_value(var);
+#endif /* __U_BOOT__ */
 		}
 	}
 
@@ -9944,7 +9966,30 @@ static NOINLINE int run_pipe(struct pipe *pi)
 #endif /* !__U_BOOT__ */
 		command = &pi->cmds[cmd_no];
 		cmd_no++;
-		if (command->argv) {
+
+#ifdef __U_BOOT__
+		/* Replace argv and argc by expanded if it exists. */
+		if (argv_expanded) {
+			/*
+			 * We need to save a pointer to argv, we will restore it
+			 * later, so it will be freed when pipe is freed.
+			 */
+			argv = command->argv;
+
+			/*
+			 * After expansion, there can be more or less argument, so we need to
+			 * update argc, for example:
+			 * - More arguments:
+			 *   foo='bar quuz'
+			 *   echo $foo
+			 * - Less arguments:
+			 *   echo $foo (if foo was never set)
+			 */
+			command->argc = list_size(argv_expanded);
+			command->argv = argv_expanded;
+		}
+#endif /* __U_BOOT__ */
+			if (command->argv) {
 			debug_printf_exec(": pipe member '%s' '%s'...\n",
 					command->argv[0], command->argv[1]);
 		} else {
@@ -10059,6 +10104,25 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		rcode = cmd_process(G.do_repeat ? CMD_FLAG_REPEAT : 0,
 				    command->argc, command->argv,
 				    &(G.flag_repeat), NULL);
+
+		if (argv_expanded) {
+			/*
+			 * expand_strvec_to_strvec() allocates memory to expand
+			 * argv, we need to free it.
+			 */
+			free(argv_expanded);
+
+			/*
+			 * We also restore command->argv to its original value
+			 * so no memory leak happens.
+			 */
+			command->argv = argv;
+
+			/*
+			 * NOTE argc exists only in U-Boot, so argv freeing does
+			 * not rely on it as this code exists in BusyBox.
+			 */
+		}
 #endif /* __U_BOOT__ */
 	}
 
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 13/21] cli: hush_2021: Add functions to be called from run_command().
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (11 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 12/21] cli: hush_2021: Enable variables expansion for hush 2021 Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 14/21] cli: Modify run_command() to add hush 2021 as parser Francis Laniel
                   ` (8 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

run_command() is called internally by the command run and it can also be called
directly from U-Boot code, e.g. to do unit tests.
This commit adds this path to go to hush 2021.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli_hush_2021_upstream.c | 66 +++++++++++++++++++++++++++++++--
 1 file changed, 63 insertions(+), 3 deletions(-)

diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
index 51e351cdc6..f2619eb49a 100644
--- a/common/cli_hush_2021_upstream.c
+++ b/common/cli_hush_2021_upstream.c
@@ -1007,6 +1007,7 @@ struct globals {
 #ifdef __U_BOOT__
 	int flag_repeat;
 	int do_repeat;
+	int run_command_flags;
 #endif /* __U_BOOT__ */
 	char *ifs_whitespace; /* = G.ifs or malloced */
 #ifndef __U_BOOT__
@@ -2994,7 +2995,24 @@ static int i_getch(struct in_str *i)
 	if (i->p && *i->p != '\0') {
 		ch = (unsigned char)*i->p++;
 		goto out;
+#ifndef __U_BOOT__
 	}
+#else /* __U_BOOT__ */
+	/*
+	 * There are two ways for command to be called:
+	 * 1. The first one is when they are typed by the user.
+	 * 2. The second one is through run_command() (NOTE command run
+	 * internally calls run_command()).
+	 *
+	 * In the second case, we do not get input from the user, so once we
+	 * get a '\0', it means we need to stop.
+	 * NOTE G.run_command_flags is only set on run_command call stack, so
+	 * we use this to know if we come from user input or run_command().
+	 */
+	} else if (i->p && *i->p == '\0' && G.run_command_flags){
+		return EOF;
+	}
+#endif /* __U_BOOT__ */
 #endif
 #ifndef __U_BOOT__
 	/* peek_buf[] is an int array, not char. Can contain EOF. */
@@ -3153,7 +3171,6 @@ static void setup_file_in_str(struct in_str *i)
 #endif /* !__U_BOOT__ */
 }
 
-#ifndef __U_BOOT__
 static void setup_string_in_str(struct in_str *i, const char *s)
 {
 	memset(i, 0, sizeof(*i));
@@ -3161,7 +3178,6 @@ static void setup_string_in_str(struct in_str *i, const char *s)
 	i->p = s;
 }
 
-#endif /* !__U_BOOT__ */
 
 /*
  * o_string support
@@ -7875,7 +7891,11 @@ static int run_and_free_list(struct pipe *pi);
  * NUL: parse all, execute, return
  * ';': parse till ';' or newline, execute, repeat till EOF
  */
+#ifndef __U_BOOT__
 static void parse_and_run_stream(struct in_str *inp, int end_trigger)
+#else /* __U_BOOT__ */
+static int parse_and_run_stream(struct in_str *inp, int end_trigger)
+#endif /* __U_BOOT__ */
 {
 	/* Why we need empty flag?
 	 * An obscure corner case "false; ``; echo $?":
@@ -7884,7 +7904,11 @@ static void parse_and_run_stream(struct in_str *inp, int end_trigger)
 	 * this breaks "false; echo `echo $?`" case.
 	 */
 	bool empty = 1;
+#ifndef __U_BOOT__
 	while (1) {
+#else /* __U_BOOT__ */
+	do {
+#endif /* __U_BOOT__ */
 		struct pipe *pipe_list;
 
 #if ENABLE_HUSH_INTERACTIVE
@@ -7921,21 +7945,57 @@ static void parse_and_run_stream(struct in_str *inp, int end_trigger)
 		empty = 0;
 		if (G_flag_return_in_progress == 1)
 			break;
+#ifndef __U_BOOT__
 	}
+#else /* __U_BOOT__ */
+	/*
+	 * This do/while is needed by run_command to avoid looping on a command
+	 * with syntax error.
+	 */
+	} while (!(G.run_command_flags & FLAG_EXIT_FROM_LOOP));
+
+	return G.last_exitcode;
+#endif /* __U_BOOT__ */
 }
 
 #ifndef __U_BOOT__
 static void parse_and_run_string(const char *s)
+#else /* __U_BOOT__ */
+static int parse_and_run_string(const char *s)
+#endif /* __U_BOOT__ */
 {
 	struct in_str input;
 	//IF_HUSH_LINENO_VAR(unsigned sv = G.parse_lineno;)
 
 	setup_string_in_str(&input, s);
+#ifndef __U_BOOT__
 	parse_and_run_stream(&input, '\0');
+#else /* __U_BOOT__ */
+	return parse_and_run_stream(&input, '\0');
+#endif /* __U_BOOT__ */
 	//IF_HUSH_LINENO_VAR(G.parse_lineno = sv;)
 }
-#endif /* !__U_BOOT__ */
 
+#ifdef __U_BOOT__
+int parse_string_outer(const char *cmd, int flags)
+{
+	int ret;
+	int old_flags;
+
+	/*
+	 * Keep old values of run_command to be able to restore them once
+	 * command was executed.
+	 */
+	old_flags = G.run_command_flags;
+	G.run_command_flags = flags;
+
+	ret = parse_and_run_string(cmd);
+
+	G.run_command_flags = old_flags;
+
+	return ret;
+}
+#endif /* __U_BOOT__ */
 #ifndef __U_BOOT__
 static void parse_and_run_file(HFILE *fp)
 #else /* __U_BOOT__ */
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 14/21] cli: Modify run_command() to add hush 2021 as parser.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (12 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 13/21] cli: hush_2021: Add functions to be called from run_command() Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 15/21] test: hush: Fix instructions list tests for hush 2021 Francis Laniel
                   ` (7 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit then enables using, in code, run_command() while using hush 2021 as
parser.
It also enables the command run to be used by CLI user of hush 2021.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli.c | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/common/cli.c b/common/cli.c
index 090677f7a0..46afa34b98 100644
--- a/common/cli.c
+++ b/common/cli.c
@@ -47,8 +47,21 @@ int run_command(const char *cmd, int flag)
 		hush_flags |= FLAG_CONT_ON_NEWLINE;
 	return parse_string_outer(cmd, hush_flags);
 #else /* HUSH_2021_PARSER */
-	/* Not yet implemented. */
-	return 0;
+	/*
+	 * Possible values for flags are the following:
+	 * FLAG_EXIT_FROM_LOOP: This flags ensures we exit from loop in
+	 * parse_and_run_stream() after first iteration while normal behavior,
+	 * i.e. when called from cli_loop(), is to loop infinitely.
+	 * FLAG_PARSE_SEMICOLON: Hush 2021 parses ';' and does not stop first
+	 * time it sees one. So, I think we do not need this flag.
+	 * FLAG_REPARSING: For the moment, I do not understand the goal of this
+	 * flag.
+	 * FLAG_CONT_ON_NEWLINE: This flag seems to be used to continue parsing
+	 * even when reading '\n' when coming from run_command(). In this case,
+	 * Hush 2021 reads until it finds '\0'. So, I think we do not need this
+	 * flag.
+	 */
+	return parse_string_outer(cmd, FLAG_EXIT_FROM_LOOP);
 #endif
 }
 
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 15/21] test: hush: Fix instructions list tests for hush 2021.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (13 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 14/21] cli: Modify run_command() to add hush 2021 as parser Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 16/21] test: hush: Fix variable expansion " Francis Laniel
                   ` (6 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit modifies the expected result for hush 2021.
Indeed, there were bugs in actual U-Boot hush which were fixed in upstream
Busybox.
As hush 2021 is based on upstream Busybox, these bugs no longer exist.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/list.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/test/hush/list.c b/test/hush/list.c
index 052cf2783c..096bb4f034 100644
--- a/test/hush/list.c
+++ b/test/hush/list.c
@@ -50,8 +50,32 @@ static int hush_test_and_or(struct unit_test_state *uts)
 {
 	/* A && B || C truth table. */
 	ut_asserteq(1, run_command("false && false || false", 0));
+#ifdef CONFIG_HUSH_OLD_PARSER
 	ut_asserteq(1, run_command("false && false || true", 0));
+#elif defined(CONFIG_HUSH_2021_PARSER)
+	/*
+	 * This difference seems to come from a bug solved in Busybox hush.
+	 *
+	 * Indeed, the following expression can be seen like this:
+	 * (false && false) || true
+	 * So, (false && false) returns 1, the second false is not executed, and
+	 * true is executed because of ||.
+	 */
+	ut_assertok(run_command("false && false || true", 0));
+#endif  /* CONFIG_HUSH_2021_PARSER */
+#ifdef CONFIG_HUSH_OLD_PARSER
 	ut_asserteq(1, run_command("false && true || true", 0));
+#elif defined(CONFIG_HUSH_2021_PARSER)
+	/*
+	 * This difference seems to come from a bug solved in Busybox hush.
+	 *
+	 * Indeed, the following expression can be seen like this:
+	 * (false && true) || true
+	 * So, (false && true) returns 1, the true is not executed, and true is
+	 * executed because of ||.
+	 */
+	ut_assertok(run_command("false && true || true", 0));
+#endif  /* CONFIG_HUSH_2021_PARSER */
 	ut_asserteq(1, run_command("false && true || false", 0));
 	ut_assertok(run_command("true && true || false", 0));
 	ut_asserteq(1, run_command("true && false || false", 0));
@@ -69,8 +93,32 @@ static int hush_test_or_and(struct unit_test_state *uts)
 	ut_asserteq(1, run_command("false || false && true", 0));
 	ut_assertok(run_command("false || true && true", 0));
 	ut_asserteq(1, run_command("false || true && false", 0));
+#ifdef CONFIG_HUSH_OLD_PARSER
 	ut_assertok(run_command("true || true && false", 0));
+#elif defined(CONFIG_HUSH_2021_PARSER)
+	/*
+	 * This difference seems to come from a bug solved in Busybox hush.
+	 *
+	 * Indeed, the following expression can be seen like this:
+	 * (true || true) && false
+	 * So, (true || true) returns 0, the second true is not executed, and
+	 * then false is executed because of &&.
+	 */
+	ut_asserteq(1, run_command("true || true && false", 0));
+#endif  /* CONFIG_HUSH_2021_PARSER */
+#ifdef CONFIG_HUSH_OLD_PARSER
 	ut_assertok(run_command("true || false && false", 0));
+#elif defined(CONFIG_HUSH_2021_PARSER)
+	/*
+	 * This difference seems to come from a bug solved in Busybox hush.
+	 *
+	 * Indeed, the following expression can be seen like this:
+	 * (true || false) && false
+	 * So, (true || false) returns 0, the false is not executed, and then
+	 * false is executed because of &&.
+	 */
+	ut_asserteq(1, run_command("true || false && false", 0));
+#endif  /* CONFIG_HUSH_2021_PARSER */
 	ut_assertok(run_command("true || false && true", 0));
 	ut_assertok(run_command("true || true && true", 0));
 
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 16/21] test: hush: Fix variable expansion tests for hush 2021.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (14 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 15/21] test: hush: Fix instructions list tests for hush 2021 Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators Francis Laniel
                   ` (5 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit modifies the expected result for hush 2021.
Indeed, there were bugs in actual U-Boot hush which were fixed in upstream
Busybox.
As hush 2021 is based on upstream Busybox, these bugs no longer exist.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/dollar.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/test/hush/dollar.c b/test/hush/dollar.c
index 9313d145d6..df2ad043f4 100644
--- a/test/hush/dollar.c
+++ b/test/hush/dollar.c
@@ -73,13 +73,27 @@ static int hush_test_simple_dollar(struct unit_test_state *uts)
 	ut_asserteq(1, run_command("dollar_foo='bar quux", 0));
 	/* Next line contains error message. */
 	ut_assert_skipline();
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/*
+	 * For some strange reasons, the console is not empty after running
+	 * above command.
+	 * So, we reset it to not have side effects for other tests.
+	 */
+	console_record_reset_enable();
+#else /* HUSH_OLD_PARSER */
 	ut_assert_console_end();
+#endif /* HUSH_OLD_PARSER */
 
 	ut_asserteq(1, run_command(R"(dollar_foo=bar quux")", 0));
 	/* Two next lines contain error message. */
 	ut_assert_skipline();
 	ut_assert_skipline();
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/* See above comments. */
+	console_record_reset_enable();
+#else /* HUSH_OLD_PARSER */
 	ut_assert_console_end();
+#endif /* HUSH_OLD_PARSER */
 
 	ut_assertok(run_command(R"(dollar_foo='bar "quux')", 0));
 
@@ -93,17 +107,35 @@ static int hush_test_simple_dollar(struct unit_test_state *uts)
 	 */
 	console_record_reset_enable();
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/*
+	 * Old parser returns an error because it waits for closing '\'', but
+	 * this behavior is wrong as the '\'' is surrounded by '"', so no need
+	 * to wait for a closing one.
+	 */
+	ut_assertok(run_command(R"(dollar_foo="bar 'quux")", 0));
+
+	ut_assertok(run_command("echo $dollar_foo", 0));
+	ut_assert_nextline("bar 'quux");
+	ut_assert_console_end();
+#else /* HUSH_OLD_PARSER */
 	ut_asserteq(1, run_command(R"(dollar_foo="bar 'quux")", 0));
 	/* Next line contains error message. */
 	ut_assert_skipline();
 	ut_assert_console_end();
+#endif /* HUSH_OLD_PARSER */
 
 	ut_assertok(run_command("dollar_foo='bar quux'", 0));
 	ut_assertok(run_command("echo $dollar_foo", 0));
 	ut_assert_nextline("bar quux");
 	ut_assert_console_end();
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/* Reset local variable. */
+	ut_assertok(run_command("dollar_foo=", 0));
+#else /* HUSH_OLD_PARSER */
 	puts("Beware: this test set local variable dollar_foo and it cannot be unset!");
+#endif /* HUSH_OLD_PARSER */
 
 	return 0;
 }
@@ -131,7 +163,12 @@ static int hush_test_env_dollar(struct unit_test_state *uts)
 	/* Clean up setting the variable. */
 	env_set("env_foo", NULL);
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/* Reset local variable. */
+	ut_assertok(run_command("env_foo=", 0));
+#else /* HUSH_OLD_PARSER */
 	puts("Beware: this test set local variable env_foo and it cannot be unset!");
+#endif /* HUSH_OLD_PARSER */
 
 	return 0;
 }
@@ -168,7 +205,15 @@ static int hush_test_command_dollar(struct unit_test_state *uts)
 	ut_assertok(run_command(R"(dollar_bar='echo bar\n')", 0));
 
 	ut_assertok(run_command("$dollar_bar", 0));
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/*
+	 * This difference seems to come from a bug solved in Busybox hush.
+	 * Behavior of hush 2021 is coherent with bash and other shells.
+	 */
+	ut_assert_nextline(R"(bar\n)");
+#else /* HUSH_OLD_PARSER */
 	ut_assert_nextline("barn");
+#endif /* HUSH_OLD_PARSER */
 	ut_assert_console_end();
 
 	ut_assertok(run_command("dollar_bar='echo $bar'", 0));
@@ -184,7 +229,13 @@ static int hush_test_command_dollar(struct unit_test_state *uts)
 	ut_assert_nextline("quux");
 	ut_assert_console_end();
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/* Reset local variables. */
+	ut_assertok(run_command("dollar_bar=", 0));
+	ut_assertok(run_command("dollar_quux=", 0));
+#else /* HUSH_OLD_PARSER */
 	puts("Beware: this test sets local variable dollar_bar and dollar_quux and they cannot be unset!");
+#endif /* HUSH_OLD_PARSER */
 
 	return 0;
 }
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (15 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 16/21] test: hush: Fix variable expansion " Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 18/21] cli: hush_2021: Enable if keyword Francis Laniel
                   ` (4 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

In Busybox hush, '<' and '>' are used as redirection operators.
For example, cat foo > bar will write content of file foo inside file bar.
In U-Boot, we do not have file system, so we can hardly redirect command output
inside a file.

But, in actual U-Boot hush, these operators ('<' and '>') are used as string
compare operators.
For example, test aaa < bbb returns 0 as aaa is before bbb in the dictionary.
Busybox hush also permits this, but operators need to be escaped ('\<' and
'\>'), so we stick here to this behavior.
Also, if escaping is needed it permits the developer to think about its code, as
in a lot of case, we want to compare integers (using '-lt' or '-gt') rather than
strings.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli_hush_2021_upstream.c | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
index f2619eb49a..d1f76d4df5 100644
--- a/common/cli_hush_2021_upstream.c
+++ b/common/cli_hush_2021_upstream.c
@@ -6128,7 +6128,33 @@ static struct pipe *parse_stream(char **pstring,
 			if (parse_redirect(&ctx, redir_fd, redir_style, input))
 				goto parse_error_exitcode1;
 			continue; /* get next char */
-#endif /* !__U_BOOT__ */
+#else /* __U_BOOT__ */
+			/*
+			 * In U-Boot, '<' and '>' can be used in test command to test if
+			 * a string is, alphabetically, before or after another.
+			 * In 2021 Busybox hush, using test foo < bar will show the
+			 * following error:
+			 * hush: can't open 'bar': No such file or directory
+			 * Indeed, in this context, '>' and '<' are used as redirection
+			 * operators, to use them as sorting operators, one should use:
+			 * [[ foo < bar ]]
+			 * Or:
+			 * test foo \< bar
+			 *
+			 * For the moment, we will not implement [[ ]], so we will need
+			 * the user to escape these operators to use them as string
+			 * comparaison operators.
+			 * NOTE In my opinion, when you use '<' or '>' I am almost sure
+			 * you wanted to use "-gt" or "-lt" in place, so thinking to
+			 * escape these will make you should check your code (sh syntax
+			 * at this level is, for me, error prone).
+			 */
+			case '>':
+				/* fallthrough */
+			case '<':
+				printf("Redirection operator ('%c') is not supported!\nPlease escape it ('\\%c') to use it as string comparaison operator.\n", (char) ch, (char) ch);
+				goto parse_error_exitcode1;
+#endif /* __U_BOOT__ */
 		case '#':
 			if (ctx.word.length == 0 && !ctx.word.has_quoted_part) {
 				/* skip "#comment" */
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 18/21] cli: hush_2021: Enable if keyword.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (16 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 19/21] test: hush: Fix if tests for hush 2021 Francis Laniel
                   ` (3 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit adds support for "if then else" construct both for command line
interface and through run_command().

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli_hush_2021_upstream.c | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
index d1f76d4df5..5c783494d1 100644
--- a/common/cli_hush_2021_upstream.c
+++ b/common/cli_hush_2021_upstream.c
@@ -3951,7 +3951,6 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 		[PIPE_OR ] = "OR" ,
 		[PIPE_BG ] = "BG" ,
 	};
-#ifndef __U_BOOT__
 	static const char *RES[] = {
 		[RES_NONE ] = "NONE" ,
 # if ENABLE_HUSH_IF
@@ -3981,7 +3980,6 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 		[RES_XXXX ] = "XXXX" ,
 		[RES_SNTX ] = "SNTX" ,
 	};
-#endif /* !__U_BOOT__ */
 	static const char *const CMDTYPE[] = {
 		"{}",
 		"()",
@@ -3999,10 +3997,8 @@ static void debug_print_tree(struct pipe *pi, int lvl)
 				lvl*2, "",
 				pin,
 				pi->num_cmds,
-#ifdef __U_BOOT__
 				(IF_HAS_KEYWORDS(pi->pi_inverted ? "! " :) ""),
 				RES[pi->res_word],
-#endif /* !__U_BOOT__ */
 				pi->followup, PIPE[pi->followup]
 		);
 		prn = 0;
@@ -9792,6 +9788,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 		rcode = 1; /* exitcode if redir failed */
 #ifndef __U_BOOT__
 		if (setup_redirects(command, &squirrel) == 0) {
+#endif /* !__U_BOOT__ */
 			debug_printf_exec(": run_list\n");
 //FIXME: we need to pass squirrel down into run_list()
 //for SH_STANDALONE case, or else this construct:
@@ -9800,10 +9797,11 @@ static NOINLINE int run_pipe(struct pipe *pi)
 //and in SH_STANDALONE mode, "find" is not execed,
 //therefore CLOEXEC on saved fd does not help.
 			rcode = run_list(command->group) & 0xff;
+#ifndef __U_BOOT__
 		}
 		restore_redirects(squirrel);
-		IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
 #endif /* !__U_BOOT__ */
+		IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
 		debug_leave();
 		debug_printf_exec("run_pipe: return %d\n", rcode);
 		return rcode;
@@ -10318,12 +10316,12 @@ static int run_list(struct pipe *pi)
 			break;
 		if (G_flag_return_in_progress == 1)
 			break;
+#endif /* !__U_BOOT__ */
 
 		IF_HAS_KEYWORDS(rword = pi->res_word;)
 		debug_printf_exec(": rword=%d cond_code=%d last_rword=%d\n",
 				rword, cond_code, last_rword);
 
-#endif /* !__U_BOOT__ */
 		sv_errexit_depth = G.errexit_depth;
 		if (
 #if ENABLE_HUSH_IF
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 19/21] test: hush: Fix if tests for hush 2021.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (17 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 18/21] cli: hush_2021: Enable if keyword Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 20/21] cli: hush_2021: Enable loops Francis Laniel
                   ` (2 subsequent siblings)
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit modifies the command run for hush 2021 when using string comparison
operator.
Indeed, '<' and '>' must be escaped to be used as string comparison operator.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/if.c | 43 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)

diff --git a/test/hush/if.c b/test/hush/if.c
index a57d88850f..b2f1f247f5 100644
--- a/test/hush/if.c
+++ b/test/hush/if.c
@@ -39,6 +39,10 @@ static int hush_test_if_basic_operators(struct unit_test_state *uts)
 {
 	char if_formatted[128];
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	console_record_reset_enable();
+#endif /* HUSH_2021_PARSER */
+
 	sprintf(if_formatted, if_format, "test aaa = aaa");
 	ut_assertok(run_command(if_formatted, 0));
 
@@ -52,15 +56,54 @@ static int hush_test_if_basic_operators(struct unit_test_state *uts)
 	ut_asserteq(1, run_command(if_formatted, 0));
 
 	sprintf(if_formatted, if_format, "test aaa < bbb");
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/*
+	 * We need to escape < and > to use as them as string comparison
+	 * operator.
+	 * Otherwise, they are interprated as redirection operators which is not
+	 * supported in U-Boot.
+	 */
+	ut_asserteq(1, run_command(if_formatted, 0));
+	/* Next lines contains error message (which is on two lines). */
+	ut_assert_skipline();
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	sprintf(if_formatted, if_format, R"(test aaa \< bbb)");
+#endif /* HUSH_2021_PARSER */
 	ut_assertok(run_command(if_formatted, 0));
 
 	sprintf(if_formatted, if_format, "test bbb < aaa");
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	ut_asserteq(1, run_command(if_formatted, 0));
+	ut_assert_skipline();
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	sprintf(if_formatted, if_format, R"(test bbb \< aaa)");
+#endif /* HUSH_2021_PARSER */
 	ut_asserteq(1, run_command(if_formatted, 0));
 
 	sprintf(if_formatted, if_format, "test bbb > aaa");
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	ut_asserteq(1, run_command(if_formatted, 0));
+	ut_assert_skipline();
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	sprintf(if_formatted, if_format, R"(test bbb \> aaa)");
+#endif /* HUSH_2021_PARSER */
 	ut_assertok(run_command(if_formatted, 0));
 
 	sprintf(if_formatted, if_format, "test aaa > bbb");
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	ut_asserteq(1, run_command(if_formatted, 0));
+	ut_assert_skipline();
+	ut_assert_skipline();
+	ut_assert_console_end();
+
+	sprintf(if_formatted, if_format, R"(test aaa \> bbb)");
+#endif /* HUSH_2021_PARSER */
 	ut_asserteq(1, run_command(if_formatted, 0));
 
 	sprintf(if_formatted, if_format, "test 123 -eq 123");
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 20/21] cli: hush_2021: Enable loops.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (18 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 19/21] test: hush: Fix if tests for hush 2021 Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2021-12-31 16:13 ` [RFC PATCH v1 21/21] test: hush: Fix loop tests for hush 2021 Francis Laniel
  2022-01-31 22:15 ` [RFC PATCH v1 00/21] Modernize U-Boot shell Tom Rini
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit enables the use of for, while and until loops for command line as
well as with run_command().

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 common/cli_hush_2021_upstream.c | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/common/cli_hush_2021_upstream.c b/common/cli_hush_2021_upstream.c
index 5c783494d1..498fd8d10b 100644
--- a/common/cli_hush_2021_upstream.c
+++ b/common/cli_hush_2021_upstream.c
@@ -10306,7 +10306,7 @@ static int run_list(struct pipe *pi)
 #ifndef __U_BOOT__
 	for (; pi; pi = IF_HUSH_LOOPS(rword == RES_DONE ? loop_top : ) pi->next) {
 #else /* __U_BOOT__ */
-	for (; pi; pi = pi->next) {
+	for (; pi; pi = rword == RES_DONE ? loop_top : pi->next) {
 #endif /* __U_BOOT__ */
 		int r;
 		int sv_errexit_depth;
@@ -10408,7 +10408,20 @@ static int run_list(struct pipe *pi)
 			}
 			/* Insert next value from for_lcur */
 			/* note: *for_lcur already has quotes removed, $var expanded, etc */
+#ifndef __U_BOOT__
 			set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*flag:*/ 0);
+#else /* __U_BOOT__ */
+			/* We cannot use xasprintf, so we emulate it. */
+			char *full_var;
+			char *var = pi->cmds[0].argv[0];
+			char *val = *for_lcur++;
+
+			/* + 1 to take into account =. */
+			full_var = xmalloc(strlen(var) + strlen(val) + 1);
+			sprintf(full_var, "%s=%s", var, val);
+
+			set_local_var(full_var, /*flag:*/ 0);
+#endif /* __U_BOOT__ */
 			continue;
 		}
 		if (rword == RES_IN) {
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* [RFC PATCH v1 21/21] test: hush: Fix loop tests for hush 2021.
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (19 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 20/21] cli: hush_2021: Enable loops Francis Laniel
@ 2021-12-31 16:13 ` Francis Laniel
  2022-01-12 20:03   ` Simon Glass
  2022-01-31 22:15 ` [RFC PATCH v1 00/21] Modernize U-Boot shell Tom Rini
  21 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2021-12-31 16:13 UTC (permalink / raw)
  To: u-boot
  Cc: Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler, Francis Laniel

This commit modifies return code got from while loop as hush 2021 always returns
0 from while loop.

Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
---
 test/hush/loop.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/test/hush/loop.c b/test/hush/loop.c
index 519c78ef7e..56aae719dc 100644
--- a/test/hush/loop.c
+++ b/test/hush/loop.c
@@ -20,7 +20,12 @@ static int hush_test_for(struct unit_test_state *uts)
 	ut_assert_nextline("quux");
 	ut_assert_console_end();
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/* Reset local variable. */
+	ut_assertok(run_command("loop_i=", 0));
+#else /* HUSH_OLD_PARSER */
 	puts("Beware: this test set local variable loop_i and it cannot be unset!");
+#endif /* HUSH_OLD_PARSER */
 
 	return 0;
 }
@@ -30,12 +35,27 @@ static int hush_test_while(struct unit_test_state *uts)
 {
 	console_record_reset_enable();
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/*
+	 * Hush 2021 always returns 0 from while loop...
+	 * You can see code snippet near this line to have a better
+	 * understanding:
+	 * debug_printf_exec(": while expr is false: breaking (exitcode:EXIT_SUCCESS)\n");
+	 */
+	ut_assertok(run_command("while test -z \"$loop_foo\"; do echo bar; loop_foo=quux; done", 0));
+#else /* HUSH_OLD_PARSER */
 	/* Exit status is that of test, so 1 since test is false to quit the loop. */
 	ut_asserteq(1, run_command("while test -z \"$loop_foo\"; do echo bar; loop_foo=quux; done", 0));
+#endif /* HUSH_OLD_PARSER */
 	ut_assert_nextline("bar");
 	ut_assert_console_end();
 
+#if CONFIG_IS_ENABLED(HUSH_2021_PARSER)
+	/* Reset local variable. */
+	ut_assertok(run_command("loop_foo=", 0));
+#else /* HUSH_OLD_PARSER */
 	puts("Beware: this test set local variable loop_foo and it cannot be unset!");
+#endif /* HUSH_OLD_PARSER */
 
 	return 0;
 }
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 01/21] test: Add framework to test hush behavior.
  2021-12-31 16:13 ` [RFC PATCH v1 01/21] test: Add framework to test hush behavior Francis Laniel
@ 2022-01-08 14:53   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-08 14:53 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit introduces a new subcommand to ut: ut hush.

Introduce a new subcommand...

(we know it is a commit :-)

Please fix for all of your patches.

Also please drop the periods in your commit subjects.

Reviewed-by: Simon Glass <sjg@chromium.org>

Regards,
Simon

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 02/21] test: hush: Test hush if/else.
  2021-12-31 16:13 ` [RFC PATCH v1 02/21] test: hush: Test hush if/else Francis Laniel
@ 2022-01-08 14:53   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-08 14:53 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> As asked in commit 9c6bf1715f6a ("test/py: hush_if_test: Add tests to cover
> octal/hex values"), this commit translates test_hush_if_test.py to a C test.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/Makefile |   1 +
>  test/hush/if.c     | 308 +++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 309 insertions(+)
>  create mode 100644 test/hush/if.c

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 03/21] test/py: hush_if_test: Remove the test file.
  2021-12-31 16:13 ` [RFC PATCH v1 03/21] test/py: hush_if_test: Remove the test file Francis Laniel
@ 2022-01-08 14:53   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-08 14:53 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> Commit 9087ab2cc4 ("test/py: hush_if_test: Remove the test file.") translated
> this test to a C test, so this python file is no more needed.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/py/tests/test_hush_if_test.py | 184 -----------------------------
>  1 file changed, 184 deletions(-)
>  delete mode 100644 test/py/tests/test_hush_if_test.py

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 04/21] test: hush: Test hush variable expansion.
  2021-12-31 16:13 ` [RFC PATCH v1 04/21] test: hush: Test hush variable expansion Francis Laniel
@ 2022-01-08 14:53   ` Simon Glass
  2022-02-06 18:22     ` Francis Laniel
  0 siblings, 1 reply; 49+ messages in thread
From: Simon Glass @ 2022-01-08 14:53 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit ensures shell variables are replaced by their values.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/Makefile |   1 +
>  test/hush/dollar.c | 191 +++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 192 insertions(+)
>  create mode 100644 test/hush/dollar.c
>
> diff --git a/test/hush/Makefile b/test/hush/Makefile
> index a3c9ae5106..feb4f71956 100644
> --- a/test/hush/Makefile
> +++ b/test/hush/Makefile
> @@ -5,3 +5,4 @@
>
>  obj-y += cmd_ut_hush.o
>  obj-y += if.o
> +obj-y += dollar.o
> diff --git a/test/hush/dollar.c b/test/hush/dollar.c
> new file mode 100644
> index 0000000000..9313d145d6
> --- /dev/null
> +++ b/test/hush/dollar.c
> @@ -0,0 +1,191 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * (C) Copyright 2021
> + * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
> + */
> +
> +#include <common.h>
> +#include <command.h>
> +#include <env_attr.h>
> +#include <test/hush.h>
> +#include <test/ut.h>
> +
> +static int hush_test_simple_dollar(struct unit_test_state *uts)
> +{
> +       console_record_reset_enable();
> +       ut_assertok(run_command("echo $dollar_foo", 0));
> +#pragma GCC diagnostic push
> +#pragma GCC diagnostic ignored "-Wformat-zero-length"
> +       /*
> +        * Next line is empty as $dollar_foo was not set before, but compiler
> +        * complains about the format being empty, so we disable this warning
> +        * only for this line.
> +        */
> +       ut_assert_nextline("");
> +#pragma GCC diagnostic pop

You might be able to dp:

ut_assert_nextline("%s", "");

> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("echo ${dollar_foo}", 0));
> +#pragma GCC diagnostic push
> +#pragma GCC diagnostic ignored "-Wformat-zero-length"
> +       /*
> +        * Next line is empty as $dollar_foo was not set before, but compiler
> +        * complains about the format being empty, so we disable this warning
> +        * only for this line.
> +        */
> +       ut_assert_nextline("");
> +#pragma GCC diagnostic pop
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("dollar_foo=bar", 0));
> +
> +       ut_assertok(run_command("echo $dollar_foo", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("echo ${dollar_foo}", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       /*
> +        * R is way of giving raw string literals in C++.
> +        * It means a "\n" is a R string literal will not be interpreted as line
> +        * feed but printed as "\n".
> +        * GCC provides it for C as an extension.
> +        */
> +       ut_assertok(run_command(R"(dollar_foo=\$bar)", 0));
> +
> +       ut_assertok(run_command("echo $dollar_foo", 0));
> +       ut_assert_nextline("$bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("dollar_foo='$bar'", 0));
> +
> +       ut_assertok(run_command("echo $dollar_foo", 0));
> +       ut_assert_nextline("$bar");
> +       ut_assert_console_end();
> +
> +       ut_asserteq(1, run_command("dollar_foo=bar quux", 0));
> +       /* Next line contains error message. */
> +       ut_assert_skipline();
> +       ut_assert_console_end();
> +
> +       ut_asserteq(1, run_command("dollar_foo='bar quux", 0));
> +       /* Next line contains error message. */
> +       ut_assert_skipline();
> +       ut_assert_console_end();
> +
> +       ut_asserteq(1, run_command(R"(dollar_foo=bar quux")", 0));
> +       /* Two next lines contain error message. */
> +       ut_assert_skipline();
> +       ut_assert_skipline();
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command(R"(dollar_foo='bar "quux')", 0));
> +
> +       ut_assertok(run_command("echo $dollar_foo", 0));
> +       /*
> +        * This one is buggy.
> +        * ut_assert_nextline(R"(bar "quux)");
> +        * ut_assert_console_end();
> +        *
> +        * So, let's reset output:
> +        */
> +       console_record_reset_enable();
> +
> +       ut_asserteq(1, run_command(R"(dollar_foo="bar 'quux")", 0));
> +       /* Next line contains error message. */
> +       ut_assert_skipline();
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("dollar_foo='bar quux'", 0));
> +       ut_assertok(run_command("echo $dollar_foo", 0));
> +       ut_assert_nextline("bar quux");
> +       ut_assert_console_end();
> +
> +       puts("Beware: this test set local variable dollar_foo and it cannot be unset!");
> +
> +       return 0;
> +}
> +HUSH_TEST(hush_test_simple_dollar, 0);
> +
> +static int hush_test_env_dollar(struct unit_test_state *uts)
> +{
> +       env_set("env_foo", "bar");
> +       console_record_reset_enable();
> +
> +       ut_assertok(run_command("echo $env_foo", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("echo ${env_foo}", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       /* Environment variables have precedence over local variable. */

spelling

also please avoid period before */

> +       ut_assertok(run_command("env_foo=quux", 0));
> +       ut_assertok(run_command("echo ${env_foo}", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       /* Clean up setting the variable. */
> +       env_set("env_foo", NULL);
> +
> +       puts("Beware: this test set local variable env_foo and it cannot be unset!");

Don't you unset it in the previous line?

> +
> +       return 0;
> +}
> +HUSH_TEST(hush_test_env_dollar, 0);
> +
> +static int hush_test_command_dollar(struct unit_test_state *uts)
> +{
> +       console_record_reset_enable();
> +
> +       ut_assertok(run_command(R"(dollar_bar="echo bar")", 0));
> +
> +       ut_assertok(run_command("$dollar_bar", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("${dollar_bar}", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command(R"(dollar_bar="echo
> +       bar")", 0));

newline in the middle of a string ?

> +
> +       ut_assertok(run_command("$dollar_bar", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command(R"(dollar_bar='echo bar
> +       ')", 0));
> +
> +       ut_assertok(run_command("$dollar_bar", 0));
> +       ut_assert_nextline("bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command(R"(dollar_bar='echo bar\n')", 0));
> +
> +       ut_assertok(run_command("$dollar_bar", 0));
> +       ut_assert_nextline("barn");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("dollar_bar='echo $bar'", 0));
> +
> +       ut_assertok(run_command("$dollar_bar", 0));
> +       ut_assert_nextline("$bar");
> +       ut_assert_console_end();
> +
> +       ut_assertok(run_command("dollar_quux=quux", 0));
> +       ut_assertok(run_command(R"(dollar_bar="echo $dollar_quux")", 0));
> +
> +       ut_assertok(run_command("$dollar_bar", 0));
> +       ut_assert_nextline("quux");
> +       ut_assert_console_end();
> +
> +       puts("Beware: this test sets local variable dollar_bar and dollar_quux and they cannot be unset!");
> +
> +       return 0;
> +}
> +HUSH_TEST(hush_test_command_dollar, 0);
> --
> 2.25.1
>

Regards,
Simon

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 05/21] test: hush: Test hush commands list.
  2021-12-31 16:13 ` [RFC PATCH v1 05/21] test: hush: Test hush commands list Francis Laniel
@ 2022-01-08 14:54   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-08 14:54 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit ensures behavior of commands separated by ';', '&&' and '||'.

s/ensures/checks/

or maybe verifies?


>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/Makefile |  1 +
>  test/hush/list.c   | 79 ++++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 80 insertions(+)
>  create mode 100644 test/hush/list.c

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 06/21] test: hush: Test hush loops.
  2021-12-31 16:13 ` [RFC PATCH v1 06/21] test: hush: Test hush loops Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> The added tests ensure correct behavior of for, while and until loops.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/Makefile |  1 +
>  test/hush/loop.c   | 64 ++++++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 65 insertions(+)
>  create mode 100644 test/hush/loop.c

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 09/21] cli: Add choice for hush parser.
  2021-12-31 16:13 ` [RFC PATCH v1 09/21] cli: Add choice for hush parser Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> For the moment, the choice contains only entry: HUSH_OLD_PARSER which is the
> default.
> The goal of this commit is to prepare the field to add a new hush parser and it
> does not change anything to actual behavior.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  cmd/Kconfig     | 14 ++++++++++++++
>  common/Makefile |  2 +-
>  2 files changed, 15 insertions(+), 1 deletion(-)
>

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 10/21] cli: Add HUSH_2021_PARSER to hush parser choice .
  2021-12-31 16:13 ` [RFC PATCH v1 10/21] cli: Add HUSH_2021_PARSER to hush parser choice Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> To use hush 2021 as U-Boot shell, one should tick "Use hush 2021 parser" under
> "Hush flavor to use" choice.
> Since this is a choice, you cannot use at the same time old and new flavor of
> hush.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  cmd/Kconfig     | 8 ++++++++
>  common/Makefile | 1 +
>  2 files changed, 9 insertions(+)
>

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 08/21] cli: Port Busybox 2021 hush to U-Boot.
  2021-12-31 16:13 ` [RFC PATCH v1 08/21] cli: Port Busybox 2021 hush to U-Boot Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit adds new file cli_hush_2021.c, it is a copy of Busybox hush file as
> it was of time to commit 37460f5da.
> This commit modifies Busybox hush to not compile some part specific to Busybox
> and adds some code
> needed by U-Boot.
> The modifications consists mainly on adding code #if(n)def guards.
>
> For the moment, this refurbished flavor of hush only permits running command
> without any keywords (i.e., if and for are not recognized) or variable expansion
> (i.e., echo $foo prints foo and not value stored in variable foo).
>
> A new file was also added to define some functions specific to U-Boot.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> Signed-off-by: Harald Seiler <hws@denx.de>
> ---
>  common/cli_hush_2021.c          | 292 ++++++++++++++++++++
>  common/cli_hush_2021_upstream.c | 470 +++++++++++++++++++++++++++++++-
>  2 files changed, 761 insertions(+), 1 deletion(-)
>  create mode 100644 common/cli_hush_2021.c
>

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 11/21] cli: Enables using hush 2021 parser as command line parser.
  2021-12-31 16:13 ` [RFC PATCH v1 11/21] cli: Enables using hush 2021 parser as command line parser Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> If one defines HUSH_2021_PARSER, cli_loop() will jump on hush 2021 parser code.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli.c       | 14 +++++++++++---
>  include/cli_hush.h |  8 ++++++++
>  2 files changed, 19 insertions(+), 3 deletions(-)

Reviewed-by: Simon Glass <sjg@chromium.org>

Can you avoid using the preprocessor here?

Could be cleaned up later.

Regards,
Simon

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 12/21] cli: hush_2021: Enable variables expansion for hush 2021.
  2021-12-31 16:13 ` [RFC PATCH v1 12/21] cli: hush_2021: Enable variables expansion for hush 2021 Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> The following commit enables variables expansion for hush 2021, both for local
> and environment variables.
> So the following commands:
> foo=bar
> echo $foo
> setenv bar foo
> echo $bar
> leads to "bar" and "foo" being printed on console output.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli_hush_2021.c          | 17 ++++++++
>  common/cli_hush_2021_upstream.c | 74 ++++++++++++++++++++++++++++++---
>  2 files changed, 86 insertions(+), 5 deletions(-)
Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 14/21] cli: Modify run_command() to add hush 2021 as parser.
  2021-12-31 16:13 ` [RFC PATCH v1 14/21] cli: Modify run_command() to add hush 2021 as parser Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit then enables using, in code, run_command() while using hush 2021 as
> parser.
> It also enables the command run to be used by CLI user of hush 2021.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli.c | 17 +++++++++++++++--
>  1 file changed, 15 insertions(+), 2 deletions(-)
>

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 13/21] cli: hush_2021: Add functions to be called from run_command().
  2021-12-31 16:13 ` [RFC PATCH v1 13/21] cli: hush_2021: Add functions to be called from run_command() Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> run_command() is called internally by the command run and it can also be called
> directly from U-Boot code, e.g. to do unit tests.
> This commit adds this path to go to hush 2021.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli_hush_2021_upstream.c | 66 +++++++++++++++++++++++++++++++--
>  1 file changed, 63 insertions(+), 3 deletions(-)

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 15/21] test: hush: Fix instructions list tests for hush 2021.
  2021-12-31 16:13 ` [RFC PATCH v1 15/21] test: hush: Fix instructions list tests for hush 2021 Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit modifies the expected result for hush 2021.
> Indeed, there were bugs in actual U-Boot hush which were fixed in upstream
> Busybox.
> As hush 2021 is based on upstream Busybox, these bugs no longer exist.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/list.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 48 insertions(+)

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 16/21] test: hush: Fix variable expansion tests for hush 2021.
  2021-12-31 16:13 ` [RFC PATCH v1 16/21] test: hush: Fix variable expansion " Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  2022-02-06 18:23     ` Francis Laniel
  0 siblings, 1 reply; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit modifies the expected result for hush 2021.
> Indeed, there were bugs in actual U-Boot hush which were fixed in upstream
> Busybox.
> As hush 2021 is based on upstream Busybox, these bugs no longer exist.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/dollar.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 51 insertions(+)

Reviewed-by: Simon Glass <sjg@chromium.org>

Can you avoid the preprocessor macros?

Also, for testing we really want to be able to run both sets of tests
in sandbox, so the parser needs to be run-time selectable.

Regards,
Simon

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 07/21] cli: Add Busybox upstream hush.c file.
  2021-12-31 16:13 ` [RFC PATCH v1 07/21] cli: Add Busybox upstream hush.c file Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit adds new file cli_hush_2021_upstream.c, it is a copy of Busybox hush
> file it was of time to Busybox commit 37460f5da.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli_hush_2021_upstream.c | 12162 ++++++++++++++++++++++++++++++
>  1 file changed, 12162 insertions(+)
>  create mode 100644 common/cli_hush_2021_upstream.c

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators.
  2021-12-31 16:13 ` [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  2022-02-06 18:23     ` Francis Laniel
  0 siblings, 1 reply; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> In Busybox hush, '<' and '>' are used as redirection operators.
> For example, cat foo > bar will write content of file foo inside file bar.
> In U-Boot, we do not have file system, so we can hardly redirect command output
> inside a file.
>
> But, in actual U-Boot hush, these operators ('<' and '>') are used as string
> compare operators.
> For example, test aaa < bbb returns 0 as aaa is before bbb in the dictionary.
> Busybox hush also permits this, but operators need to be escaped ('\<' and
> '\>'), so we stick here to this behavior.
> Also, if escaping is needed it permits the developer to think about its code, as
> in a lot of case, we want to compare integers (using '-lt' or '-gt') rather than
> strings.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli_hush_2021_upstream.c | 28 +++++++++++++++++++++++++++-
>  1 file changed, 27 insertions(+), 1 deletion(-)

Reviewed-by: Simon Glass <sjg@chromium.org>

We should add a file interface to U-Boot. Is that the only impediment
to implementing redirection? I thought the problem was lack of fork()
?

Regards,
Simon

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 18/21] cli: hush_2021: Enable if keyword.
  2021-12-31 16:13 ` [RFC PATCH v1 18/21] cli: hush_2021: Enable if keyword Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit adds support for "if then else" construct both for command line
> interface and through run_command().
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli_hush_2021_upstream.c | 10 ++++------
>  1 file changed, 4 insertions(+), 6 deletions(-)
>

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 19/21] test: hush: Fix if tests for hush 2021.
  2021-12-31 16:13 ` [RFC PATCH v1 19/21] test: hush: Fix if tests for hush 2021 Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit modifies the command run for hush 2021 when using string comparison
> operator.
> Indeed, '<' and '>' must be escaped to be used as string comparison operator.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/if.c | 43 +++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 43 insertions(+)
>

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 21/21] test: hush: Fix loop tests for hush 2021.
  2021-12-31 16:13 ` [RFC PATCH v1 21/21] test: hush: Fix loop tests for hush 2021 Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit modifies return code got from while loop as hush 2021 always returns
> 0 from while loop.
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  test/hush/loop.c | 20 ++++++++++++++++++++
>  1 file changed, 20 insertions(+)

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 20/21] cli: hush_2021: Enable loops.
  2021-12-31 16:13 ` [RFC PATCH v1 20/21] cli: hush_2021: Enable loops Francis Laniel
@ 2022-01-12 20:03   ` Simon Glass
  0 siblings, 0 replies; 49+ messages in thread
From: Simon Glass @ 2022-01-12 20:03 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

On Fri, 31 Dec 2021 at 09:14, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> This commit enables the use of for, while and until loops for command line as
> well as with run_command().
>
> Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> ---
>  common/cli_hush_2021_upstream.c | 15 ++++++++++++++-
>  1 file changed, 14 insertions(+), 1 deletion(-)
>

Reviewed-by: Simon Glass <sjg@chromium.org>

Can you shorten the filename here a bit?

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 00/21] Modernize U-Boot shell
  2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
                   ` (20 preceding siblings ...)
  2021-12-31 16:13 ` [RFC PATCH v1 21/21] test: hush: Fix loop tests for hush 2021 Francis Laniel
@ 2022-01-31 22:15 ` Tom Rini
  21 siblings, 0 replies; 49+ messages in thread
From: Tom Rini @ 2022-01-31 22:15 UTC (permalink / raw)
  To: Francis Laniel
  Cc: u-boot, Marek Behun, Michael Nazzareno Trimarchi, Simon Glass,
	Wolfgang Denk, Harald Seiler

[-- Attachment #1: Type: text/plain, Size: 6478 bytes --]

On Fri, Dec 31, 2021 at 05:13:06PM +0100, Francis Laniel wrote:

> Hi.
> 
> First I hope you are fine and the same for your relatives.
> 
> During 2021 summer, Sean Anderson wrote a contribution to add a new shell, based
> on LIL, to U-Boot [1][2].
> While one of the goals of this contribution was to address the fact actual
> U-Boot shell, which is based on Busybox hush, is old there was a discussion
> about adding a new shell versus updating the actual one [3][4].
> 
> So, in this series, with Harald Seiler, we updated the actual U-Boot shell to
> reflect what is currently in Busybox source code.
> Basically, this contribution is about taking a snapshot of Busybox shell/hush.c
> file (as it exists in commit 37460f5da) and adapt it to suit U-Boot needs.
> 
> This contribution was written to be as backward-compatible as possible to avoid
> breaking the existing.
> So, the 2021 hush flavor offers the same as the actual, that is to say:
> 1. Variable expansion.
> 2. Instruction lists (;, && and ||).
> 3. If, then and else.
> 4. Loops (for, while and until).
> No new features offered by Busybox hush were implemented (e.g. functions).
> 
> In terms of testing, new unit tests were added to ut to ensure the new behavior
> is the same as the old one and it does not add regression.
> Nonetheless, if old behavior was buggy and fixed upstream, the fix is then added
> to U-Boot [5].
> In sandbox, all of these tests pass smoothly:
> 2021> printenv board
> board=sandbox
> 2021> ut hush
> Running 20 hush tests
> ...
> Failures: 0
> Thanks to the effort of Harald Seiler, I was successful booting a board:
> 2021> printenv board_rev
> board_rev=iMX8MP
> 2021> boot
> ...
> root@iMX8MPboard:~# fw_printenv board_rev
> board_rev=iMX8MP
> 
> I marked this contribution as RFC to indeed collect your opinion.
> My goal is not to change suddenly actual shell to this one, we clearly need a
> transition period to think about it.
> I think it is better to see this contribution as a proof of concept which shows
> it is possible to update the actual shell.
> 
> If you want to review it - your review will really be appreciated - here are
> some information regarding the commits:
> * commits marked as "test:" deal with unit tests.
> * commit "cli: Add Busybox upstream hush.c file." copies Busybox shell/hush.c
> into U-Boot tree, this explain why this commit contains around 12000 additions.
> * commit "cli: Port Busybox 2021 hush to U-Boot." modifies previously added file
> to permit us to use this as new shell.
> The really good idea of #include'ing Busybox code into a wrapper file to define
> some particular functions while minimizing modifications to upstream code comes
> from Harald Seiler.
> * Other commits focus on enabling features we need (e.g. if).
> 
> Francis Laniel (21):
>   test: Add framework to test hush behavior.
>   test: hush: Test hush if/else.
>   test/py: hush_if_test: Remove the test file.
>   test: hush: Test hush variable expansion.
>   test: hush: Test hush commands list.
>   test: hush: Test hush loops.
>   cli: Add Busybox upstream hush.c file.
>   cli: Port Busybox 2021 hush to U-Boot.
>   cli: Add choice for hush parser.
>   cli: Add HUSH_2021_PARSER to hush parser choice .
>   cli: Enables using hush 2021 parser as command line parser.
>   cli: hush_2021: Enable variables expansion for hush 2021.
>   cli: hush_2021: Add functions to be called from run_command().
>   cli: Modify run_command() to add hush 2021 as parser.
>   test: hush: Fix instructions list tests for hush 2021.
>   test: hush: Fix variable expansion tests for hush 2021.
>   cli: hush_2021: Enable using \< and \> as string compare operators.
>   cli: hush_2021: Enable if keyword.
>   test: hush: Fix if tests for hush 2021.
>   cli: hush_2021: Enable loops.
>   test: hush: Fix loop tests for hush 2021.
> 
>  cmd/Kconfig                        |    22 +
>  common/Makefile                    |     3 +-
>  common/cli.c                       |    27 +-
>  common/cli_hush_2021.c             |   309 +
>  common/cli_hush_2021_upstream.c    | 12791 +++++++++++++++++++++++++++
>  include/cli_hush.h                 |     8 +
>  include/test/hush.h                |    15 +
>  include/test/suites.h              |     1 +
>  test/Makefile                      |     3 +
>  test/cmd_ut.c                      |     6 +
>  test/hush/Makefile                 |    10 +
>  test/hush/cmd_ut_hush.c            |    20 +
>  test/hush/dollar.c                 |   242 +
>  test/hush/if.c                     |   351 +
>  test/hush/list.c                   |   127 +
>  test/hush/loop.c                   |    84 +
>  test/py/tests/test_hush_if_test.py |   184 -
>  17 files changed, 14015 insertions(+), 188 deletions(-)
>  create mode 100644 common/cli_hush_2021.c
>  create mode 100644 common/cli_hush_2021_upstream.c
>  create mode 100644 include/test/hush.h
>  create mode 100644 test/hush/Makefile
>  create mode 100644 test/hush/cmd_ut_hush.c
>  create mode 100644 test/hush/dollar.c
>  create mode 100644 test/hush/if.c
>  create mode 100644 test/hush/list.c
>  create mode 100644 test/hush/loop.c
>  delete mode 100644 test/py/tests/test_hush_if_test.py
> 
> 
> Best regards, thank you in advance for your reviews and happy new year!

Sorry for the extremely delayed response here.  I'm glad you did this.
The first thing I see is that there's still some fail to build problems
to sort out:
https://source.denx.de/u-boot/u-boot/-/commits/TEST/RFC-modernize-u-boot-shell
and doing a world build locally shows a few more also fall out so please
give https://u-boot.readthedocs.io/en/latest/develop/ci_testing.html a
read on how to get at least an Azure run for the next iteration.

When enabled by default there's a few unused function warnings (I
enabled the option for all boards for a test) that also need to be
addressed.

Then the next step would be to get all of the current tests to pass, for
me I get:
FAILED test/py/tests/test_md.py::test_md_repeat - AssertionError: assert '00200040: ' in ''
FAILED test/py/tests/test_ut.py::test_ut[ut_hush_hush_test_simple_dollar] - Exception: Bad p...
FAILED test/py/tests/test_ut.py::test_ut[ut_lib_lib_test_hush_echo] - Exception: Bad pattern...
after dropping the forced SYS_PROMPT.

Again, thanks for doing this and I do look forward to the next posting.

-- 
Tom

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 659 bytes --]

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 04/21] test: hush: Test hush variable expansion.
  2022-01-08 14:53   ` Simon Glass
@ 2022-02-06 18:22     ` Francis Laniel
  0 siblings, 0 replies; 49+ messages in thread
From: Francis Laniel @ 2022-02-06 18:22 UTC (permalink / raw)
  To: Simon Glass
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi.


First, sorry for the delayed answer and thank you a lot for all your reviews!

Le samedi 8 janvier 2022, 15:53:49 CET Simon Glass a écrit :
> Hi Francis,
> 
> On Fri, 31 Dec 2021 at 09:14, Francis Laniel
> 
> <francis.laniel@amarulasolutions.com> wrote:
> > This commit ensures shell variables are replaced by their values.
> > 
> > Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> > ---
> > 
> >  test/hush/Makefile |   1 +
> >  test/hush/dollar.c | 191 +++++++++++++++++++++++++++++++++++++++++++++
> >  2 files changed, 192 insertions(+)
> >  create mode 100644 test/hush/dollar.c
> > 
> > diff --git a/test/hush/Makefile b/test/hush/Makefile
> > index a3c9ae5106..feb4f71956 100644
> > --- a/test/hush/Makefile
> > +++ b/test/hush/Makefile
> > @@ -5,3 +5,4 @@
> > 
> >  obj-y += cmd_ut_hush.o
> >  obj-y += if.o
> > 
> > +obj-y += dollar.o
> > diff --git a/test/hush/dollar.c b/test/hush/dollar.c
> > new file mode 100644
> > index 0000000000..9313d145d6
> > --- /dev/null
> > +++ b/test/hush/dollar.c
> > @@ -0,0 +1,191 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * (C) Copyright 2021
> > + * Francis Laniel, Amarula Solutions, francis.laniel@amarulasolutions.com
> > + */
> > +
> > +#include <common.h>
> > +#include <command.h>
> > +#include <env_attr.h>
> > +#include <test/hush.h>
> > +#include <test/ut.h>
> > +
> > +static int hush_test_simple_dollar(struct unit_test_state *uts)
> > +{
> > +       console_record_reset_enable();
> > +       ut_assertok(run_command("echo $dollar_foo", 0));
> > +#pragma GCC diagnostic push
> > +#pragma GCC diagnostic ignored "-Wformat-zero-length"
> > +       /*
> > +        * Next line is empty as $dollar_foo was not set before, but
> > compiler +        * complains about the format being empty, so we disable
> > this warning +        * only for this line.
> > +        */
> > +       ut_assert_nextline("");
> > +#pragma GCC diagnostic pop
> 
> You might be able to dp:
> 
> ut_assert_nextline("%s", "");

Thank you for the trick! With this I was able to get rid of this bad warning 
canceling.

> 
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("echo ${dollar_foo}", 0));
> > +#pragma GCC diagnostic push
> > +#pragma GCC diagnostic ignored "-Wformat-zero-length"
> > +       /*
> > +        * Next line is empty as $dollar_foo was not set before, but
> > compiler +        * complains about the format being empty, so we disable
> > this warning +        * only for this line.
> > +        */
> > +       ut_assert_nextline("");
> > +#pragma GCC diagnostic pop
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("dollar_foo=bar", 0));
> > +
> > +       ut_assertok(run_command("echo $dollar_foo", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("echo ${dollar_foo}", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       /*
> > +        * R is way of giving raw string literals in C++.
> > +        * It means a "\n" is a R string literal will not be interpreted
> > as line +        * feed but printed as "\n".
> > +        * GCC provides it for C as an extension.
> > +        */
> > +       ut_assertok(run_command(R"(dollar_foo=\$bar)", 0));
> > +
> > +       ut_assertok(run_command("echo $dollar_foo", 0));
> > +       ut_assert_nextline("$bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("dollar_foo='$bar'", 0));
> > +
> > +       ut_assertok(run_command("echo $dollar_foo", 0));
> > +       ut_assert_nextline("$bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_asserteq(1, run_command("dollar_foo=bar quux", 0));
> > +       /* Next line contains error message. */
> > +       ut_assert_skipline();
> > +       ut_assert_console_end();
> > +
> > +       ut_asserteq(1, run_command("dollar_foo='bar quux", 0));
> > +       /* Next line contains error message. */
> > +       ut_assert_skipline();
> > +       ut_assert_console_end();
> > +
> > +       ut_asserteq(1, run_command(R"(dollar_foo=bar quux")", 0));
> > +       /* Two next lines contain error message. */
> > +       ut_assert_skipline();
> > +       ut_assert_skipline();
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command(R"(dollar_foo='bar "quux')", 0));
> > +
> > +       ut_assertok(run_command("echo $dollar_foo", 0));
> > +       /*
> > +        * This one is buggy.
> > +        * ut_assert_nextline(R"(bar "quux)");
> > +        * ut_assert_console_end();
> > +        *
> > +        * So, let's reset output:
> > +        */
> > +       console_record_reset_enable();
> > +
> > +       ut_asserteq(1, run_command(R"(dollar_foo="bar 'quux")", 0));
> > +       /* Next line contains error message. */
> > +       ut_assert_skipline();
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("dollar_foo='bar quux'", 0));
> > +       ut_assertok(run_command("echo $dollar_foo", 0));
> > +       ut_assert_nextline("bar quux");
> > +       ut_assert_console_end();
> > +
> > +       puts("Beware: this test set local variable dollar_foo and it
> > cannot be unset!"); +
> > +       return 0;
> > +}
> > +HUSH_TEST(hush_test_simple_dollar, 0);
> > +
> > +static int hush_test_env_dollar(struct unit_test_state *uts)
> > +{
> > +       env_set("env_foo", "bar");
> > +       console_record_reset_enable();
> > +
> > +       ut_assertok(run_command("echo $env_foo", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("echo ${env_foo}", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       /* Environment variables have precedence over local variable. */
> 
> spelling
> 
> also please avoid period before */
> 
> > +       ut_assertok(run_command("env_foo=quux", 0));
> > +       ut_assertok(run_command("echo ${env_foo}", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       /* Clean up setting the variable. */
> > +       env_set("env_foo", NULL);
> > +
> > +       puts("Beware: this test set local variable env_foo and it cannot
> > be unset!");
> Don't you unset it in the previous line?

I unsetted the environment variable named env_foo, but the local variable 
sadly remained because it is not possible to unset local variable.
So, after running "ut hush", the following should occur:
=> ut hush
Running 20 hush tests
...
Failures: 0
=> echo env_foo
quux

> > +
> > +       return 0;
> > +}
> > +HUSH_TEST(hush_test_env_dollar, 0);
> > +
> > +static int hush_test_command_dollar(struct unit_test_state *uts)
> > +{
> > +       console_record_reset_enable();
> > +
> > +       ut_assertok(run_command(R"(dollar_bar="echo bar")", 0));
> > +
> > +       ut_assertok(run_command("$dollar_bar", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("${dollar_bar}", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command(R"(dollar_bar="echo
> > +       bar")", 0));
> 
> newline in the middle of a string ?

I think this should be a problem with how the mailer rendered this because on 
my side it is:
ut_assertok(run_command(R"(dollar_bar="echo bar")", 0));
I will check this on v2 (which should arrive from within 15-30 minutes).

> 
> > +
> > +       ut_assertok(run_command("$dollar_bar", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command(R"(dollar_bar='echo bar
> > +       ')", 0));
> > +
> > +       ut_assertok(run_command("$dollar_bar", 0));
> > +       ut_assert_nextline("bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command(R"(dollar_bar='echo bar\n')", 0));
> > +
> > +       ut_assertok(run_command("$dollar_bar", 0));
> > +       ut_assert_nextline("barn");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("dollar_bar='echo $bar'", 0));
> > +
> > +       ut_assertok(run_command("$dollar_bar", 0));
> > +       ut_assert_nextline("$bar");
> > +       ut_assert_console_end();
> > +
> > +       ut_assertok(run_command("dollar_quux=quux", 0));
> > +       ut_assertok(run_command(R"(dollar_bar="echo $dollar_quux")", 0));
> > +
> > +       ut_assertok(run_command("$dollar_bar", 0));
> > +       ut_assert_nextline("quux");
> > +       ut_assert_console_end();
> > +
> > +       puts("Beware: this test sets local variable dollar_bar and
> > dollar_quux and they cannot be unset!"); +
> > +       return 0;
> > +}
> > +HUSH_TEST(hush_test_command_dollar, 0);
> > --
> > 2.25.1
> 
> Regards,
> Simon





^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 16/21] test: hush: Fix variable expansion tests for hush 2021.
  2022-01-12 20:03   ` Simon Glass
@ 2022-02-06 18:23     ` Francis Laniel
  2022-02-07 20:22       ` Simon Glass
  0 siblings, 1 reply; 49+ messages in thread
From: Francis Laniel @ 2022-02-06 18:23 UTC (permalink / raw)
  To: Simon Glass
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Le mercredi 12 janvier 2022, 21:03:37 CET Simon Glass a écrit :
> Hi Francis,
> 
> On Fri, 31 Dec 2021 at 09:14, Francis Laniel
> 
> <francis.laniel@amarulasolutions.com> wrote:
> > This commit modifies the expected result for hush 2021.
> > Indeed, there were bugs in actual U-Boot hush which were fixed in upstream
> > Busybox.
> > As hush 2021 is based on upstream Busybox, these bugs no longer exist.
> > 
> > Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> > ---
> > 
> >  test/hush/dollar.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++
> >  1 file changed, 51 insertions(+)
> 
> Reviewed-by: Simon Glass <sjg@chromium.org>
> 
> Can you avoid the preprocessor macros?
> 
> Also, for testing we really want to be able to run both sets of tests
> in sandbox, so the parser needs to be run-time selectable.

To be sure, you would like to have a global variable named, for example, 
parser which value could be:
* parser = "old", for old hush parser
* parser = "2021", for "new" hush parser
Is my understanding correct?

> Regards,
> Simon





^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators.
  2022-01-12 20:03   ` Simon Glass
@ 2022-02-06 18:23     ` Francis Laniel
  0 siblings, 0 replies; 49+ messages in thread
From: Francis Laniel @ 2022-02-06 18:23 UTC (permalink / raw)
  To: Simon Glass
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Le mercredi 12 janvier 2022, 21:03:40 CET Simon Glass a écrit :
> Hi Francis,
> 
> On Fri, 31 Dec 2021 at 09:14, Francis Laniel
> 
> <francis.laniel@amarulasolutions.com> wrote:
> > In Busybox hush, '<' and '>' are used as redirection operators.
> > For example, cat foo > bar will write content of file foo inside file bar.
> > In U-Boot, we do not have file system, so we can hardly redirect command
> > output inside a file.
> > 
> > But, in actual U-Boot hush, these operators ('<' and '>') are used as
> > string compare operators.
> > For example, test aaa < bbb returns 0 as aaa is before bbb in the
> > dictionary. Busybox hush also permits this, but operators need to be
> > escaped ('\<' and '\>'), so we stick here to this behavior.
> > Also, if escaping is needed it permits the developer to think about its
> > code, as in a lot of case, we want to compare integers (using '-lt' or
> > '-gt') rather than strings.
> > 
> > Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> > ---
> > 
> >  common/cli_hush_2021_upstream.c | 28 +++++++++++++++++++++++++++-
> >  1 file changed, 27 insertions(+), 1 deletion(-)
> 
> Reviewed-by: Simon Glass <sjg@chromium.org>
> 
> We should add a file interface to U-Boot. Is that the only impediment
> to implementing redirection? I thought the problem was lack of fork()
> ?

From my understanding of Busybox hush code dealing with I/O redirection 
(mainly parse_redirect() and struct redir_struct), it seems fork is never used 
here.
So, if we add something to emulate file descriptor it should be OK to add this 
feature to U-Boot.

> Regards,
> Simon





^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 16/21] test: hush: Fix variable expansion tests for hush 2021.
  2022-02-06 18:23     ` Francis Laniel
@ 2022-02-07 20:22       ` Simon Glass
  2022-03-24  1:49         ` Francis Laniel
  0 siblings, 1 reply; 49+ messages in thread
From: Simon Glass @ 2022-02-07 20:22 UTC (permalink / raw)
  To: Francis Laniel
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi Francis,

On Sun, 6 Feb 2022 at 11:23, Francis Laniel
<francis.laniel@amarulasolutions.com> wrote:
>
> Le mercredi 12 janvier 2022, 21:03:37 CET Simon Glass a écrit :
> > Hi Francis,
> >
> > On Fri, 31 Dec 2021 at 09:14, Francis Laniel
> >
> > <francis.laniel@amarulasolutions.com> wrote:
> > > This commit modifies the expected result for hush 2021.
> > > Indeed, there were bugs in actual U-Boot hush which were fixed in upstream
> > > Busybox.
> > > As hush 2021 is based on upstream Busybox, these bugs no longer exist.
> > >
> > > Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> > > ---
> > >
> > >  test/hush/dollar.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++
> > >  1 file changed, 51 insertions(+)
> >
> > Reviewed-by: Simon Glass <sjg@chromium.org>
> >
> > Can you avoid the preprocessor macros?
> >
> > Also, for testing we really want to be able to run both sets of tests
> > in sandbox, so the parser needs to be run-time selectable.
>
> To be sure, you would like to have a global variable named, for example,
> parser which value could be:
> * parser = "old", for old hush parser
> * parser = "2021", for "new" hush parser
> Is my understanding correct?

Yes although an int would be better than a string, perhaps controlled
by a flag in global_data and a 'cli' subcommand to change  the
setting.

Regards,
Simon

^ permalink raw reply	[flat|nested] 49+ messages in thread

* Re: [RFC PATCH v1 16/21] test: hush: Fix variable expansion tests for hush 2021.
  2022-02-07 20:22       ` Simon Glass
@ 2022-03-24  1:49         ` Francis Laniel
  0 siblings, 0 replies; 49+ messages in thread
From: Francis Laniel @ 2022-03-24  1:49 UTC (permalink / raw)
  To: Simon Glass
  Cc: U-Boot Mailing List, Marek Behun, Michael Nazzareno Trimarchi,
	Wolfgang Denk, Harald Seiler

Hi.


(Sorry for the very late reply)

Le lundi 7 février 2022, 21:22:20 CET Simon Glass a écrit :
> Hi Francis,
> 
> On Sun, 6 Feb 2022 at 11:23, Francis Laniel
> 
> <francis.laniel@amarulasolutions.com> wrote:
> > Le mercredi 12 janvier 2022, 21:03:37 CET Simon Glass a écrit :
> > > Hi Francis,
> > > 
> > > On Fri, 31 Dec 2021 at 09:14, Francis Laniel
> > > 
> > > <francis.laniel@amarulasolutions.com> wrote:
> > > > This commit modifies the expected result for hush 2021.
> > > > Indeed, there were bugs in actual U-Boot hush which were fixed in
> > > > upstream
> > > > Busybox.
> > > > As hush 2021 is based on upstream Busybox, these bugs no longer exist.
> > > > 
> > > > Signed-off-by: Francis Laniel <francis.laniel@amarulasolutions.com>
> > > > ---
> > > > 
> > > >  test/hush/dollar.c | 51
> > > >  ++++++++++++++++++++++++++++++++++++++++++++++
> > > >  1 file changed, 51 insertions(+)
> > > 
> > > Reviewed-by: Simon Glass <sjg@chromium.org>
> > > 
> > > Can you avoid the preprocessor macros?
> > > 
> > > Also, for testing we really want to be able to run both sets of tests
> > > in sandbox, so the parser needs to be run-time selectable.
> > 
> > To be sure, you would like to have a global variable named, for example,
> > parser which value could be:
> > * parser = "old", for old hush parser
> > * parser = "2021", for "new" hush parser
> > Is my understanding correct?
> 
> Yes although an int would be better than a string, perhaps controlled
> by a flag in global_data and a 'cli' subcommand to change  the
> setting.

If I understand correctly, that would mean to compile both the old and new 
parser?
Hence, I think I will need to rename some functions in the new parser to avoid 
problems at this level.

> Regards,
> Simon





^ permalink raw reply	[flat|nested] 49+ messages in thread

end of thread, other threads:[~2022-03-24  1:49 UTC | newest]

Thread overview: 49+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-12-31 16:13 [RFC PATCH v1 00/21] Modernize U-Boot shell Francis Laniel
2021-12-31 16:13 ` [RFC PATCH v1 01/21] test: Add framework to test hush behavior Francis Laniel
2022-01-08 14:53   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 02/21] test: hush: Test hush if/else Francis Laniel
2022-01-08 14:53   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 03/21] test/py: hush_if_test: Remove the test file Francis Laniel
2022-01-08 14:53   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 04/21] test: hush: Test hush variable expansion Francis Laniel
2022-01-08 14:53   ` Simon Glass
2022-02-06 18:22     ` Francis Laniel
2021-12-31 16:13 ` [RFC PATCH v1 05/21] test: hush: Test hush commands list Francis Laniel
2022-01-08 14:54   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 06/21] test: hush: Test hush loops Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 07/21] cli: Add Busybox upstream hush.c file Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 08/21] cli: Port Busybox 2021 hush to U-Boot Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 09/21] cli: Add choice for hush parser Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 10/21] cli: Add HUSH_2021_PARSER to hush parser choice Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 11/21] cli: Enables using hush 2021 parser as command line parser Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 12/21] cli: hush_2021: Enable variables expansion for hush 2021 Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 13/21] cli: hush_2021: Add functions to be called from run_command() Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 14/21] cli: Modify run_command() to add hush 2021 as parser Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 15/21] test: hush: Fix instructions list tests for hush 2021 Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 16/21] test: hush: Fix variable expansion " Francis Laniel
2022-01-12 20:03   ` Simon Glass
2022-02-06 18:23     ` Francis Laniel
2022-02-07 20:22       ` Simon Glass
2022-03-24  1:49         ` Francis Laniel
2021-12-31 16:13 ` [RFC PATCH v1 17/21] cli: hush_2021: Enable using \< and \> as string compare operators Francis Laniel
2022-01-12 20:03   ` Simon Glass
2022-02-06 18:23     ` Francis Laniel
2021-12-31 16:13 ` [RFC PATCH v1 18/21] cli: hush_2021: Enable if keyword Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 19/21] test: hush: Fix if tests for hush 2021 Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 20/21] cli: hush_2021: Enable loops Francis Laniel
2022-01-12 20:03   ` Simon Glass
2021-12-31 16:13 ` [RFC PATCH v1 21/21] test: hush: Fix loop tests for hush 2021 Francis Laniel
2022-01-12 20:03   ` Simon Glass
2022-01-31 22:15 ` [RFC PATCH v1 00/21] Modernize U-Boot shell Tom Rini

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.