#!/bin/sh # # Copyright 2013-2020 Lawrence Livermore National Security, LLC and other # Spack Project Developers. See the top-level COPYRIGHT file for details. # # SPDX-License-Identifier: (Apache-2.0 OR MIT) # # `sbang`: Run scripts with long shebang lines. # # Many operating systems limit the length and number of possible # arguments in shebang lines, making it hard to use interpreters that are # deep in the directory hierarchy or require special arguments. # # `sbang` can run such scripts, either as a shebang interpreter, or # directly on the command line. # # Usage # ----- # Suppose you have a script, long-shebang.sh, like this: # # 1 #!/very/long/path/to/some/interp # 2 # 3 echo "success!" # # Invoking this script will result in an error on some OS's. On # Linux, you get this: # # $ ./longshebang.sh # -bash: ./longshebang.sh: /very/long/path/to/some/interp: bad interpreter: # No such file or directory # # On macOS, the system simply assumes the interpreter is the shell and # tries to run with it, which is not likely what you want. # # # `sbang` on the command line # --------------------------- # You can use `sbang` in two ways. The first is to use it directly, # from the command line, like this: # # $ sbang ./long-shebang.sh # success! # # # `sbang` as the interpreter # -------------------------- # You can also use `sbang` *as* the interpreter for your script. Put # `#!/bin/sh /path/to/sbang` on line 1, and move the original # shebang to line 2 of the script: # # 1 #!/bin/sh /path/to/sbang # 2 #!/long/path/to/real/interpreter with arguments # 3 # 4 echo "success!" # # $ ./long-shebang.sh # success! # # On Linux, you could shorten line 1 to `#!/path/to/sbang`, but other # operating systems like Mac OS X require the interpreter to be a binary, # so it's best to use `sbang` as an argument to `/bin/sh`. Obviously, for # this to work, `sbang` needs to have a short enough path that *it* will # run without hitting OS limits. # # For Lua, node, and php scripts, the second line can't start with #!, as # # is not the comment character in these languages (though they all # ignore #! on the *first* line of a script). So, instrument such scripts # like this, using --, //, or instead of # on the second # line, e.g.: # # 1 #!/bin/sh /path/to/sbang # 2 --!/long/path/to/lua with arguments # 3 print "success!" # # 1 #!/bin/sh /path/to/sbang # 2 //!/long/path/to/node with arguments # 3 print "success!" # # 1 #!/bin/sh /path/to/sbang # 2 # 3 # # How it works # ------------ # `sbang` is a very simple posix shell script. It looks at the first two # lines of a script argument and runs the last line starting with `#!`, # with the script as an argument. It also forwards arguments. # # We disable two shellcheck errors below: # SC2124: when saving arguments, we intentionally assign as an array # SC2086: when splitting $shebang_line and exec args, we want to expand args # Generic error handling die() { echo "$@" 1>&2; exit 1 } # set SBANG_DEBUG to make the script print what would normally be executed. exec="exec" if [ -n "${SBANG_DEBUG}" ]; then exec="echo " fi # First argument is the script we want to actually run. script="$1" # ensure that the script actually exists if [ -z "$script" ]; then die "error: sbang requires exactly one argument" elif [ ! -f "$script" ]; then die "$script: no such file or directory" fi # Search the first two lines of script for interpreters. lines=0 while read -r line && [ $lines -ne 2 ]; do if [ "${line#\#!}" != "$line" ]; then shebang_line="${line#\#!}" elif [ "${line#//!}" != "$line" ]; then # // comments shebang_line="${line#//!}" elif [ "${line#--!}" != "$line" ]; then # -- lua comments shebang_line="${line#--!}" elif [ "${line#}" fi lines=$((lines+1)) done < "$script" # shellcheck disable=SC2124 # this saves arguments for later and intentionally assigns as an array args="$@" # handle scripts with sbang parameters, e.g.: # # #!//perl -w # # put the shebang line with all the parameters in the $@ array and get # the first element. # shellcheck disable=SC2086 set $shebang_line set -- "$@" interpreter="$1" arg1="$2" # error if we did not find any interpreter if [ -z "$interpreter" ]; then die "error: sbang found no interpreter in $script" fi # Determine if the interpreter is a particular program, accounting for the # '#!/usr/bin/env PROGRAM' convention. So: # # interpreter_is perl # # will be true for '#!/usr/bin/perl' and '#!/usr/bin/env perl' interpreter_is() { if [ "${interpreter##*/}" = "$1" ]; then return 0 elif [ "$interpreter" = "/usr/bin/env" ] && [ "$arg1" = "$1" ]; then return 0 else return 1 fi } if interpreter_is "sbang"; then die "error: refusing to re-execute sbang to avoid infinite loop." fi # Finally invoke the real shebang line # ruby and perl need -x to ignore the first line of input (the sbang line) # if interpreter_is perl || interpreter_is ruby; then # shellcheck disable=SC2086 $exec $shebang_line -x "$args" else # shellcheck disable=SC2086 $exec $shebang_line "$args" fi