17

The scenario is that users are asked to source a script file:

$ source envsetup.sh

This script file may use bash only feature so we have detect the running shell is bash or not.

For other shells that share common syntax with bash, for example, sh, zsh, ksh, I'd like to report a warning.

What is the most reliable way to detect the current shell across Linux, Cygwin, OS X?

What I know is $BASH, but I am wondering the chances it could fail.

8 Answers 8

21

There are a bunch of environment variables that you can look at but many of them will not detect if a different shell is spawned from bash. Consider the following:

bash$ echo "SHELL: $SHELL, shell: $shell, ARGV[0]: $0, PS1: $PS1, prompt: $prompt"
SHELL: /bin/bash, shell: , ARGV[0]: -bash, PS1: bash$ , prompt: 

bash$ csh
[lorien:~] daveshawley% echo "SHELL: $SHELL, shell: $shell, \$0: $0, PS1: $PS1, prompt: $prompt"
SHELL: /bin/bash, shell: /bin/tcsh, ARGV[0]: csh, PS1: bash$ , prompt: [%m:%c3] %n%#

[lorien:~] daveshawley% bash -r
bash$ echo "SHELL: $SHELL, shell: $shell, ARGV[0]: $0, PS1: $PS1, prompt: $prompt"
SHELL: /bin/bash, shell: , ARGV[0]: sh, PS1: bash$ , prompt:

bash$ zsh
% echo "SHELL: $SHELL, shell: $shell, ARGV[0]: $0, PS1: $PS1, prompt: $prompt"
SHELL: /bin/bash, shell: , ARGV[0]: zsh, PS1: % , prompt: % 

% ksh
$ echo "SHELL: $SHELL, shell: $shell, ARGV[0]: $0, PS1: $PS1, prompt: $prompt"
SHELL: /bin/bash, shell: , ARGV[0]: ksh, PS1: bash$ , prompt: 

There are a number of variables specific to the various shells except that they have a habit of being inherited by sub-shells which is where the environment thing really breaks. The only thing that almost works is ps -o command -p $$. This technically gives you the command name that the shell is running as. In most cases this will work... since applications are started with some variant of the exec system call and it allows for the name of the command and the executable to differ, it is possible for this to fail as well. Consider:

bash$ exec -a "-csh" bash
bash$ echo "$0, $SHELL, $BASH"
-csh, /bin/bash, /bin/bash
bash$ ps -o command -p $$
COMMAND
-csh
bash$

Another trick is to use lsof -p $$ | awk '(NR==2) {print $1}'. This is probably as close as you can get if you are lucky enough to have lsof handy.

Sign up to request clarification or add additional context in comments.

1 Comment

lsof -p $$ | awk '(NR==2) {print $1}' is not reliable on Darwin so ps -o command -p $$ is the best alternative.
15

This works also

[ -z "$BASH_VERSION" ] && return

4 Comments

Can also do this for zsh, so if [ -n "${BASH_VERSION}" ]; then ... elif [ -n "${ZSH_VERSION}" ]; then ... fi
have the script: #!/bin/sh \n echo $BASH_VERSION and even run it with sh or csh from a bash shell: $sh ./test.sh and you will get the env var from the user prefered bash version.... totally misleading the running script
@gcb This is not true, as $BASH_VERSION is not an exported variable. However, remember that even if you run a script from a different shell, that shell will be looking at the script's shebang line and append the script name to it. So if that's #!/bin/sh and your /bin/sh is a symlink to bash, then your script will indeed be running in bash, no matter what shell you ran it from.
@Bachsau thanks. it was indeed a symlink and i didn't noticed it on my test! It was bash all along :)
15

Here is a nice way:

if test -z "$(type -p)" ; then echo bash ; else echo sh ; fi

and you can of course replace the "echo" statements with anything you want.

=================

Discussion:

  • The $SHELL variable indicates the user's preferred shell ... which tells you nothing about the shell that is running at the moment.

  • Testing $BASH_VERSION is a 99% good idea, but it could fail if some wise-guy sticks a variable of that name into the sh environment. Furthermore, it doesn't tell you much about which non-bash shell is running.

  • The $(type -p) method is super-easy, and works even if some wise guy creates a file called "-p" in your $PATH. Furthermore, it can be used as the basis for a 4-way discrimination, or 80% of a 5-way discrimination, as discussed below.

  • Putting a hash-bang i.e. #! at the top of your script does not guarantee that it will be fed to the interpreter of your choice. For example, my ~/.xinitrc gets interpreted by /bin/sh no matter what hash-bang (if any) appears at the top.

  • Good practice is to test some feature that reliably exists in both languages, but behaves differently. In contrast, it would not be safe in general to try the feature you want and see whether it fails. For instance, if you want to use the built-in declare feature and it's not present, it could run a program, and that has unlimited downside potential.

  • Sometimes it is reasonable to write compatible code, using the lowest-common-denominator feature set ... but sometimes it isn't. Most of those added features were added for a reason. Since these interpreters are «almost» Turing-complete it is «almost» guaranteed to be possible to emulate one with the other ... possible, but not reasonable.
  • There are two layers of incompatibility: syntax and semantics. For instance, the if-then-else syntax for csh is so different from bash that the only way to write compatible code would be to do without if-then-else statements altogether. That's possible, but it imposes a high cost. If the syntax is wrong, the script won't execute at all. Once you get past that hurdle, there are dozens of ways in which reasonable-looking code produces different results, depending on which dialect of interpreter is running.
  • For a large, complicated program, it does not make sense to write two versions. Write it once, in the language of your choice. If somebody starts it under the wrong interpreter, you can detect that and simply exec the right interpreter.

  • A 5-way detector can be found here:

    https://www.av8n.com/computer/shell-dialect-detect

    It can discriminate:

    • bash
    • bsd-csh
    • dash
    • ksh93
    • zsh5

    Furthermore, on my Ubuntu Xenial box, that 5-way check also covers the following:

    • ash is a symlink to dash
    • csh is a symlink to /bin/bsd-csh
    • ksh is a symlink to /bin/ksh93
    • sh is a symlink to dash

1 Comment

type -p check fails to detect Bash’s sh mode.
6

I think this would be the most practical and cross shell compatible

/proc/self/exe --version 2>/dev/null | grep -q 'GNU bash' && USING_BASH=true || USING_BASH=false

Explanation:

/proc/self will always point to the current executing process, for example, running the following reveals the pid of readlink it self (not the shell which executed readlink)

$ bash -c 'echo "The shell pid = $$"; echo -n "readlink (subprocess) pid = "; readlink /proc/self; echo "And again the running shells pid = $$"'

Results in:

The shell pid = 34233
readlink (subprocess) pid = 34234
And again the running shells pid = 34233

Now: /proc/self/exe is a symbolic link to the running executable

Example:

bash -c 'echo -n "readlink binary = "; readlink /proc/self/exe; echo -n "shell binary = "; readlink /proc/$$/exe'

results in:

readlink binary = /bin/readlink
shell binary = /bin/bash

And here is the results running in dash and zsh, and running bash through a symlink, and even through a copy.

aron@aron:~$ cp /bin/bash ./my_bash_copy
aron@aron:~$ ln -s /bin/bash ./hello_bash
aron@aron:~$ 

aron@aron:~$ dash -c '/proc/self/exe -c "readlink /proc/$$/exe"; zsh -c "/proc/self/exe --version"; ./hello_bash --version | grep bash; ./my_bash_copy --version | grep bash'
/bin/dash
zsh 5.0.7 (x86_64-pc-linux-gnu)
GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu)
GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu)
aron@aron:~$ dash -c '/proc/self/exe -c "readlink /proc/$$/exe"; zsh -c "/proc/self/exe --version"; ./hello_bash --version | grep bash; ./my_bash_copy --version | grep bash'

1 Comment

For Linux that seems like a good idea... Not sure if it is portable to OS X or Cygwin as asked in the question (I know that on FreeBSD /proc is not mounted by default...)
2

to improve @Dumble0re answer to support more shell dialets:

# original detect shell dialet, see: https://www.av8n.com/computer/shell-dialect-detect
# improvement version: https://gist.github.com/gzm55/028912a3d4c2846790c7438d0863fd7f
# `&&` will defeat the errexit option, see: http://www.binaryphile.com/bash/2018/08/13/approach-bash-like-a-developer-part-15-strict-mode-caveats.html
eval `echo ! : 2>/dev/null` : && echo "[ERROR] must not be executed or sourced by csh!" && exit 64

__DO_NOT_SUPPORT_FISH__=1

# Now that csh and fish has been excluded, it is safe to continue

__SHELL_DIALECT__=
case "${__SHELL_DIALECT__:=$(
  PATH="/dev/null/$$" set +e
  PATH="/dev/null/$$" export PATH="/dev/null/$$"
  type -p 2>/dev/null >/dev/null
  echo $? $(type declare 2>/dev/null; echo err=$?)
)}" in
"0 "*"not found err=1") __SHELL_DIALECT__=mksh ;;
"0 "*"not found err=127") __SHELL_DIALECT__=busybox ;; # ash busybox various
"0 "*"shell builtin err="*) __SHELL_DIALECT__=bash4 ;;
"1 "*"reserved word err="*) __SHELL_DIALECT__=zsh5 ;;
"1 "*"shell builtin err="*) __SHELL_DIALECT__=bash3 ;;
"2 err="*) __SHELL_DIALECT__=ksh93 ;;
"127 "*"not found err="*) __SHELL_DIALECT__=dash ;; # ash debian various
"127 err=127") __SHELL_DIALECT__=ash-bsd ;; # ash bsd-sh various
*) __SHELL_DIALECT__="unknown:$__SHELL_DIALECT__" ;;
esac

# detect bash posix mode, on bash, SHELLOPTS is a read only variable
case "$__SHELL_DIALECT__:${SHELLOPTS-}:" in
bash*:posix:*) __SHELL_DIALECT__="$__SHELL_DIALECT__-posix" ;;
*) ;;
esac

echo "__SHELL_DIALECT__=[$__SHELL_DIALECT__]"

The importanted is that, bash 3 has different detecting method, which should be the default /bin/sh on MacOS.

Another, when detecting csh, the improved method avoids depending on the location of test at /usr/bin/test, the MacOS does not has this path.

Comments

0

I would recommend trying to detect the presence of the feature you need rather than bash vs zsh vs etc. If the feature is present, use it, if not use an alternative. If there's no good way to detect the feature other than using it and checking for errors, then although that's kind of ugly, I don't have a better solution. What matters is that it should work more reliably than trying to detect bash. And since other shells (that are still in development) may have this formerly-bash-only feature at some point in the future, it really deals with what matters, and allows you to avoid having to maintain a database of which versions of each shell support which feature and which don't.

2 Comments

7 years later, while I understand this, it doesn't seem very practical to me. In contrast to feature-detection in browsers, figuring out how to test a variety of shell features sounds significantly more challenging to me than using something like Modernizr or detecting the shell.
@xdhmoore 5 years later, what you're talking about is essentially what GNU Autotools does for every aspect of the system but primarily the C language and system calls. You're absolutely right that it's challenging, and IMO a bad idea.
0

Within a script (i.e. not in the interactive command line), with Bash, we have BASH_SOURCE set to the pathname of the script (whether the script is executed normally or is being source'd).

Actually, with Bash, we might want not to use "${BASH_SOURCE}", but rather "${BASH_SOURCE[0]}.

But if we do not know whether it is Bash or non-Bash, we might first want to test -n "${BASH_SOURCE}".

With a non-Bash shell, within a script, we do not have BASH_SOURCE set to anything (i.e. the following is true: test -z "${BASH_SOURCE}") (and we have to try to use "${0}" a.k.a. "$0" in order to obtain the name of the script) (and it is going to be the name of the originally executed script, not the name of the source'd script, and actually we cannot use the source keyword in order to source a sub-script -- we can only use the . for this).

Example:

if test -n "${BASH_SOURCE}"; then
    printf 'This is Bash executing %s.\n' "'${BASH_SOURCE[0]}'"
    # We can use Bash-specific syntax here. The Other Shell is not going to complain.
else
    printf 'This is Another Shell executing %s.\n' "'${0}'"
fi

To emphasize: Neither "${BASH_SOURCE}" nor "${0}" (a.k.a. "$0") are set to anything if we are not executing a script and instead are in the interactive command-line.

If we source a script (instead of executing it) from the interactive command-line (via . path/to/subscript.sh or, with Bash, via source path/to/subscript.sh), "${0}" (a.k.a. "$0") expands to -bash or dash or something else, not to the pathname of the sourced subscript.

I hope this helps.

Comments

-1

The SHELL environment variable will tell you what login shell is running.

Also, you can use ps $$ to find the current shell, which can be used if you want to know what shell the script is running under (not necessarily the login shell). To whittle down the ps output to just the shell name: ps o command= $$ (not sure how cross-platform safe this is, but it works on Mac OS X).

6 Comments

Try this from within bash before you rely on $SHELL: csh -c 'echo "$SHELL"'
My impression of the question is that the script needs to know the login shell. Using bash as the login shell, running that command should return bash, which is expected.
Not the login shell. If you login use bash then change to csh, $SHELL is remain bash, but source would fail.
ps o command= $$ also works on Linux, now I have to check cygwin.. But you know, the ps command has so many variants..
If you're running in fish shell, then $$ is not the alias for the pid. Instead, in fish you would run ps -o command -p %self. PITA.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.