Merge remote-tracking branch 'toybox/master' into HEAD

Change-Id: Id3b82ed12c8b6cfd39aae00fd45fdfcd3d0d3dea
diff --git a/android/device/generated/flags.h b/android/device/generated/flags.h
index 3b63c79..30ec3a4 100644
--- a/android/device/generated/flags.h
+++ b/android/device/generated/flags.h
@@ -823,6 +823,16 @@
 #undef FLAG_t
 #endif
 
+// export   np
+#undef OPTSTR_export
+#define OPTSTR_export "np"
+#ifdef CLEANUP_export
+#undef CLEANUP_export
+#undef FOR_export
+#undef FLAG_p
+#undef FLAG_n
+#endif
+
 // expr    
 #undef OPTSTR_expr
 #define OPTSTR_expr 0
@@ -4166,6 +4176,14 @@
 #define FLAG_t (1<<0)
 #endif
 
+#ifdef FOR_export
+#ifndef TT
+#define TT this.export
+#endif
+#define FLAG_p (FORCED_FLAG<<0)
+#define FLAG_n (FORCED_FLAG<<1)
+#endif
+
 #ifdef FOR_expr
 #ifndef TT
 #define TT this.expr
diff --git a/android/device/generated/globals.h b/android/device/generated/globals.h
index 00d6ed7..5789624 100644
--- a/android/device/generated/globals.h
+++ b/android/device/generated/globals.h
@@ -809,8 +809,6 @@
 struct rtcwake_data {
   long long t, s;
   char *m, *d;
-
-  struct rtc_wkalrm alarm;
 };
 
 // toys/pending/sh.c
@@ -827,10 +825,16 @@
 
   // keep lineno here, we use it to work around a compiler bug
   long lineno;
-  char **locals, *subshell_env, *ifs;
+  char *ifs;
   struct double_list functions;
-  unsigned options, jobcnt, loc_ro, loc_magic;
-  int hfd, pid;
+  unsigned options, jobcnt;
+  int hfd, pid, varlen, cdcount;
+  unsigned long long SECONDS;
+
+  struct sh_vars {
+    long flags;
+    char *str;
+  } *vars;
 
   // Running jobs for job control.
   struct sh_job {
diff --git a/android/device/generated/help.h b/android/device/generated/help.h
index 461559b..a8fe186 100644
--- a/android/device/generated/help.h
+++ b/android/device/generated/help.h
@@ -346,6 +346,8 @@
 
 #define HELP_stty "usage: stty [-ag] [-F device] SETTING...\n\nGet/set terminal configuration.\n\n-F	Open device instead of stdin\n-a	Show all current settings (default differences from \"sane\")\n-g	Show all current settings usable as input to stty\n\nSpecial characters (syntax ^c or undef): intr quit erase kill eof eol eol2\nswtch start stop susp rprnt werase lnext discard\n\nControl/input/output/local settings as shown by -a, '-' prefix to disable\n\nCombo settings: cooked/raw, evenp/oddp/parity, nl, ek, sane\n\nN	set input and output speed (ispeed N or ospeed N for just one)\ncols N	set number of columns\nrows N	set number of rows\nline N	set line discipline\nmin N	set minimum chars per read\ntime N	set read timeout\nspeed	show speed only\nsize	show size only"
 
+#define HELP_export "usage: export [-n] [NAME[=VALUE]...]\n\nMake variables available to child processes. NAME exports existing local\nvariable(s), NAME=VALUE sets and exports.\n\n-n	Unexport. Turn listed variable(s) into local variables.\n\nWith no arguments list exported variables/attributes as \"declare\" statements."
+
 #define HELP_exec "usage: exec [-cl] [-a NAME] COMMAND...\n\n-a	set argv[0] to NAME\n-c	clear environment\n-l	prepend - to argv[0]"
 
 #define HELP_eval "usage: eval COMMAND...\n\nExecute (combined) arguments as a shell command."
diff --git a/android/device/generated/newtoys.h b/android/device/generated/newtoys.h
index 276ca3a..695a26f 100644
--- a/android/device/generated/newtoys.h
+++ b/android/device/generated/newtoys.h
@@ -73,6 +73,7 @@
 USE_SH(NEWTOY(exec, "cla:", TOYFLAG_NOFORK))
 USE_SH(NEWTOY(exit, 0, TOYFLAG_NOFORK))
 USE_EXPAND(NEWTOY(expand, "t*", TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_LOCALE))
+USE_SH(NEWTOY(export, "np", TOYFLAG_NOFORK))
 USE_EXPR(NEWTOY(expr, NULL, TOYFLAG_USR|TOYFLAG_BIN))
 USE_FACTOR(NEWTOY(factor, 0, TOYFLAG_USR|TOYFLAG_BIN))
 USE_FALLOCATE(NEWTOY(fallocate, ">1l#|o#", TOYFLAG_USR|TOYFLAG_BIN))
diff --git a/android/linux/generated/flags.h b/android/linux/generated/flags.h
index e3486f8..1514d02 100644
--- a/android/linux/generated/flags.h
+++ b/android/linux/generated/flags.h
@@ -823,6 +823,16 @@
 #undef FLAG_t
 #endif
 
+// export   np
+#undef OPTSTR_export
+#define OPTSTR_export "np"
+#ifdef CLEANUP_export
+#undef CLEANUP_export
+#undef FOR_export
+#undef FLAG_p
+#undef FLAG_n
+#endif
+
 // expr    
 #undef OPTSTR_expr
 #define OPTSTR_expr 0
@@ -4166,6 +4176,14 @@
 #define FLAG_t (FORCED_FLAG<<0)
 #endif
 
+#ifdef FOR_export
+#ifndef TT
+#define TT this.export
+#endif
+#define FLAG_p (FORCED_FLAG<<0)
+#define FLAG_n (FORCED_FLAG<<1)
+#endif
+
 #ifdef FOR_expr
 #ifndef TT
 #define TT this.expr
diff --git a/android/linux/generated/globals.h b/android/linux/generated/globals.h
index 00d6ed7..5789624 100644
--- a/android/linux/generated/globals.h
+++ b/android/linux/generated/globals.h
@@ -809,8 +809,6 @@
 struct rtcwake_data {
   long long t, s;
   char *m, *d;
-
-  struct rtc_wkalrm alarm;
 };
 
 // toys/pending/sh.c
@@ -827,10 +825,16 @@
 
   // keep lineno here, we use it to work around a compiler bug
   long lineno;
-  char **locals, *subshell_env, *ifs;
+  char *ifs;
   struct double_list functions;
-  unsigned options, jobcnt, loc_ro, loc_magic;
-  int hfd, pid;
+  unsigned options, jobcnt;
+  int hfd, pid, varlen, cdcount;
+  unsigned long long SECONDS;
+
+  struct sh_vars {
+    long flags;
+    char *str;
+  } *vars;
 
   // Running jobs for job control.
   struct sh_job {
diff --git a/android/linux/generated/help.h b/android/linux/generated/help.h
index d5f21e6..4e2cdfa 100644
--- a/android/linux/generated/help.h
+++ b/android/linux/generated/help.h
@@ -348,6 +348,8 @@
 
 #define HELP_stty "usage: stty [-ag] [-F device] SETTING...\n\nGet/set terminal configuration.\n\n-F	Open device instead of stdin\n-a	Show all current settings (default differences from \"sane\")\n-g	Show all current settings usable as input to stty\n\nSpecial characters (syntax ^c or undef): intr quit erase kill eof eol eol2\nswtch start stop susp rprnt werase lnext discard\n\nControl/input/output/local settings as shown by -a, '-' prefix to disable\n\nCombo settings: cooked/raw, evenp/oddp/parity, nl, ek, sane\n\nN	set input and output speed (ispeed N or ospeed N for just one)\ncols N	set number of columns\nrows N	set number of rows\nline N	set line discipline\nmin N	set minimum chars per read\ntime N	set read timeout\nspeed	show speed only\nsize	show size only"
 
+#define HELP_export "usage: export [-n] [NAME[=VALUE]...]\n\nMake variables available to child processes. NAME exports existing local\nvariable(s), NAME=VALUE sets and exports.\n\n-n	Unexport. Turn listed variable(s) into local variables.\n\nWith no arguments list exported variables/attributes as \"declare\" statements."
+
 #define HELP_exec "usage: exec [-cl] [-a NAME] COMMAND...\n\n-a	set argv[0] to NAME\n-c	clear environment\n-l	prepend - to argv[0]"
 
 #define HELP_eval "usage: eval COMMAND...\n\nExecute (combined) arguments as a shell command."
diff --git a/android/linux/generated/newtoys.h b/android/linux/generated/newtoys.h
index 276ca3a..695a26f 100644
--- a/android/linux/generated/newtoys.h
+++ b/android/linux/generated/newtoys.h
@@ -73,6 +73,7 @@
 USE_SH(NEWTOY(exec, "cla:", TOYFLAG_NOFORK))
 USE_SH(NEWTOY(exit, 0, TOYFLAG_NOFORK))
 USE_EXPAND(NEWTOY(expand, "t*", TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_LOCALE))
+USE_SH(NEWTOY(export, "np", TOYFLAG_NOFORK))
 USE_EXPR(NEWTOY(expr, NULL, TOYFLAG_USR|TOYFLAG_BIN))
 USE_FACTOR(NEWTOY(factor, 0, TOYFLAG_USR|TOYFLAG_BIN))
 USE_FALLOCATE(NEWTOY(fallocate, ">1l#|o#", TOYFLAG_USR|TOYFLAG_BIN))
diff --git a/android/mac/generated/flags.h b/android/mac/generated/flags.h
index f30fb95..0bf7cd4 100644
--- a/android/mac/generated/flags.h
+++ b/android/mac/generated/flags.h
@@ -823,6 +823,16 @@
 #undef FLAG_t
 #endif
 
+// export   np
+#undef OPTSTR_export
+#define OPTSTR_export "np"
+#ifdef CLEANUP_export
+#undef CLEANUP_export
+#undef FOR_export
+#undef FLAG_p
+#undef FLAG_n
+#endif
+
 // expr    
 #undef OPTSTR_expr
 #define OPTSTR_expr 0
@@ -4166,6 +4176,14 @@
 #define FLAG_t (FORCED_FLAG<<0)
 #endif
 
+#ifdef FOR_export
+#ifndef TT
+#define TT this.export
+#endif
+#define FLAG_p (FORCED_FLAG<<0)
+#define FLAG_n (FORCED_FLAG<<1)
+#endif
+
 #ifdef FOR_expr
 #ifndef TT
 #define TT this.expr
diff --git a/android/mac/generated/globals.h b/android/mac/generated/globals.h
index 00d6ed7..5789624 100644
--- a/android/mac/generated/globals.h
+++ b/android/mac/generated/globals.h
@@ -809,8 +809,6 @@
 struct rtcwake_data {
   long long t, s;
   char *m, *d;
-
-  struct rtc_wkalrm alarm;
 };
 
 // toys/pending/sh.c
@@ -827,10 +825,16 @@
 
   // keep lineno here, we use it to work around a compiler bug
   long lineno;
-  char **locals, *subshell_env, *ifs;
+  char *ifs;
   struct double_list functions;
-  unsigned options, jobcnt, loc_ro, loc_magic;
-  int hfd, pid;
+  unsigned options, jobcnt;
+  int hfd, pid, varlen, cdcount;
+  unsigned long long SECONDS;
+
+  struct sh_vars {
+    long flags;
+    char *str;
+  } *vars;
 
   // Running jobs for job control.
   struct sh_job {
diff --git a/android/mac/generated/help.h b/android/mac/generated/help.h
index d5f21e6..4e2cdfa 100644
--- a/android/mac/generated/help.h
+++ b/android/mac/generated/help.h
@@ -348,6 +348,8 @@
 
 #define HELP_stty "usage: stty [-ag] [-F device] SETTING...\n\nGet/set terminal configuration.\n\n-F	Open device instead of stdin\n-a	Show all current settings (default differences from \"sane\")\n-g	Show all current settings usable as input to stty\n\nSpecial characters (syntax ^c or undef): intr quit erase kill eof eol eol2\nswtch start stop susp rprnt werase lnext discard\n\nControl/input/output/local settings as shown by -a, '-' prefix to disable\n\nCombo settings: cooked/raw, evenp/oddp/parity, nl, ek, sane\n\nN	set input and output speed (ispeed N or ospeed N for just one)\ncols N	set number of columns\nrows N	set number of rows\nline N	set line discipline\nmin N	set minimum chars per read\ntime N	set read timeout\nspeed	show speed only\nsize	show size only"
 
+#define HELP_export "usage: export [-n] [NAME[=VALUE]...]\n\nMake variables available to child processes. NAME exports existing local\nvariable(s), NAME=VALUE sets and exports.\n\n-n	Unexport. Turn listed variable(s) into local variables.\n\nWith no arguments list exported variables/attributes as \"declare\" statements."
+
 #define HELP_exec "usage: exec [-cl] [-a NAME] COMMAND...\n\n-a	set argv[0] to NAME\n-c	clear environment\n-l	prepend - to argv[0]"
 
 #define HELP_eval "usage: eval COMMAND...\n\nExecute (combined) arguments as a shell command."
diff --git a/android/mac/generated/newtoys.h b/android/mac/generated/newtoys.h
index 276ca3a..695a26f 100644
--- a/android/mac/generated/newtoys.h
+++ b/android/mac/generated/newtoys.h
@@ -73,6 +73,7 @@
 USE_SH(NEWTOY(exec, "cla:", TOYFLAG_NOFORK))
 USE_SH(NEWTOY(exit, 0, TOYFLAG_NOFORK))
 USE_EXPAND(NEWTOY(expand, "t*", TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_LOCALE))
+USE_SH(NEWTOY(export, "np", TOYFLAG_NOFORK))
 USE_EXPR(NEWTOY(expr, NULL, TOYFLAG_USR|TOYFLAG_BIN))
 USE_FACTOR(NEWTOY(factor, 0, TOYFLAG_USR|TOYFLAG_BIN))
 USE_FALLOCATE(NEWTOY(fallocate, ">1l#|o#", TOYFLAG_USR|TOYFLAG_BIN))
diff --git a/kconfig/macos_miniconfig b/kconfig/macos_miniconfig
index b9bf6a3..7166313 100644
--- a/kconfig/macos_miniconfig
+++ b/kconfig/macos_miniconfig
@@ -11,7 +11,6 @@
 CONFIG_CMP=y
 CONFIG_COMM=y
 CONFIG_CP=y
-CONFIG_CP_PRESERVE=y
 CONFIG_CPIO=y
 CONFIG_CUT=y
 CONFIG_DATE=y
diff --git a/lib/env.c b/lib/env.c
index 614a504..3017c40 100644
--- a/lib/env.c
+++ b/lib/env.c
@@ -34,18 +34,19 @@
 // Frees entries we set earlier. Use with libc getenv but not setenv/putenv.
 // if name has an equals and !val, act like putenv (name=val must be malloced!)
 // if !val unset name. (Name with = and val is an error)
-void xsetmyenv(int *envc, char ***env, char *name, char *val)
+// returns pointer to new name=value environment string, NULL if none
+char *xsetenv(char *name, char *val)
 {
   unsigned i, len, ec;
   char *new;
 
   // If we haven't snapshot initial environment state yet, do so now.
-  if (!*envc) {
+  if (!toys.envc) {
     // envc is size +1 so even if env empty it's nonzero after initialization
-    while ((*env)[(*envc)++]);
-    memcpy(new = xmalloc(((*envc|0xff)+1)*sizeof(char *)), *env,
-      *envc*sizeof(char *));
-    *env = (void *)new;
+    while (environ[toys.envc++]);
+    memcpy(new = xmalloc(((toys.envc|0xff)+1)*sizeof(char *)), environ,
+      toys.envc*sizeof(char *));
+    environ = (void *)new;
   }
 
   new = strchr(name, '=');
@@ -58,36 +59,31 @@
     if (val) new = xmprintf("%s=%s", name, val);
   }
 
-  ec = (*envc)-1;  // compensate for size +1 above
-  for (i = 0; (*env)[i]; i++) {
+  ec = toys.envc-1;  // compensate for size +1 above
+  for (i = 0; environ[i]; i++) {
     // Drop old entry, freeing as appropriate. Assumes no duplicates.
-    if (!memcmp(name, (*env)[i], len) && (*env)[i][len]=='=') {
-      if (i>=ec) free((*env)[i]);
+    if (!memcmp(name, environ[i], len) && environ[i][len]=='=') {
+      if (i>=ec) free(environ[i]);
       else {
         // move old entries down, add at end of old data
-        *envc = ec--;
-        for (; new ? i<ec : !!(*env)[i]; i++) (*env)[i] = (*env)[i+1];
+        toys.envc = ec--;
+        for (; new ? i<ec : !!environ[i]; i++) environ[i] = environ[i+1];
         i = ec;
       }
       break;
     }
   }
 
-  if (!new) return;
+  if (!new) return 0;
 
   // resize and null terminate if expanding
-  if (!(*env)[i]) {
+  if (!environ[i]) {
     len = i+1;
-    if (!(len&255)) *env = xrealloc(*env, (len+256)*sizeof(char *));
-    (*env)[len] = 0;
+    if (!(len&255)) environ = xrealloc(environ, (len+256)*sizeof(char *));
+    environ[len] = 0;
   }
-  (*env)[i] = new;
-}
 
-// xsetenv for normal environment (extern variables).
-void xsetenv(char *name, char *val)
-{
-  return xsetmyenv(&toys.envc, &environ, name, val);
+  return environ[i] = new;
 }
 
 void xunsetenv(char *name)
@@ -96,6 +92,27 @@
   xsetenv(name, 0);
 }
 
+// remove entry and return pointer instead of freeing
+char *xpop_env(char *name)
+{
+  int len, i;
+  char *s = 0;
+
+  for (len = 0; name[len] && name[len]!='='; len++);
+  for (i = 0; environ[i]; i++) {
+    if (!s && !strncmp(name, environ[i], len) && environ[i][len] == '=') {
+      s = environ[i];
+      if (toys.envc-1>i) {
+        s = xstrdup(s);
+        toys.envc--;
+      }
+    }
+    if (s) environ[i] = environ[i+1];
+  }
+
+  return s;
+}
+
 // reset environment for a user, optionally clearing most of it
 void reset_env(struct passwd *p, int clear)
 {
diff --git a/lib/lib.h b/lib/lib.h
index c528f19..3200dc3 100644
--- a/lib/lib.h
+++ b/lib/lib.h
@@ -269,7 +269,6 @@
 void do_lines(int fd, char delim, void (*call)(char **pline, long len));
 long long millitime(void);
 char *format_iso_time(char *buf, size_t len, struct timespec *ts);
-void reset_env(struct passwd *p, int clear);
 void loggit(int priority, char *format, ...);
 unsigned tar_cksum(void *data);
 int is_tar_header(void *pkt);
@@ -284,9 +283,11 @@
 // env.c
 
 long environ_bytes();
-void xsetenv(char *name, char *val);
+char *xsetenv(char *name, char *val);
 void xunsetenv(char *name);
+char *xpop_env(char *name); // because xpopenv() looks like xpopen_v()
 void xclearenv(void);
+void reset_env(struct passwd *p, int clear);
 
 // linestack.c
 
diff --git a/lib/portability.h b/lib/portability.h
index 4112895..acc32fd 100644
--- a/lib/portability.h
+++ b/lib/portability.h
@@ -175,7 +175,6 @@
 // Linux headers not listed by POSIX or LSB
 #include <sys/mount.h>
 #ifdef __linux__
-#include <linux/rtc.h>
 #include <sys/statfs.h>
 #include <sys/swap.h>
 #include <sys/sysinfo.h>
diff --git a/toys/other/hwclock.c b/toys/other/hwclock.c
index 5ab308d..3b3ff69 100644
--- a/toys/other/hwclock.c
+++ b/toys/other/hwclock.c
@@ -25,6 +25,7 @@
 
 #define FOR_hwclock
 #include "toys.h"
+#include <linux/rtc.h>
 
 GLOBALS(
   char *f;
diff --git a/toys/pending/rtcwake.c b/toys/pending/rtcwake.c
index 0929d51..61f76fb 100644
--- a/toys/pending/rtcwake.c
+++ b/toys/pending/rtcwake.c
@@ -33,20 +33,21 @@
 
 #define FOR_rtcwake
 #include "toys.h"
+#include <linux/rtc.h>
 
 GLOBALS(
   long long t, s;
   char *m, *d;
-
-  struct rtc_wkalrm alarm;
 )
 
 void rtcwake_main(void)
 {
-  time_t now, rtc_now, then;
+  struct rtc_wkalrm alarm;
   struct tm rtc_tm;
+  time_t now, rtc_now, then;
   int fd, utc;
 
+  memset(&alarm, 0, sizeof(alarm));
   if (FLAG(list_modes)) {
     xreadfile("/sys/power/state", toybuf, sizeof(toybuf));
     printf("off no on disable show %s", toybuf);
@@ -78,17 +79,17 @@
   }
 
   if (!strcmp(TT.m, "show")) { // Don't suspend, just show current alarm.
-    xioctl(fd, RTC_WKALM_RD, &TT.alarm);
-    if (!TT.alarm.enabled) xputs("alarm: off");
+    xioctl(fd, RTC_WKALM_RD, &alarm);
+    if (!alarm.enabled) xputs("alarm: off");
     else {
-      if ((then = mktime((void *)&TT.alarm.time)) < 0) perror_exit("mktime");
+      if ((then = mktime((void *)&alarm.time)) < 0) perror_exit("mktime");
       xprintf("alarm: on %s", ctime(&then));
     }
     goto done;
   } else if (!strcmp(TT.m, "disable")) { // Cancel current alarm.
-    xioctl(fd, RTC_WKALM_RD, &TT.alarm);
-    TT.alarm.enabled = 0;
-    xioctl(fd, RTC_WKALM_SET, &TT.alarm);
+    xioctl(fd, RTC_WKALM_RD, &alarm);
+    alarm.enabled = 0;
+    xioctl(fd, RTC_WKALM_SET, &alarm);
     goto done;
   }
 
@@ -100,11 +101,11 @@
   } else help_exit("-m %s needs -s or -t", TT.m);
   if (FLAG(v)) xprintf("Wake time:\t%lld / %s", (long long)then, ctime(&then));
 
-  if (!(utc ? gmtime_r : localtime_r)(&then, (void *)&TT.alarm.time))
+  if (!(utc ? gmtime_r : localtime_r)(&then, (void *)&alarm.time))
     error_exit(utc ? "gmtime_r failed" : "localtime_r failed");
 
-  TT.alarm.enabled = 1;
-  xioctl(fd, RTC_WKALM_SET, &TT.alarm);
+  alarm.enabled = 1;
+  xioctl(fd, RTC_WKALM_SET, &alarm);
   sync();
 
   xprintf("wakeup using \"%s\" from %s at %s", TT.m, TT.d, ctime(&then));
diff --git a/toys/pending/sh.c b/toys/pending/sh.c
index 0b2e85c..9b116a7 100644
--- a/toys/pending/sh.c
+++ b/toys/pending/sh.c
@@ -23,7 +23,6 @@
 
  * TODO: test that $PS1 color changes work without stupid \[ \] hack
  * TODO: Handle embedded NUL bytes in the command line? (When/how?)
- * TODO: replace getenv() with faster func: sort env and binary search
 
  * builtins: alias bg command fc fg getopts jobs newgrp read umask unalias wait
  *           disown umask suspend source pushd popd dirs logout times trap
@@ -48,10 +47,11 @@
  *   then until while { } time [[ ]]
 
 USE_SH(NEWTOY(cd, ">1LP[-LP]", TOYFLAG_NOFORK))
-USE_SH(NEWTOY(exit, 0, TOYFLAG_NOFORK))
-USE_SH(NEWTOY(unset, "fvn", TOYFLAG_NOFORK))
 USE_SH(NEWTOY(eval, 0, TOYFLAG_NOFORK))
 USE_SH(NEWTOY(exec, "cla:", TOYFLAG_NOFORK))
+USE_SH(NEWTOY(exit, 0, TOYFLAG_NOFORK))
+USE_SH(NEWTOY(export, "np", TOYFLAG_NOFORK))
+USE_SH(NEWTOY(unset, "fvn", TOYFLAG_NOFORK))
 
 USE_SH(NEWTOY(sh, "(noediting)(noprofile)(norc)sc:i", TOYFLAG_BIN))
 USE_SH(OLDTOY(toysh, sh, TOYFLAG_BIN))
@@ -126,6 +126,20 @@
     -a	set argv[0] to NAME
     -c	clear environment
     -l	prepend - to argv[0]
+
+config EXPORT
+  bool
+  default n
+  depends on SH
+  help
+    usage: export [-n] [NAME[=VALUE]...]
+
+    Make variables available to child processes. NAME exports existing local
+    variable(s), NAME=VALUE sets and exports.
+
+    -n	Unexport. Turn listed variable(s) into local variables.
+
+    With no arguments list exported variables/attributes as "declare" statements.
 */
 
 #define FOR_sh
@@ -143,10 +157,16 @@
 
   // keep lineno here, we use it to work around a compiler bug
   long lineno;
-  char **locals, *subshell_env, *ifs;
+  char *ifs;
   struct double_list functions;
-  unsigned options, jobcnt, loc_ro, loc_magic;
-  int hfd, pid;
+  unsigned options, jobcnt;
+  int hfd, pid, varlen, cdcount;
+  unsigned long long SECONDS;
+
+  struct sh_vars {
+    long flags;
+    char *str;
+  } *vars;
 
   // Running jobs for job control.
   struct sh_job {
@@ -197,6 +217,7 @@
 
 #define BUGBUG 0
 
+// call with NULL to just dump FDs
 static void dump_state(struct sh_function *sp)
 {
   struct sh_pipeline *pl;
@@ -205,7 +226,7 @@
   DIR *dir = fdopendir(fd);
   char buf[256];
 
-  if (sp->expect) {
+  if (sp && sp->expect) {
     struct double_list *dl;
 
     for (dl = sp->expect; dl; dl = (dl->next == sp->expect) ? 0 : dl->next)
@@ -215,7 +236,7 @@
         sp->pipeline->prev->here);
   }
 
-  for (pl = sp->pipeline; pl ; pl = (pl->next == sp->pipeline) ? 0 : pl->next) {
+  if (sp) for (pl = sp->pipeline; pl ; pl = (pl->next == sp->pipeline) ? 0 : pl->next) {
     for (i = 0; i<pl->arg->c; i++)
       dprintf(255, "arg[%d][%ld]=%s\n", q, i, pl->arg->v[i]);
     if (pl->arg->c<0) dprintf(255, "argc=%d\n", pl->arg->c);
@@ -275,64 +296,161 @@
   array_add(list, count, data);
 }
 
+// return length of valid variable name
+static char *varend(char *s)
+{
+  if (isdigit(*s)) return s;
+  while (*s>' ' && (*s=='_' || !ispunct(*s))) s++;
+
+  return s;
+}
+
 // Return index of variable within this list
-static unsigned findvar(char **list, char *name, int len)
+static struct sh_vars *findvar(char *name)
 {
-  unsigned i;
+  int len = varend(name)-name;
+  struct sh_vars *var = TT.vars+TT.varlen;
 
-  for (i = 0; list[i]; i++)
-    if (!strncmp(list[i], name, len) && list[i][len] == '=') break;
-
-  return i;
-}
-
-// Assign one variable
-// s: key=val
-// type: 0 = whatever it was before, local otherwise
-#define TAKE_MEM 0x80000000
-// declare -aAilnrux
-// ft
-static void setvar(char *s, unsigned type)
-{
-  unsigned uu;
-  int len = stridx(s, '=');
-
-  if (len == -1) return error_msg("no = in setvar %s\n", s);
-
-  if (type&TAKE_MEM) type ^= TAKE_MEM;
-  else s = xstrdup(s);
-
-  if (len == 3 && !memcmp(s, "IFS", 3)) TT.ifs = s+4;
-
-  // local, export, readonly, integer...
-
-  // exported variable?
-  if (environ && environ[uu = findvar(environ, s, len)]) {
-    if (uu>=toys.envc) free(environ[uu]);
-    environ[uu] = s;
-  } else if (TT.locals[uu = findvar(TT.locals, s, len)]) {
-    if (uu<TT.loc_ro) return error_msg("%.*s: readonly variable", len, s);
-    free(TT.locals[uu]);
-    TT.locals[uu] = s;
-  } else array_add(&TT.locals, uu, s);
-}
-
-// get variable of length len starting at s.
-static char *getvarbylen(char *s, int len)
-{
-  int i;
-
-  if (TT.locals && TT.locals[i = findvar(TT.locals, s, len)])
-    return TT.locals[i]+len+1;
-  if (environ && environ[i = findvar(environ, s, len)])
-    return environ[i]+len+1;
+  if (len) while (var-- != TT.vars) 
+    if (!strncmp(var->str, name, len) && var->str[len] == '=') return var;
 
   return 0;
 }
 
+// Append variable to TT.vars, returning *struct. Does not check duplicates.
+static struct sh_vars *addvar(char *s)
+{
+  if (!(TT.varlen&31))
+    TT.vars = xrealloc(TT.vars, (TT.varlen+32)*sizeof(*TT.vars));
+  TT.vars[TT.varlen].flags = 0;
+  TT.vars[TT.varlen].str = s;
+
+  return TT.vars+TT.varlen++;
+}
+
+// TODO function to resolve a string into a number for $((1+2)) etc
+long long do_math(char *s)
+{
+  return atoll(s);
+}
+
+// Assign one variable from malloced key=val string, returns var struct
+// TODO implement remaining types
+#define VAR_DICT      256
+#define VAR_ARRAY     128
+#define VAR_INT       64
+#define VAR_TOLOWER   32
+#define VAR_TOUPPER   16
+#define VAR_NAMEREF   8
+#define VAR_GLOBAL    4
+#define VAR_READONLY  2
+#define VAR_MAGIC     1
+
+// declare -aAilnrux
+// ft
+static struct sh_vars *setvar(char *s)
+{
+  int len = varend(s)-s;
+  long flags;
+  struct sh_vars *var;
+
+  if (s[len] != '=') {
+    error_msg("bad setvar %s\n", s);
+    free(s);
+    return 0;
+  }
+  if (len == 3 && !memcmp(s, "IFS", 3)) TT.ifs = s+4;
+
+  if (!(var = findvar(s))) return addvar(s);
+  flags = var->flags;
+
+  if (flags&VAR_READONLY) {
+    error_msg("%.*s: read only", len, s);
+    free(s);
+
+    return var;
+  } else if (flags&VAR_MAGIC) {
+    if (*s == 'S') TT.SECONDS = millitime() - 1000*do_math(s+len-1);
+    else if (*s == 'R') srandom(do_math(s+len-1));
+  } else if (flags&VAR_GLOBAL) xsetenv(var->str = s, 0);
+  else {
+    free(var->str);
+    var->str = s;
+  }
+// TODO if (flags&(VAR_TOUPPER|VAR_TOLOWER)) 
+// unicode _is stupid enough for upper/lower case to be different utf8 byte
+// lengths. example: lowercase of U+0130 (C4 B0) is U+0069 (69)
+// TODO VAR_INT
+// TODO VAR_ARRAY VAR_DICT
+
+  return var;
+}
+
+static struct sh_vars *setvarval(char *name, char *val)
+{
+  return setvar(xmprintf("%s=%s", name, val));
+}
+
+// get value of variable starting at s.
 static char *getvar(char *s)
 {
-  return getvarbylen(s, strlen(s));
+  struct sh_vars *var = findvar(s);
+
+  if (!var) return 0;
+
+  if (var->flags & VAR_MAGIC) {
+    char c = *var->str;
+
+    if (c == 'S') sprintf(toybuf, "%lld", (millitime()-TT.SECONDS)/1000);
+    else if (c == 'R') sprintf(toybuf, "%ld", random()&((1<<16)-1));
+    else if (c == 'L') sprintf(toybuf, "%ld", TT.lineno);
+    else if (c == 'G') sprintf(toybuf, "TODO: GROUPS");
+
+    return toybuf;
+  }
+
+  return varend(var->str)+1;
+}
+
+static void unsetvar(char *name)
+{
+  struct sh_vars *var = findvar(name);
+  int ii;
+
+  if (!var) return;
+  if (var->flags&VAR_GLOBAL) {
+    *varend(var->str) = 0;
+    xunsetenv(var->str);
+  } else free(var->str);
+
+  ii = var-TT.vars;
+  memmove(TT.vars+ii, TT.vars+ii+1, TT.varlen-ii);
+}
+
+// malloc declare -x "escaped string"
+static char *declarep(struct sh_vars *var)
+{
+  char *types = "-rgnuliaA", *in = types, flags[16], *out = flags, *ss;
+  int len;
+
+  while (*++in) if (var->flags&(1<<(in-types))) *out++ = *in;
+  if (in == types) *out++ = *types;
+  *out = 0;
+  len = out-flags;
+
+  for (in = types = varend(var->str); *in; in++) len += !!strchr("$\"\\`", *in);
+  len += in-types;
+  ss = xmalloc(len+13);
+
+  out = ss + sprintf(ss, "declare -%s \"", out);
+  while (types) {
+    if (strchr("$\"\\`", *in)) *out++ = '\\';
+    *out++ = *types++;
+  }
+  *out++ = '"';
+  *out = 0;
+ 
+  return ss; 
 }
 
 // return length of match found at this point (try is null terminated array)
@@ -359,8 +477,7 @@
   char *s = word;
 
   if (*s == '{') {
-    for (s++; isalnum(*s) || *s=='_'; s++);
-    if (*s == '}' && s != word+1) s++;
+    if (*(s = varend(s+1)) == '}' && s != word+1) s++;
     else s = word;
   } else while (isdigit(*s)) s++;
 
@@ -494,7 +611,7 @@
       return 1;
     }
   } else {
-dprintf(255, "%d schedule close %d\n", getpid(), to);
+if (BUGBUG) dprintf(255, "%d schedule close %d\n", getpid(), to);
     hfd = to;
     to = -1;
   }
@@ -509,12 +626,12 @@
 }
 
 // TODO: waitpid(WNOHANG) to clean up zombies and catch background& ending
-// TODO: xunsetenv() after vfork()?
 static void subshell_callback(void)
 {
-  TT.subshell_env = xmprintf("@%d,%d=", getpid(), getppid());
-  xsetenv(TT.subshell_env, 0);
-  TT.subshell_env[strlen(TT.subshell_env)-1] = 0;
+  char *s;
+
+  xsetenv(s = xmprintf("@%d,%d=", getpid(), getppid()), 0);
+  s[strlen(s)-1] = 0;
   xsetenv(xmprintf("$=%d", TT.pid), 0);
 // TODO: test $$ in (nommu)
 }
@@ -552,10 +669,22 @@
     // vfork child
     pid = xpopen_setup(0, 0, subshell_callback);
 
+    // free entries added to end of environment by callback (shared heap)
+    for (i = 0; environ[i]; i++) {
+      if (!ispunct(environ[i][0])) continue;
+      free(environ[i]);
+      environ[i] = 0;
+    }
+
     // marshall data to child
     close(254);
-    if (TT.locals)
-      for (i = 0; TT.locals[i]; i++) dprintf(pipes[1], "%s\n", TT.locals[i]);
+    for (i = 0; i<TT.varlen; i++) {
+      char *s;
+
+      if (TT.vars[i].flags&VAR_GLOBAL) continue;
+      dprintf(pipes[1], "%s\n", s = declarep(TT.vars+i));
+      free(s);
+    }
     dprintf(pipes[1], "%.*s\n", len, str);
     close(pipes[1]);
   }
@@ -680,7 +809,7 @@
   struct arg_list **delete)
 {
   char cc, qq = 0, *old = str, *new = str, *s, *ss, *ifs = 0, *del = 0;
-  int at = 0, ii = 0, dd, jj, kk, ll, oo;
+  int at = 0, ii = 0, dd, jj, kk, ll, oo = 0;
 
 if (BUGBUG) dprintf(255, "expand %s\n", str);
   if (flags&FORCE_KEEP) old = 0;
@@ -691,9 +820,8 @@
   if (!(flags&NO_TILDE) && *str == '~') {
     struct passwd *pw = 0;
 
-    // first expansion so don't need to free previous new
     ss = 0;
-    while (str[ii] && str[ii]!=':' && str[ii]!='/') s++;
+    while (str[ii] && str[ii]!=':' && str[ii]!='/') ii++;
     if (ii==1) {
       if (!(ss = getvar("HOME")) || !*ss) pw = bufgetpwuid(getuid());
     } else {
@@ -701,16 +829,21 @@
       pw = getpwnam(s = xstrndup(str+1, ii-1));
       free(s);
     }
-    if (pw && pw->pw_dir) ss = pw->pw_dir;
-    if (!ss || !*ss) ss = "/";
-    s = xmprintf("%s%s", ss, str+ii);
-    if (old != new) free(new);
-    new = s;
+    if (pw) {
+      ss = pw->pw_dir;
+      if (!ss || !*ss) ss = "/";
+    }
+    if (ss) {
+      oo = strlen(ss);
+      s = xmprintf("%s%s", ss, str+ii);
+      if (old != new) free(new);
+      new = s;
+    }
   }
 
   // parameter/variable expansion, and dequoting
 
-  for (oo = 0; (cc = str[ii++]); old!=new && (new[oo] = 0)) {
+  for (; (cc = str[ii++]); old!=new && (new[oo] = 0)) {
 
     // skip literal chars
     if (!strchr("$'`\\\"", cc)) {
@@ -807,11 +940,10 @@
 
       // $VARIABLE
       } else {
-        s = str+--ii;
-        for (jj = 0; s[jj] && (s[jj]=='_' || !ispunct(s[jj])); jj++);
-        if (!jj) new[oo++] = '$';
+        s = varend(ss = str+--ii);
+        if (!(jj = s-ss)) new[oo++] = '$';
 // TODO: $((a=42)) can change var, affect lifetime here
-        else ifs = getvarbylen(str+ii, jj);
+        else ifs = getvar(ss);
         ii += jj;
       }
     }
@@ -1132,9 +1264,10 @@
     to = *ss != '<';
     if (isdigit(*s)) to = atoi(s);
     else if (*s == '{') {
+      if (*varend(s+1) != '}') break;
       // when we close a filehandle, we _read_ from {var}, not write to it
       if ((!strcmp(ss, "<&") || !strcmp(ss, ">&")) && !strcmp(sss, "-")) {
-        if (!(ss = getvarbylen(s+1, ss-s-2))) break;
+        if (!(ss = getvar(s+1))) break;
         to = atoi(ss); // TODO trailing garbage?
         if (save_redirect(&pp->urd, -1, to)) break;
         close(to);
@@ -1144,7 +1277,7 @@
       } else {
         // we don't save this, it goes in the env var and user can close it.
         if (-1 == (to = next_hfd())) break;
-        cv = xmprintf("%.*s=%d", (int)(ss-s-1), s+1, to);
+        cv = xmprintf("%.*s=%d", (int)(ss-s-2), s+1, to);
       }
     }
 
@@ -1237,7 +1370,7 @@
     // Do we save displaced "to" in env variable instead of undo list?
     if (cv) {
       --*pp->urd;
-      setvar(cv, TAKE_MEM);
+      setvar(cv);
       cv = 0;
     }
     if ((saveclose&1) && save_redirect(&pp->urd, -1, from)) bad++;
@@ -1268,9 +1401,8 @@
 
   // Grab leading variable assignments
   for (envlen = 0; envlen<arg->c; envlen++) {
-    s = arg->v[envlen];
-    for (j=0; s[j] && (s[j]=='_' || !ispunct(s[j])); j++);
-    if (!j || s[j] != '=') break;
+    s = varend(arg->v[envlen]);
+    if (s == arg->v[envlen] || *s != '=') break;
   }
 
   // expand arguments and perform redirects
@@ -1281,7 +1413,8 @@
   if (envlen == arg->c) {
     for (j = 0; j<envlen; j++) {
       s = expand_one_arg(arg->v[j], NO_PATH|NO_SPLIT, 0);
-      setvar(s, TAKE_MEM*(s!=arg->v[j]));
+      if (s == arg->v[j]) s = xstrdup(s);
+      setvar(s);
     }
 
   // Do nothing if nothing to do
@@ -1320,16 +1453,21 @@
     if (toys.old_umask) umask(toys.old_umask);
     memcpy(&toys, &temp, j);
   } else {
-    char **env = 0, **old = environ, *ss, *sss;
-    int kk = 0, ll;
+    char **env = 0, **old = environ, *ss = 0, *sss;
+    int kk = 0, ll, mm = -1;
 
     // We don't allocate/free any array members, just the array
-    if (environ) while (environ[kk]) kk++;
+    if (environ) while (environ[kk]) {
+      if (strncmp(environ[kk], "SHLVL=", 6)) mm = kk;
+      kk++;
+    }
     if (kk) {
       env = xmalloc(sizeof(char *)*(kk+33));
       memcpy(env, environ, sizeof(char *)*(kk+1));
+      if (mm != -1) env[mm] = xmprintf("SHLVL=%d", atoi(env[mm]+6)+1);
       environ = env;
     }
+
     // assign leading environment variables
     for (j = 0; j<envlen; j++) {
       sss = expand_one_arg(arg->v[j], NO_PATH|NO_SPLIT, &pp->delete);
@@ -1341,15 +1479,13 @@
       }
       if (ll == kk) array_add(&environ, kk++, sss);
     }
-    ss = getvar("SHLVL");
-    sprintf(toybuf, "%d", atoi(ss ? ss : "")+1);
-    xsetenv("SHLVL", toybuf);
 
     if (-1 == (pp->pid = xpopen_both(pp->arg.v, 0)))
       perror_msg("%s: vfork", *pp->arg.v);
 
     // Restore environment variables
     environ = old;
+    if (mm != -1) free(env[mm]);
     free(env);
   }
 
@@ -2008,8 +2144,7 @@
           continue;
         } else if (!strncmp(blk->fvar, "((", 2)) {
 dprintf(2, "TODO skipped running for((;;)), need math parser\n");
-        } else setvar(xmprintf("%s=%s", blk->fvar, blk->farg.v[blk->loop++]),
-          TAKE_MEM);
+        } else setvarval(blk->fvar, blk->farg.v[blk->loop++]);
       }
 
     // end of block, may have trailing redirections and/or pipe
@@ -2131,71 +2266,113 @@
   writeall(2, toybuf, len);
 }
 
-// only set local variable when global not present
-static void setonlylocal(char ***to, char *name, char *val)
+// only set local variable when global not present, does not extend array
+static struct sh_vars *initlocal(char *name, char *val)
 {
-  if (getenv(name)) return;
-  *(*to)++ = xmprintf("%s=%s", name, val ? val : "");
+  return addvar(xmprintf("%s=%s", name, val ? val : ""));
+}
+
+// export malloced name=value string
+static void export(char *str)
+{
+  struct sh_vars *shv;
+  char *s;
+
+  // Make sure variable exists and is updated
+  if (strchr(str, '=')) shv = setvar(xstrdup(str));
+  else if (!findvar(str)) shv = addvar(str = xmprintf("%s=", str));
+  if (!shv || (shv->flags&VAR_GLOBAL)) return;
+
+  // Resolve local magic for export
+  if (shv->flags&VAR_MAGIC) {
+    s = shv->str;
+    shv->str = xmprintf("%.*s=%s", (int)(varend(str)-str), str, getvar(str));
+    free(s);
+  }
+
+  xsetenv(shv->str, 0);
+  shv->flags |= VAR_GLOBAL;
+}
+
+static void unexport(char *str)
+{
+  struct sh_vars *shv = findvar(str);
+
+  if (shv) {
+    if (shv->flags&VAR_GLOBAL) shv->str = xpop_env(str);
+    shv->flags &=~VAR_GLOBAL;
+  }
+  if (strchr(str, '=')) setvar(str);
 }
 
 // init locals, sanitize environment, handle nommu subshell handoff
 static void subshell_setup(void)
 {
   struct passwd *pw = getpwuid(getuid());
-  int to, from, pid = 0, ppid = 0, zpid = 0, mypid, myppid, len;
+  int ii, to, from, pid, ppid, zpid, myppid = getppid(), len;
 // TODO: you can unset readonly and these first 4 aren't malloc()
-  char *s, *ss, **ll, *locals[] = {"GROUPS=", "SECONDS=", "RANDOM=", "LINENO=",
-    xmprintf("PPID=%d", myppid = getppid()), xmprintf("EUID=%d", geteuid()),
-    xmprintf("$=%d", mypid = getpid()), xmprintf("UID=%d", getuid())};
+  char *s, *ss, *magic[] = {"SECONDS","RANDOM","LINENO","GROUPS"},
+    *readonly[] = {xmprintf("EUID=%d", geteuid()), xmprintf("UID=%d", getuid()),
+                   xmprintf("PPID=%d", myppid)};
   struct stat st;
   struct utsname uu;
   FILE *fp;
 
-  // Initialize read only local variables
-  TT.locals = xmalloc(32*sizeof(char *));
-  memcpy(TT.locals, locals, sizeof(locals));
-  ll = TT.locals+(TT.loc_ro = ARRAY_LEN(locals));
-  TT.loc_magic = 4;
+  // Initialize magic and read only local variables
+  srandom(TT.SECONDS = millitime());
+  for (ii = 0; ii<ARRAY_LEN(magic); ii++)
+    initlocal(magic[ii], "")->flags = VAR_MAGIC|(VAR_INT*('G'!=*magic[ii]));
+  for (ii = 0; ii<ARRAY_LEN(readonly); ii++)
+    addvar(readonly[ii])->flags = VAR_READONLY|VAR_INT;
 
   // Add local variables that can be overwritten
-  setonlylocal(&ll, "PATH", _PATH_DEFPATH);
+  initlocal("PATH", _PATH_DEFPATH);
   if (!pw) pw = (void *)toybuf; // first use, so still zeroed
-  setonlylocal(&ll, "HOME", *pw->pw_dir ? pw->pw_dir : "/");
-  setonlylocal(&ll, "SHELL", pw->pw_shell);
-  setonlylocal(&ll, "USER", pw->pw_name);
-  setonlylocal(&ll, "LOGNAME", pw->pw_name);
+  initlocal("HOME", *pw->pw_dir ? pw->pw_dir : "/");
+  initlocal("SHELL", pw->pw_shell);
+  initlocal("USER", pw->pw_name);
+  initlocal("LOGNAME", pw->pw_name);
   gethostname(toybuf, sizeof(toybuf)-1);
-  *ll++ = xmprintf("HOSTNAME=%s", toybuf);
+  initlocal("HOSTNAME", toybuf);
   uname(&uu);
-  setonlylocal(&ll, "HOSTTYPE", uu.machine);
+  initlocal("HOSTTYPE", uu.machine);
   sprintf(toybuf, "%s-unknown-linux", uu.machine);
-  setonlylocal(&ll, "MACHTYPE", toybuf);
-  setonlylocal(&ll, "OSTYPE", uu.sysname);
+  initlocal("MACHTYPE", toybuf);
+  initlocal("OSTYPE", uu.sysname);
   // sprintf(toybuf, "%s-toybox", TOYBOX_VERSION);
-  // setonlylocal(&ll, "BASH_VERSION", toybuf);
-  *ll++ = xstrdup("OPTERR=1");
-  *toybuf = 0;
+  // initlocal("BASH_VERSION", toybuf);
+  initlocal("OPTERR", "1"); // TODO: test if already exported?
   if (readlink0("/proc/self/exe", toybuf, sizeof(toybuf)))
-    setonlylocal(&ll, "BASH", toybuf);
-  *ll = 0;
+    initlocal("BASH", toybuf);
 
   // Ensure environ copied and toys.envc set, and clean out illegal entries
-  xunsetenv("");
   TT.ifs = " \t\n";
-  for (to = from = 0; (s = environ[from]); from++) {
+  for (to = from = pid = ppid = zpid = 0; (s = environ[from]); from++) {
 
     // If nommu subshell gets handoff
     if (!CFG_TOYBOX_FORK && !toys.stacktop) {
       len = 0;
       sscanf(s, "@%d,%d%n", &pid, &ppid, &len);
-      if (len && s[len]) pid = ppid = 0;
+      if (s[len]) pid = ppid = 0;
+      if (*s == '$' && s[1] == '=') zpid = atoi(s+2);
     }
 
-    // Filter out non-shell variable names
-    for (len = 0; s[len] && ((s[len] == '_') || !ispunct(s[len])); len++);
-    if (s[len] == '=') environ[to++] = environ[from];
+    // Filter out non-shell variable names from inherited environ.
+    // (haven't xsetenv() yet so no need to free() or adjust toys.envc)
+    ss = varend(s);
+    if (*ss == '=') {
+      struct sh_vars *shv = findvar(s);
+
+      if (!shv) addvar(environ[from])->flags = VAR_GLOBAL;
+      else if (shv->flags&VAR_READONLY) continue;
+      else {
+        shv->flags |= VAR_GLOBAL;
+        free(shv->str);
+        shv->str = s;
+      }
+      environ[to++] = s;
+    }
     if (!memcmp(s, "IFS=", 4)) TT.ifs = s+4;
-    if (!CFG_TOYBOX_FORK && *s == '$' && s[1] == '=') zpid = atoi(s+2);
   }
   environ[toys.optc = to] = 0;
 
@@ -2206,23 +2383,25 @@
   s = toys.argv[0];
   ss = 0;
   if (!strchr(s, '/')) {
-    if (!(ss = getcwd(0, 0))) {
-      if (*toybuf) s = toybuf;
-    } else {
+    if ((ss = getcwd(0, 0))) {
       s = xmprintf("%s/%s", ss, s);
       free(ss);
       ss = s;
-    }
+    } else if (*toybuf) s = toybuf;
   }
-  xsetenv("_", s);
+  s = xsetenv("_", s);
+  if (!findvar(s)) addvar(s)->flags = VAR_GLOBAL;
   free(ss);
-  if (!getvar("SHLVL")) xsetenv("SHLVL", "1");
+  if (!getvar("SHLVL")) {
+    s = xsetenv("SHLVL", "1");
+    if (!findvar(s)) addvar(s)->flags = VAR_GLOBAL;
+  }
 
 //TODO indexed array,associative array,integer,local,nameref,readonly,uppercase
 //          if (s+1<ss && strchr("aAilnru", *s)) {
 
   // sanity check: magic env variable, pipe status
-  if (CFG_TOYBOX_FORK || toys.stacktop || pid!=mypid || ppid!=myppid) return;
+  if (CFG_TOYBOX_FORK || toys.stacktop || pid!=getpid() || ppid!=myppid) return;
   if (fstat(254, &st) || !S_ISFIFO(st.st_mode)) error_exit(0);
   TT.pid = zpid;
   fcntl(254, F_SETFD, FD_CLOEXEC);
@@ -2248,6 +2427,7 @@
   signal(SIGPIPE, SIG_IGN);
 
   TT.pid = getpid();
+  TT.SECONDS = time(0);
   TT.arg = &arg;
   if (!(arg.c = toys.optc)) {
     arg.v = xmalloc(2*sizeof(char *));
@@ -2281,6 +2461,7 @@
 // TODO: syntax_err should exit from shell scripts
     if (!(f = fopen(*toys.optargs, "r"))) {
       char *pp = getvar("PATH");
+
       struct string_list *sl = find_in_path(pp?pp:_PATH_DEFPATH, *toys.optargs);
 
       for (;sl; free(llist_pop(&sl))) if ((f = fopen(sl->str, "r"))) break;
@@ -2293,7 +2474,7 @@
     // Prompt and read line
     TT.lineno++;
     if (ii && f == stdin) {
-      char *s = getenv(prompt ? "PS2" : "PS1");
+      char *s = getvar(prompt ? "PS2" : "PS1");
 
       if (!s) s = prompt ? "> " : (getpid() ? "\\$ " : "# ");
       do_prompt(s);
@@ -2346,7 +2527,7 @@
   if (*dd != '/') {
     to = 0;
     from = pwd ? pwd : (to = getcwd(0, 0));
-    if (!from) xsetenv("PWD", "(nowhere)");
+    if (!from) setvarval("PWD", "(nowhere)");
     else {
       from = xmprintf("%s/%s", from, dd);
       free(dd);
@@ -2385,8 +2566,18 @@
 
   if (bad || chdir(dd)) perror_msg("chdir '%s'", dd);
   else {
-    if (pwd) xsetenv("OLDPWD", pwd);
-    xsetenv("PWD", dd);
+    if (pwd) {
+      setvarval("OLDPWD", pwd);
+      if (TT.cdcount == 1) {
+        export("OLDPWD");
+        TT.cdcount++;
+      }
+    }
+    setvarval("PWD", dd);
+    if (!TT.cdcount) {
+      export("PWD");
+      TT.cdcount++;
+    }
   }
   free(dd);
 }
@@ -2398,30 +2589,45 @@
 
 void unset_main(void)
 {
-  char **arg;
-  unsigned vv, xx;
+  char **arg, *s;
 
   for (arg = toys.optargs; *arg; arg++) {
-    if (!strcmp(*arg, "IFS")) TT.ifs = " \t\n";
-    if (strchr(*arg, '=')) error_msg("bad '%s'", *arg);
-    else {
-
-      // find and unset local
-      vv = findvar(TT.locals, *arg, strlen(*arg));
-      if (vv<TT.loc_ro || vv<TT.loc_magic) error_msg("nope"); // TODO this
-      else {
-        for (xx = vv; TT.locals[xx]; xx++);
-        if (xx) {
-          free(TT.locals[vv]);
-          memmove(TT.locals+vv, TT.locals+xx+1, xx);
-
-          continue;
-        }
-      }
-
-      // unset global
-      xsetenv(*arg, 0);
+    s = varend(*arg);
+    if (s == *arg || *s) {
+      error_msg("bad '%s'", *arg);
+      continue;
     }
+
+    // unset magic variable?
+    if (!strcmp(*arg, "IFS")) TT.ifs = " \t\n";
+    unsetvar(*arg);
+  }
+}
+
+#define CLEANUP_cd
+#define FOR_export
+#include "generated/flags.h"
+
+void export_main(void)
+{
+  char **arg, *eq;
+
+  // list existing variables?
+  if (!toys.optc) {
+    for (arg = environ; *arg; arg++) xprintf("declare -x %s\n", *arg);
+    return;
+  }
+
+  // set/move variables
+  for (arg = toys.optargs; *arg; arg++) {
+    eq = varend(*arg);
+    if (eq == *arg || (*eq && *eq != '=')) {
+      error_msg("bad %s", *arg);
+      continue;
+    }
+
+    if (FLAG(n)) unexport(*arg);
+    else export(*arg);
   }
 }
 
@@ -2434,7 +2640,7 @@
   free(s);
 }
 
-#define CLEANUP_cd
+#define CLEANUP_export
 #define FOR_exec
 #include "generated/flags.h"