--- a/lib/global.c +++ b/lib/global.c @@ -97,6 +97,7 @@ mc_global_t mc_global = { #endif /* !ENABLE_SUBSHELL */ .shell = NULL, + .shell_realpath = NULL, .xterm_flag = FALSE, .disable_x11 = FALSE, --- a/lib/global.h +++ b/lib/global.h @@ -255,6 +255,7 @@ typedef struct /* The user's shell */ char *shell; + char *shell_realpath; /* This flag is set by xterm detection routine in function main() */ /* It is used by function view_other_cmd() */ --- a/lib/mcconfig/paths.c +++ b/lib/mcconfig/paths.c @@ -84,6 +84,7 @@ static const struct /* 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}, --- a/src/main.c +++ b/src/main.c @@ -87,6 +87,9 @@ /*** file scope variables ************************************************************************/ /*** file scope functions ************************************************************************/ + +static char rp_shell[PATH_MAX]; + /* --------------------------------------------------------------------------------------------- */ static void @@ -118,6 +121,44 @@ check_codeset (void) } /* --------------------------------------------------------------------------------------------- */ +/** + * Get a system shell. + * + * @return newly allocated string with shell name + */ + +static char * +mc_get_system_shell (void) +{ + char *sh_str; + /* 3rd choice: look for existing shells supported as MC subshells. */ + if (access ("/bin/bash", X_OK) == 0) + sh_str = g_strdup ("/bin/bash"); + else if (access ("/bin/ash", X_OK) == 0) + sh_str = g_strdup ("/bin/ash"); + else if (access ("/bin/dash", X_OK) == 0) + sh_str = g_strdup ("/bin/dash"); + else if (access ("/bin/busybox", X_OK) == 0) + sh_str = g_strdup ("/bin/busybox"); + else if (access ("/bin/zsh", X_OK) == 0) + sh_str = g_strdup ("/bin/zsh"); + else if (access ("/bin/tcsh", X_OK) == 0) + sh_str = 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) + * mc_global.tty.shell = g_strdup ("/bin/fish"); + */ + else + /* Fallback and last resort: system default shell */ + sh_str = g_strdup ("/bin/sh"); + + return sh_str; +} + +/* --------------------------------------------------------------------------------------------- */ /** POSIX version. The only version we support. */ static void @@ -126,9 +167,11 @@ OS_Setup (void) const char *shell_env; const char *datadir_env; + shell_env = getenv ("SHELL"); if ((shell_env == NULL) || (shell_env[0] == '\0')) { + /* 2nd choice: user login shell */ struct passwd *pwd; pwd = getpwuid (geteuid ()); @@ -136,13 +179,15 @@ OS_Setup (void) mc_global.tty.shell = g_strdup (pwd->pw_shell); } else + /* 1st choice: SHELL environment variable */ mc_global.tty.shell = g_strdup (shell_env); if ((mc_global.tty.shell == NULL) || (mc_global.tty.shell[0] == '\0')) { g_free (mc_global.tty.shell); - mc_global.tty.shell = g_strdup ("/bin/sh"); + mc_global.tty.shell = mc_get_system_shell (); } + mc_global.tty.shell_realpath = mc_realpath (mc_global.tty.shell, rp_shell); /* This is the directory, where MC was installed, on Unix this is DATADIR */ /* and can be overriden by the MC_DATADIR environment variable */ --- a/src/subshell.c +++ b/src/subshell.c @@ -114,6 +114,8 @@ enum static enum { BASH, + ASH_BUSYBOX, /* BusyBox default shell (ash) */ + DASH, /* Debian variant of ash */ TCSH, ZSH, FISH @@ -209,6 +211,7 @@ static void init_subshell_child (const char *pty_name) { char *init_file = NULL; + char *putenv_str = NULL; pid_t mc_sid; (void) pty_name; @@ -257,32 +260,53 @@ init_subshell_child (const char *pty_nam 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); 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 */ { 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); } g_free (input_file); } 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: @@ -320,10 +344,6 @@ init_subshell_child (const char *pty_nam execl (mc_global.tty.shell, "bash", "-rcfile", init_file, (char *) NULL); break; - case TCSH: - execl (mc_global.tty.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 */ @@ -331,8 +351,11 @@ init_subshell_child (const char *pty_nam break; + case ASH_BUSYBOX: + case DASH: + case TCSH: case FISH: - execl (mc_global.tty.shell, "fish", (char *) NULL); + execl (mc_global.tty.shell, mc_global.tty.shell, (char *) NULL); break; default: @@ -341,6 +364,7 @@ init_subshell_child (const char *pty_nam /* If we get this far, everything failed miserably */ g_free (init_file); + g_free (putenv_str); my_exit (FORK_FAILURE); } @@ -742,6 +766,171 @@ pty_open_slave (const char *pty_name) } #endif /* !HAVE_GRANTPT */ + +/* --------------------------------------------------------------------------------------------- */ +/** + * Get a subshell type and store in subshell_type variable + * + * @return TRUE if subtype was gotten, FALSE otherwise + */ + +static gboolean +init_subshell_type (void) +{ + gboolean result = TRUE; + + /* 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 (mc_global.tty.shell, "/zsh") || strstr (mc_global.tty.shell_realpath, "/zsh") + || getenv ("ZSH_VERSION")) + /* Also detects ksh symlinked to zsh */ + subshell_type = ZSH; + else if (strstr (mc_global.tty.shell, "/tcsh") + || strstr (mc_global.tty.shell_realpath, "/tcsh")) + /* Also detects csh symlinked to tcsh */ + subshell_type = TCSH; + else if (strstr (mc_global.tty.shell, "/fish") + || strstr (mc_global.tty.shell_realpath, "/fish")) + subshell_type = FISH; + else if (strstr (mc_global.tty.shell, "/dash") + || strstr (mc_global.tty.shell_realpath, "/dash")) + /* Debian ash (also found if symlinked to by ash/sh) */ + subshell_type = DASH; + else if (strstr (mc_global.tty.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 (mc_global.tty.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 + { + mc_global.tty.use_subshell = FALSE; + result = FALSE; + } + return result; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * 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" ). + * + * @return initialized pre-command string + */ + +static void +init_subshell_precmd (char *precmd, size_t buff_size) +{ + + switch (subshell_type) + { + case BASH: + g_snprintf (precmd, buff_size, + " PROMPT_COMMAND='pwd>&%d; kill -STOP $$';\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, buff_size, + "precmd() { " + "if [ ! \"${PWD##$HOME}\" ]; then " + "MC_PWD=\"~\"; " + "else " + "[ \"${PWD##$HOME/}\" = \"$PWD\" ] && MC_PWD=\"$PWD\" || MC_PWD=\"~/${PWD##$HOME/}\"; " + "fi; " + "echo \"$USER@openwrt:$MC_PWD\"; " + "pwd>&%d; " + "kill -STOP $$; " + "}; " "PRECMD=precmd; " "PS1='$($PRECMD)$ '\n", subshell_pipe[WRITE]); + break; + + case ZSH: + g_snprintf (precmd, buff_size, + " precmd() { pwd>&%d; kill -STOP $$; }; " + "PS1='%%n@%%m:%%~%%# '\n", subshell_pipe[WRITE]); + break; + + case TCSH: + g_snprintf (precmd, buff_size, + "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, buff_size, + "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; + + default: + break; + } +} + /* --------------------------------------------------------------------------------------------- */ /*** public functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ @@ -761,6 +950,7 @@ init_subshell (void) { /* This must be remembered across calls to init_subshell() */ static char pty_name[BUF_SMALL]; + /* Must be considerably longer than BUF_SMALL (128) to support fancy shell prompts */ char precmd[BUF_MEDIUM]; switch (check_sid ()) @@ -782,23 +972,8 @@ init_subshell (void) if (mc_global.tty.subshell_pty == 0) { /* First time through */ - /* Find out what type of shell we have */ - - if (strstr (mc_global.tty.shell, "/zsh") || getenv ("ZSH_VERSION")) - subshell_type = ZSH; - else if (strstr (mc_global.tty.shell, "/tcsh")) - subshell_type = TCSH; - else if (strstr (mc_global.tty.shell, "/csh")) - subshell_type = TCSH; - else if (strstr (mc_global.tty.shell, "/bash") || getenv ("BASH")) - subshell_type = BASH; - else if (strstr (mc_global.tty.shell, "/fish")) - subshell_type = FISH; - else - { - mc_global.tty.use_subshell = FALSE; + if (!init_subshell_type ()) return; - } /* Open a pty for talking to the subshell */ @@ -844,7 +1019,7 @@ init_subshell (void) return; } } - else /* subshell_type is BASH or ZSH */ if (pipe (subshell_pipe)) + else if (pipe (subshell_pipe)) /* subshell_type is BASH, ASH_BUSYBOX, DASH or ZSH */ { perror (__FILE__ ": couldn't create pipe"); mc_global.tty.use_subshell = FALSE; @@ -872,39 +1047,116 @@ init_subshell (void) init_subshell_child (pty_name); } - /* Set up 'precmd' or equivalent for reading the subshell's CWD */ + init_subshell_precmd (precmd, BUF_MEDIUM); + + /* 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=${PROMPT_COMMAND:+$PROMPT_COMMAND\n}'pwd>&%d;kill -STOP $$'\n", - subshell_pipe[WRITE]); + " PROMPT_COMMAND=${PROMPT_COMMAND:+$PROMPT_COMMAND\n}'pwd>&%d;kill -STOP $$'\n" + "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@openwrt:$MC_PWD\"; " + "pwd>&%d; " + "kill -STOP $$; " + "}; " "PRECMD=precmd; " "PS1='$($PRECMD)$ '\n", subshell_pipe[WRITE]); break; case ZSH: g_snprintf (precmd, sizeof (precmd), - " _mc_precmd(){ pwd>&%d;kill -STOP $$ }; precmd_functions+=(_mc_precmd)\n", - subshell_pipe[WRITE]); + " _mc_precmd(){ pwd>&%d;kill -STOP $$ }; precmd_functions+=(_mc_precmd)\n" + "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: /* Use fish_prompt_mc function for prompt, if not present then copy fish_prompt to it. */ + /* 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), "if not functions -q fish_prompt_mc;" "functions -c fish_prompt fish_prompt_mc; end;" - "function fish_prompt; echo $PWD>&%d; fish_prompt_mc; kill -STOP %%self; end\n", + "function fish_prompt;" + "echo (whoami)@(hostname -s):(set_color $fish_color_cwd)(pwd)(set_color normal)\\$\\ ; " + "echo \"$PWD\">&%d; fish_prompt_mc; kill -STOP %%self; end\n", subshell_pipe[WRITE]); break; default: break; } + write_all (mc_global.tty.subshell_pty, precmd, strlen (precmd)); /* Wait until the subshell has started up and processed the command */ @@ -1108,6 +1360,13 @@ subshell_name_quote (const char *s) 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\" '";