Improved subshell support (was: How to add subshell support for ash)



Hi!

I discovered that there is a Trac ticket system on your web site, but
somehow my provider's mail server seems to filter out registration
e-mails, maybe because of a non-existent noreply@... sender address.

Maybe someone can activate my e-mail address (user name kriegaex)
manually? Anyway, for the time being I will keep posting here in this
thread instead of creating a ticket with attachments in Trac:

I think I have resolved the remaining issues with differentiating
between BusyBox ash and Debian ash (dash). I also refactored the
subshell code and documentation a bit for all subshell types. I
installed and tested *all* of them on my Ubuntu 11.10 64-bit desktop
system plus Bash and BusyBox ash on my embedded mipsel platform.

I think now I have reached a mature enough code state for you to commit
it upstream because everything which worked before still does and
several things which were buggy or unavailable have been fixed/added.

The patches currently contain the following changes:

030-bash_inputrc.patch

  - Fix non-functional INPUTRC for bash (variable was unset and never
    used)

040-ash_as_subshell.patch

  - Fix chdir for fish: communication with subshell was not working and
    showing an error message because "pwd" returns abbreviated home
    directory "~" instead of full path name. So now we use "$PWD" and
    it works. This must have been broken for a while. This is unrelated
    to ash, but I included it in the refactoring patch for
    src/subshell.c anyway because it is small and the same code section
    was also changed for other reasons (set fish prompt).

  - All subshells now have a dynamic, meaningful prompt user@host:cwd.
    This was tricky for ash/dash and a little easier for the other
    shells.

  - Shell type recognition for automatic subshell selection has been
    improved: Now not only the SHELL names are checked, but also their
    link targets in order to determine the correct type (e.g. if a ksh
    is really a zsh or if an ash is BusyBox or dash, if sh is whichever
    type or if bash is symlinked to BusyBox (but BB still is just an
    ash, never a bash). Even if the type could not be determined or a
    compatible subshell not found via inspection of SHELL and
    /etc/passwd, function OS_Setup in main.c now searches for known
    shells by their usual paths. For instance, if you have an exotic
    login shell called foosh, you can now have subshell support anyway,
    if MC finds an ash, bash etc. in the system.

  - I have added many more comments to the code sections I changed.
    Maybe some of you think there are too many now, but I think this
    tricky part of the code base should be documented rather too much
    than not wnough, so as to make it easiert for maintainers to
    upgrade subshell support or add new subshell types in the future.
    Sometimes it is not enough to have working code, but helpful to
    understand *why* the code was written in a certain way.

040-ash_as_subshell_additional.patch

  - ashrc init file added to config file list

  - Subshell documentation page updated with a few corrections, better
    formatting and some new information (doc/man/mc.1.in), e.g. how to
    use init files for bash and ash, how to manually call mc with SHELL
    variable set to another shell type if you want to have a specific
    subshell type.

BTW, the patches have been created for mc-4.8.1, *not* for the
trunk/master. But they should be easy to merge into the master.

All in all I believe that adding these patches to MC will significantly
improve subshell support, fixing two old issues and adding new subshell
types often used on servers or embedded systems. That could make MC even
more popular. E.g. I have heard from several people that they would like
to get rid feature-rich, but big shells like bash, zsh or tcsh on such
systems if they had busybox or dash subshell support. I think MC is far
from dead and did what I could with my humble means to improve a small
part of it.


Open issues (legacy, not created by my patches):

  - Still open is the old issue of fish not reliably showing its prompt
    in the bottom line of mc. With my limited knowledge I could not
    find out why this happens, but I noticed an interesting fact: If I
    start mc in an strace command line (I did this because I was
    debugging/logging something during development), all of a sudden
    the fish prompt is shown reliably on my test machine. Maybe strace
    was slowing down something, so this might be a timing issue,
    possibly a race condition, but this is just a guess. It might be a
    starting point for further investigation by one of you guys.

  - Another (old) open issue which still exists is that if you cd to
    another directory in full-screen mode (hide panels with Ctrl-O),
    then show the panels again with Ctro-O, the new path is not
    immediately shown on the bottom line, but only after some user
    interaction such as pressing Tab twice, switching back and forth
    between left/right panel.

--
Alexander Kriegisch (kriegaex)
http://freetz.org
--- src/subshell.c	2012-03-02 13:55:52.018954847 +0100
+++ src/subshell.c	2012-03-05 13:59:36.697459979 +0100
@@ -266,11 +266,14 @@
         putenv (g_strdup (sid_str));
     }
 
+    char *putenv_str = NULL;
     switch (subshell_type)
     {
     case BASH:
+        /* Do we have a custom init file ~/.local/share/mc/bashrc? */
         init_file = mc_config_get_full_path ("bashrc");
 
+        /* Otherwise use ~/.bashrc */
         if (access (init_file, R_OK) == -1)
         {
             g_free (init_file);
@@ -285,9 +288,9 @@
             char *input_file = mc_config_get_full_path ("inputrc");
             if (access (input_file, R_OK) == 0)
             {
-                char *putenv_str = g_strconcat ("INPUTRC=", input_file, NULL);
+                putenv_str = g_strconcat ("INPUTRC=", input_file, NULL);
                 putenv (putenv_str);
-                g_free (putenv_str);
+                /* Do not use "g_free (putenv_str)" here, otherwise INPUTRC will be undefined! */
             }
             g_free (input_file);
         }
@@ -350,6 +353,7 @@
 
     /* If we get this far, everything failed miserably */
     g_free (init_file);
+    g_free (putenv_str);
     _exit (FORK_FAILURE);
 }
 
--- src/main.h	2012-03-06 02:32:24.493495264 +0100
+++ src/main.h	2012-03-06 02:24:03.000000000 +0100
@@ -72,6 +72,7 @@
 #endif /* !HAVE_CHARSET */
 
 extern char *shell;
+extern char *shell_realpath;
 extern const char *mc_prompt;
 
 /* index to record_macro_buf[], -1 if not recording a macro */
--- src/main.c	2012-03-06 02:32:24.493495264 +0100
+++ src/main.c	2012-03-06 23:33:24.000000000 +0100
@@ -101,6 +101,8 @@
 
 /* The user's shell */
 char *shell = NULL;
+char rp_shell[PATH_MAX];
+char *shell_realpath = NULL;
 
 /* The prompt */
 const char *mc_prompt = NULL;
@@ -165,19 +167,44 @@
 
     if ((shell_env == NULL) || (shell_env[0] == '\0'))
     {
+        /* 2nd choice: user login shell */
         struct passwd *pwd;
         pwd = getpwuid (geteuid ());
         if (pwd != NULL)
             shell = g_strdup (pwd->pw_shell);
     }
     else
+        /* 1st choice: SHELL environment variable */
         shell = g_strdup (shell_env);
 
     if ((shell == NULL) || (shell[0] == '\0'))
     {
         g_free (shell);
-        shell = g_strdup ("/bin/sh");
+        /* 3rd choice: look for existing shells supported as MC subshells.  */
+        if (access("/bin/bash", X_OK) == 0)
+            shell = g_strdup ("/bin/bash");
+        else if (access("/bin/ash", X_OK) == 0)
+            shell = g_strdup ("/bin/ash");
+        else if (access("/bin/dash", X_OK) == 0)
+            shell = g_strdup ("/bin/dash");
+        else if (access("/bin/busybox", X_OK) == 0)
+            shell = g_strdup ("/bin/busybox");
+        else if (access("/bin/zsh", X_OK) == 0)
+            shell = g_strdup ("/bin/zsh");
+        else if (access("/bin/tcsh", X_OK) == 0)
+            shell = g_strdup ("/bin/tcsh");
+        /* No fish as fallback because it is so much different from other shells and
+         * in a way exotic (even though user-friendly by name) that we should not
+         * present it as a subshell without the user's explicit intention. We rather
+         * will not use a subshell but just a command line.
+         * else if (access("/bin/fish", X_OK) == 0)
+         *     shell = g_strdup ("/bin/fish");
+         */
+        else
+            /* Fallback and last resort: system default shell */
+            shell = g_strdup ("/bin/sh");
     }
+    shell_realpath = mc_realpath (shell, rp_shell);
 }
 
 /* --------------------------------------------------------------------------------------------- */
--- src/subshell.c	2012-03-05 13:59:36.697459979 +0100
+++ src/subshell.c	2012-03-06 23:37:48.000000000 +0100
@@ -126,6 +126,8 @@
 static enum
 {
     BASH,
+    ASH_BUSYBOX,    /* BusyBox default shell (ash) */
+    DASH,           /* Debian variant of ash */
     TCSH,
     ZSH,
     FISH
@@ -280,8 +282,9 @@
             init_file = g_strdup (".bashrc");
         }
 
-        /* Make MC's special commands not show up in bash's history */
-        putenv ((char *) "HISTCONTROL=ignorespace");
+        /* Make MC's special commands not show up in bash's history and also suppress
+         * consecutive identical commands*/
+        putenv ((char *) "HISTCONTROL=ignoreboth");
 
         /* Allow alternative readline settings for MC */
         {
@@ -297,7 +300,26 @@
 
         break;
 
-        /* TODO: Find a way to pass initfile to TCSH and ZSH */
+    case ASH_BUSYBOX:
+    case DASH:
+        /* Do we have a custom init file ~/.local/share/mc/ashrc? */
+        init_file = mc_config_get_full_path ("ashrc");
+
+        /* Otherwise use ~/.profile */
+        if (access (init_file, R_OK) == -1)
+        {
+            g_free (init_file);
+            init_file = g_strdup (".profile");
+        }
+
+        /* Put init file to ENV variable used by ash */
+        putenv_str = g_strconcat ("ENV=", init_file, NULL);
+        putenv (putenv_str);
+        /* Do not use "g_free (putenv_str)" here, otherwise ENV will be undefined! */
+
+        break;
+
+        /* TODO: Find a way to pass initfile to TCSH, ZSH and FISH */
     case TCSH:
     case ZSH:
     case FISH:
@@ -335,19 +357,17 @@
         execl (shell, "bash", "-rcfile", init_file, (char *) NULL);
         break;
 
-    case TCSH:
-        execl (shell, "tcsh", (char *) NULL);
-        break;
-
     case ZSH:
         /* Use -g to exclude cmds beginning with space from history
          * and -Z to use the line editor on non-interactive term */
         execl (shell, "zsh", "-Z", "-g", (char *) NULL);
-
         break;
 
+    case ASH_BUSYBOX:
+    case DASH:
+    case TCSH:
     case FISH:
-        execl (shell, "fish", (char *) NULL);
+        execl (shell, shell, (char *) NULL);
         break;
     }
 
@@ -769,7 +789,8 @@
 {
     /* This must be remembered across calls to init_subshell() */
     static char pty_name[BUF_SMALL];
-    char precmd[BUF_SMALL];
+    /* Must be considerably longer than BUF_SMALL (128) to support fancy shell prompts */
+    char precmd[300];
 
     switch (check_sid ())
     {
@@ -785,21 +806,36 @@
     /* Take the current (hopefully pristine) tty mode and make */
     /* a raw mode based on it now, before we do anything else with it */
     init_raw_mode ();
-
     if (mc_global.tty.subshell_pty == 0)
     {                           /* First time through */
-        /* Find out what type of shell we have */
+        /* Find out what type of shell we have. Also consider real paths (resolved symlinks)
+         * because e.g. csh might point to tcsh, ash to dash or busybox, sh to anything. */
 
-        if (strstr (shell, "/zsh") || getenv ("ZSH_VERSION"))
+        if (strstr (shell, "/zsh") || strstr (shell_realpath, "/zsh") || getenv ("ZSH_VERSION"))
+            /* Also detects ksh symlinked to zsh */
             subshell_type = ZSH;
-        else if (strstr (shell, "/tcsh"))
-            subshell_type = TCSH;
-        else if (strstr (shell, "/csh"))
+        else if (strstr (shell, "/tcsh") || strstr (shell_realpath, "/tcsh"))
+            /* Also detects csh symlinked to tcsh */
             subshell_type = TCSH;
+        else if (strstr (shell, "/fish") || strstr (shell_realpath, "/fish"))
+            subshell_type = FISH;
+        else if (strstr (shell, "/dash") || strstr (shell_realpath, "/dash"))
+            /* Debian ash (also found if symlinked to by ash/sh) */
+            subshell_type = DASH;
+        else if (strstr (shell_realpath, "/busybox"))
+        {
+            /* If shell is symlinked to busybox, assume it is an ash, even though theoretically
+             * it could also be a hush (a mini shell for non-MMU systems deactivated by default).
+             * For simplicity's sake we assume that busybox always contains an ash, not a hush.
+             * On embedded platforms or on server systems, /bin/sh often points to busybox.
+             * Sometimes even bash is symlinked to busybox (CONFIG_FEATURE_BASH_IS_ASH option),
+             * so we need to check busybox symlinks *before* checking for the name "bash"
+             * in order to avoid that case. */
+            subshell_type = ASH_BUSYBOX;
+        }
         else if (strstr (shell, "/bash") || getenv ("BASH"))
+            /* If bash is not symlinked to busybox, it is safe to assume it is a real bash */
             subshell_type = BASH;
-        else if (strstr (shell, "/fish"))
-            subshell_type = FISH;
         else
         {
             mc_global.tty.use_subshell = FALSE;
@@ -850,7 +886,7 @@
                 return;
             }
         }
-        else /* subshell_type is BASH or ZSH */ if (pipe (subshell_pipe))
+        else /* subshell_type is BASH, ASH_BUSYBOX, DASH or ZSH */ if (pipe (subshell_pipe))
         {
             perror (__FILE__ ": couldn't create pipe");
             mc_global.tty.use_subshell = FALSE;
@@ -878,29 +914,113 @@
         init_subshell_child (pty_name);
     }
 
-    /* Set up `precmd' or equivalent for reading the subshell's CWD */
+    /* Set up `precmd' or equivalent for reading the subshell's CWD
+     *
+     * Attention! Never forget that these are *one-liners* even though the concatenated
+     * substrings contain line breaks and indentation for better understanding of the
+     * shell code. It is vital that each one-liner ends with a line feed character ("\n" ).
+     */
 
     switch (subshell_type)
     {
     case BASH:
         g_snprintf (precmd, sizeof (precmd),
-                    " PROMPT_COMMAND='pwd>&%d;kill -STOP $$'\n", subshell_pipe[WRITE]);
+            " PROMPT_COMMAND='pwd>&%d; kill -STOP $$'; "
+            "PS1='\\u@\\h:\\w\\$ '\n",
+            subshell_pipe[WRITE]);
+        break;
+
+    case ASH_BUSYBOX:
+        /* BusyBox ash needs a somewhat complicated precmd emulation via PS1, and it is vital
+         * that BB be built with active CONFIG_ASH_EXPAND_PRMT, but this is the default anyway.
+         *
+         * A: This leads to a stopped subshell (=frozen mc) if user calls "ash" command
+         *    "PS1='$(pwd>&%d; kill -STOP $$)\\u@\\h:\\w\\$ '\n",
+         *
+         * B: This leads to "sh: precmd: not found" in sub-subshell if user calls "ash" command
+         *    "precmd() { pwd>&%d; kill -STOP $$; }; "
+         *    "PS1='$(precmd)\\u@\\h:\\w\\$ '\n",
+         *
+         * C: This works if user calls "ash" command because in sub-subshell
+         *    PRECMD is unfedined, thus evaluated to empty string - no damage done.
+         *    Attention: BusyBox must be built with FEATURE_EDITING_FANCY_PROMPT to
+         *    permit \u, \w, \h, \$ escape sequences. Unfortunately this cannot be guaranteed,
+         *    especially on embedded systems where people try to save space, so let's use
+         *    the dash version below. It should work on virtually all systems.
+         *    "precmd() { pwd>&%d; kill -STOP $$; }; "
+         *    "PRECMD=precmd; "
+         *    "PS1='$(eval $PRECMD)\\u@\\h:\\w\\$ '\n",
+         */
+    case DASH:
+        /* Debian ash needs a precmd emulation via PS1, similar to BusyBox ash,
+         * but does not support escape sequences for user, host and cwd in prompt.
+         * Attention! Make sure that the buffer for precmd is big enough.
+         *
+         * We want to have a fancy dynamic prompt with user@host:cwd just like in the BusyBox
+         * examples above, but because replacing the home directory part of the path by "~" is
+         * complicated, it bloats the precmd to a size > BUF_SMALL (128).
+         *
+         * The following example is a little less fancy (home directory not replaced)
+         * and shows the basic workings of our prompt for easier understanding:
+         *
+         * "precmd() { "
+         *     "echo \"$USER@$(hostname -s):$PWD\"; "
+         *     "pwd>&%d; "
+         *     "kill -STOP $$; "
+         * "}; "
+         * "PRECMD=precmd; "
+         * "PS1='$($PRECMD)$ '\n",
+         */
+        g_snprintf (precmd, sizeof (precmd),
+            "precmd() { "
+                "if [ ! \"${PWD##$HOME}\" ]; then "
+                    "MC_PWD=\"~\"; "
+                "else "
+                    "[ \"${PWD##$HOME/}\" = \"$PWD\" ] && MC_PWD=\"$PWD\" || MC_PWD=\"~/${PWD##$HOME/}\"; "
+                "fi; "
+                "echo \"$USER@$(hostname -s):$MC_PWD\"; "
+                "pwd>&%d; "
+                "kill -STOP $$; "
+            "}; "
+            "PRECMD=precmd; "
+            "PS1='$($PRECMD)$ '\n",
+            subshell_pipe[WRITE]);
         break;
 
     case ZSH:
         g_snprintf (precmd, sizeof (precmd),
-                    " precmd(){ pwd>&%d;kill -STOP $$ }\n", subshell_pipe[WRITE]);
+            " precmd() { pwd>&%d; kill -STOP $$; }; "
+            "PS1='%%n@%%m:%%~%%# '\n",
+            subshell_pipe[WRITE]);
         break;
 
     case TCSH:
         g_snprintf (precmd, sizeof (precmd),
-                    "set echo_style=both;"
-                    "alias precmd 'echo $cwd:q >>%s;kill -STOP $$'\n", tcsh_fifo);
+            "set echo_style=both; "
+            "set prompt='%%n@%%m:%%~%%# '; "
+            "alias precmd 'echo $cwd:q >>%s; kill -STOP $$'\n",
+            tcsh_fifo);
         break;
+
     case FISH:
+        /* We also want a fancy user@host:cwd prompt here, but fish makes it very easy to also
+         * use colours, which is what we will do. But first here is a simpler, uncoloured version:
+         * "function fish_prompt; "
+         *     "echo (whoami)@(hostname -s):(pwd)\\$\\ ; "
+         *     "echo \"$PWD\">&%d; "
+         *     "kill -STOP %%self; "
+         * "end\n",
+         *
+         * TODO: fish prompt is shown when panel is hidden (Ctrl-O), but not when it is visible.
+         * Find out how to fix this.
+         */
         g_snprintf (precmd, sizeof (precmd),
-                    "function fish_prompt ; pwd>&%d;kill -STOP %%self; end\n",
-                    subshell_pipe[WRITE]);
+             "function fish_prompt; "
+                 "echo (whoami)@(hostname -s):(set_color $fish_color_cwd)(pwd)(set_color normal)\\$\\ ; "
+                 "echo \"$PWD\">&%d; "
+                 "kill -STOP %%self; "
+             "end\n",
+            subshell_pipe[WRITE]);
         break;
 
     }
@@ -1107,6 +1227,13 @@
         quote_cmd_start = "(printf \"%b\" '";
         quote_cmd_end = "')";
     }
+    /* TODO: When BusyBox printf is fixed, get rid of this "else if", see
+       http://lists.busybox.net/pipermail/busybox/2012-March/077460.html */
+    else if (subshell_type == ASH_BUSYBOX)
+    {
+        quote_cmd_start = "\"`echo -en '";
+        quote_cmd_end = "'`\"";
+    }
     else
     {
         quote_cmd_start = "\"`printf \"%b\" '";
--- lib/mcconfig/paths.c	2012-03-04 04:28:07.000000000 +0100
+++ lib/mcconfig/paths.c	2012-03-04 04:28:43.000000000 +0100
@@ -82,6 +82,7 @@
     /* data */
     { "skins",                                 &mc_data_str, MC_SKINS_SUBDIR},
     { "fish",                                  &mc_data_str, FISH_PREFIX},
+    { "ashrc",                                 &mc_data_str, "ashrc"},
     { "bashrc",                                &mc_data_str, "bashrc"},
     { "inputrc",                               &mc_data_str, "inputrc"},
     { "extfs.d",                               &mc_data_str, MC_EXTFS_DIR},
--- tests/lib/mcconfig/user_configs_path.c	2012-03-04 04:27:47.000000000 +0100
+++ tests/lib/mcconfig/user_configs_path.c	2012-03-04 05:33:48.418447747 +0100
@@ -96,6 +96,7 @@
 
     path_fail_unless (CONF_DATA, MC_SKINS_SUBDIR);
     path_fail_unless (CONF_DATA, FISH_PREFIX);
+    path_fail_unless (CONF_DATA, "ashrc");
     path_fail_unless (CONF_DATA, "bashrc");
     path_fail_unless (CONF_DATA, "inputrc");
     path_fail_unless (CONF_DATA, MC_EXTFS_DIR);
--- doc/man/mc.1.in	2012-03-04 05:18:35.970419532 +0100
+++ doc/man/mc.1.in	2012-03-06 18:49:07.000000000 +0100
@@ -2408,7 +2408,7 @@
 .\"NODE "  The subshell support"
 .SH "  The subshell support"
 The subshell support is a compile time option, that works with the
-shells: bash, tcsh and zsh.
+shells: bash, ash (BusyBox and Debian), tcsh, zsh and fish.
 .PP
 When the subshell code is activated the Midnight Commander will
 spawn a concurrent copy of your shell (the one defined in the
@@ -2420,28 +2420,34 @@
 environment variables, use shell functions and define aliases that are
 valid until you quit the Midnight Commander.
 .PP
-If you are using
 .B bash
-you can specify startup
-commands for the subshell in your ~/.local/share/mc/bashrc file and
-special keyboard maps in the ~/.local/share/mc/inputrc file.
-.B tcsh
-users may specify startup commands in the ~/.local/share/mc/tcshrc file.
+users may specify startup commands in ~/.local/share/mc/bashrc (fallback ~/.bashrc)
+and special keyboard maps in ~/.local/share/mc/inputrc (fallback ~/.inputrc).
+.PP
+.B ash/dash
+users (BusyBox or Debian) may specify startup commands in ~/.local/share/mc/ashrc (fallback ~/.profile).
+.PP
+.B tcsh, zsh, fish
+users cannot specify mc-specific startup commands at present. They have to rely on
+shell-specific startup files.
 .PP
 When the subshell code is used, you can suspend applications at any
 time with the sequence C\-o and jump back to the Midnight Commander, if
 you interrupt an application, you will not be able to run other
 external commands until you quit the application you interrupted.
 .PP
-An extra added feature of using the subshell is that the prompt
-displayed by the Midnight Commander is the same prompt that you are
-currently using in your shell.
+A special subshell feature is that Midnight Commander displays a dynamic prompt
+like "user@host:current_path> " (with known problems for fish which displays the prompt in
+full-screen mode (Ctrl-o), but not when the MC panels are visible).
 .PP
 The
 .\"LINK2"
 OPTIONS
 .\"OPTIONS"
-section has more information on how you can control the subshell code.
+section has more information on how you can control subshell usage (-U/-u).
+Furthermore, to set a specific subshell different from your current SHELL variable or
+login shell defined in /etc/passwd, you may call MC like this:
+.B SHELL=/bin/myshell mc
 .\"NODE "Chmod"
 .SH "Chmod"
 The Chmod window is used to change the attribute bits in a group of


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]