summaryrefslogblamecommitdiff
path: root/bin/sbang
blob: 983f77865151a356d0b33a65c288b157078d4138 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
         
 
                                                                         
                                                                         
 

                                              


                                               



                                                                         



                                                                   
       

                                                        
                                           





                                                                

                                                                               

                                      

                                                                      


                             
                             







                                                                    
                            
                                                                    
                                                             

                                  
                                   







                                                                     



                                                                         
 




                                                                         
 
                                   
                                              

                           
                                   


                                               
                                   

                                                     

              



                                                                        

 















                                                                            


                                                       






                                                    

                                                        









                                                                  



                      































                                                                          
        
                
      











                                                                          
    

                               
  
#!/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 <?php ... ?> 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    <?php #/long/path/to/php with arguments ?>
#     3    <?php echo "success!\n"; ?>
#
# 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#<?php\ }" != "$line" ]; then  # php comments
        shebang_line="${line#<?php\ \#!}"
        shebang_line="${shebang_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.:
#
#   #!/<spack-long-path>/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