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.
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.
This works also
[ -z "$BASH_VERSION" ] && return
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
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'
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.
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).
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.
精彩评论