summaryrefslogtreecommitdiff
path: root/bin/sbang
blob: 983f77865151a356d0b33a65c288b157078d4138 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/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