9

I'm writing a bash script and it throws an error when using "sh" command in Ubuntu (it seems it's not compatible with dash, I'm learning on this subject). So I would like to detect if dash is being used instead of bash to throw an error.

How can I detect it in a script context?. Is it even possible?

2
  • 3
    What you can do is to define the interpreter in the first line of your script: #!/bin/bash will make it be executed with /bin/bash when called with ./script.sh. Also, good to see Pink Floyd around :) Commented Apr 11, 2014 at 11:31
  • bash's features are a superset of dash's features; dash is smaller and faster, but is mostly limited to POSIX features - see en.wikipedia.org/wiki/Debian_Almquist_shell Commented Apr 11, 2014 at 11:49

5 Answers 5

16

You can check for the presence of shell-specific variables:

For instance, bash defines $BASH_VERSION. Since that variable won't be defined while running in dash, you can use it to make the distinction:

[ -n "$BASH_VERSION" ] && isBash=1

Afterthought: If you wanted to avoid relying on variables (which, conceivably, could be set incorrectly), you could try to obtain the ultimate name of the shell executable running your script, by determining the invoking executable and, if it is a symlink, following it to its (ultimate) target.

The shell function getTrueShellExeName() below does that; for instance, it would return 'dash' on Ubuntu for a script run with sh (whether explicitly or via shebang #!/bin/sh), because sh is symlinked to dash there.

Note that the function's goal is twofold:

  • Be portable:
    • Work with all POSIX-compatible (Bourne-like) shells,
    • across at least most platforms, with respect to what utilities and options are used - see caveats below.
  • Work in all invocation scenarios:
    • sourced (whether from a login shell or not)
    • executed stand-alone, via the shebang line
    • executed by being passed as a filename argument to a shell executable
    • executed by having its contents piped via stdin to a shell executable

Caveats:

  • While the function returns the true shell binary name, bear in mind that some shells, notably bash and zsh, run in POSIX compatibility mode when invoked via a symlink named sh.

  • On at least one platform - macOS - sh is NOT a symlink. On macOS, it is a binary executable that, per man sh, re-execs the symlink at /private/var/select/sh, which may point to one of the following: /bin/bash, /bin/dash, or /bin/zsh. As of macOS Sequoia (15), it still points to /bin/bash by default (even though recent macOS versions now use /bin/zsh as the interactive default shell for users), and the function therefore returns bash.[1]

  • The function uses readlink, which, while not mandated by POSIX, is present on most modern platforms - though with differing syntax and features. Therefore, using GNU readlink's -f option to find a symlink's ultimate target is not an option.
    (The only modern platform I'm personally aware of that does not have a readlink utility is HP-UX - see https://stackoverflow.com/a/24114056/45375 for a recursive-readlink implementation that should work on all POSIX platforms.)

  • The function uses the which utility (except in zsh, where it's a builtin), which, while not mandated by POSIX, is present on most modern platforms.

  • Ideally, ps -p $$ -o comm= would be sufficient to determine the path of the executable underlying the process, but that doesn't work as intended when directly executing shell scripts with shebang lines on Linux, at least when using the ps implementation from the procps-ng package, as found on Ubuntu, for instance: there, such scripts report the script's file name rather than the underlying script engine's.Tip of the hat to ferdymercury for his help.
    Therefore, the content of special file /proc/$$/cmdline is parsed on Linux, whose first NUL-separated field contains the true executable path.

Example use of the function:

[ "$(getTrueShellExeName)" = 'bash' ] && isBash=1 

Shell function getTrueShellExeName():

getTrueShellExeName() {
  local trueExe nextTarget 2>/dev/null # ignore error in shells without `local`
  # Determine the shell executable filename.
  if [ -r /proc/$$/cmdline ]; then
    trueExe=$(cut -d '' -f1 /proc/$$/cmdline) || return 1
  else
    trueExe=$(ps -p $$ -o comm=) || return 1
  fi
  # Strip a leading "-", as added e.g. by macOS for login shells.
  [ "${trueExe#-}" = "$trueExe" ] || trueExe=${trueExe#-}
  # Determine full executable path.
  [ "${trueExe#/}" != "$trueExe" ] || trueExe=$([ -n "$ZSH_VERSION" ] && which -p "$trueExe" || which "$trueExe")
  # If the executable is a symlink, resolve it to its *ultimate*
  # target.
  while nextTarget=$(readlink "$trueExe"); do trueExe=$nextTarget; done
  # on MacOS, sh is not a symlink, but an executable that
  # re-execs /private/var/select/sh, so resolve it
  if [ "$(basename $trueExe)" = "sh" -a -L "/private/var/select/sh" ]; then
    trueExe="/private/var/select/sh";
    while nextTarget=$(readlink "$trueExe"); do trueExe=$nextTarget; done
  fi
  # Output the executable name only.
  printf '%s\n' "$(basename "$trueExe")"
}

[1] If symlink /private/var/select/sh doesn't exist "or does not point to a valid shell, sh will use one of the supported shells", per man sh; in other words: it is then unspecified which shell among bash, dash, and zsh is used; in practice, as of macOS Sequoia (15), it falls back to /bin/bash. Note that while macOS comes with said symlink, it is possibly to modify or even delete it via sudo, though doing so has far-reaching ramifications, given that different shells may behave subtly differently even when run in POSIX-compatibility mode.

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

12 Comments

@zrajm: The purpose of my shell function is twofold: (a) be portable and (b) work in all invocation scenarios. I've updated my answer to clarify. Your suggestion doesn't satisfy (a), because readlink -f is a GNU extension that doesn't work on OSX, for instance. It doesn't satisfy (b), because analyzing the shebang line is insufficient - for instance, your script could have been passed as a direct argument to any shell executable instead.
@ferdymercury: -d '' uses NUL (\0) as the delimiter (in the GNU implementation of cut, not in the macOS (and presumably also the BSD) implementation), and -f1 extracts the first field. Note that /proc/$$/cmdline contains a string in which the binary path and its individual arguments are separated with NULs, and that the first field, i.e. the binary's path, at least hypothetically may contain spaces, so using a space as the delimiter isn't robust.
@ferdymercury: You cannot communicate a literal NUL as an argument, so there's no cut workaround that I'm aware of. awk -F '\0' works with GNU Awk and Mawk, but not with the BWK Awk used on macOS. As long as ps -p $$ -o comm= works on all non-Linux platforms, there shouldn't be a problem here, given that /proc/$$/cmdline only exists on Linux platform, the cut solution is only required there, and cut can (reasonably) be assumed to be the GNU implementation on Linux distros.
@ferdymercury, $'\000' is an ANSI C-quoted string, supported in bash, ksh, zsh, but not POSIX-compliant. In arguments (as opposed to data provided via stdin), POSIX-compatible shells consider a NUL the end of the string, so that passing $'\000' is in effect the same as passing '' - hence the simpler use of -d '' in the answer. While xargs -0 is portable in principle, virtual file /proc/$$/cmdline is not (it is Linux-specific). Aside from that, to make your xargs -0 command work as intended, you'd need to use xargs -0 -n 1 < /proc/$$/cmdline 2>/dev/null | head -n 1
Thanks for the addition, @ferdymercury.
|
6

Use $0 (that is the name of the executable of the shell being called).The command for example

echo $0

gives

/usr/bin/dash

for the dash and

/bin/bash

for a bash.The parameter substitution

${0##*/}

gives just 'dash' or 'bash'. This can be used in a test.

2 Comments

This isn't reliable. What if the script is executable and invoked as ./script? What if the script is sourced?
$0 is set to an arbitrary string by whatever invokes the shell. It's almost always right...
2

An alternative approach might be to test if a shell feature is available, for example to give an idea...

[[ 1 ]] 2>/dev/null && echo could be bash || echo not bash, maybe dash

Comments

-1
is_x_shell() { which ${1} >/dev/null && diff $(realpath /proc/$$/exe) $(which ${1}) >/dev/null; }

is_x_shell dash && echo is_dash
is_x_shell bash && echo is_bash
is_x_shell ash && echo is_ash
is_x_shell zsh && echo is_zsh

Comments

-2
echo $0 and [[ 1 ]] 2>/dev/null && echo 

could be bash || echo not bash, maybe bash worked for me running Ubuntu 19.

Done slight Pascal, Fortran and C in school, but need to become fluent in shell script.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.