10

I have a shell script in which I want to add a shebang. Given a variable defined as follows:

SHEBANG="#!/bin/sh"

My question is if I can use that variable in another script like this:

$SHEBANG
# other stuff...
  • 5
    I don't understand the plan. What is this supposed to be good for? Generating a script from another script? The shebang line is read by the kernel. – Hauke Laging Nov 24 '17 at 20:28
  • You probably haven't tried, have you? Because I did withc export SHEBANG='#!/bin/tcsh' and it fails with message #!/bin/tcsh: No such file or directory despite the /bin/tcsh exists. – Jaroslav Kucera Nov 24 '17 at 20:35
  • @JaroslavKucera: you probably ran it in a way that ran your shell on it directly. So exactly the same thing happened that would happen if you'd put $SHEBANG on a line by itself later in a bash or tcsh script, or if you'd run it interactively. /bin/tcsh is a valid path, but #!/bin/tcsh isn't, unless you happen to be CDed to a directory where ./#! is a directory or symlink that exists. – Peter Cordes Nov 25 '17 at 02:36

3 Answers3

13

No. The shebang line is processed by your kernel or system library* and doesn't process shell variables.

  • If you wanted to use an environment variable as the interpreter for some reason, you could create a "trampoline" executable (not script!) that re-spawned $SHEBANG "$@". You'd need to omit the #! from the variable in this case; #! is only useful when it is literally the first two bytes of a file.

  • Another option is to make use of the fact that scripts without shebang lines will (generally) be executed as shell scripts with either sh or the calling shell. If there's a comment syntax for your shebang target that overlaps usefully with shell syntax, you could make it work. This strategy is or used to be common with Lisp interpreters. In the case of your example, where the interpreter is also sh, this isn't viable.

  • You can push things even further on that line if you can rely on bash being present: a script whose first line is

    exec /bin/bash -c 'exec "$SHEBANG" <(tail -n +2 "$0")' "$0" "$@"
    

    will work for most interpreters; Bash is used as an intermediate step so that process substitution can be used to have tail skip the first line for us, so that we don't end up in an infinite loop. Some interpreters require that the file be seekable, and they won't accept a pipeline like this.


It's worth considering whether this is really something you want to do. Aside from any security concerns, it's just not very useful: few scripts can have their interpreters changed out like that, and where they can you'd almost certainly be better off with a more restricted piece of logic that spawned the correct one for the situation (perhaps a shell script in between, that calls the interpreter with the path to the real script as an argument).


* Not always; in certain circumstances some shells will read it themselves, but they still won't do variable expansion.

Michael Homer
  • 76,565
  • 1
    pretty much always it's execve() that receives the filename and that's a system call (not a library), – Jasen Nov 25 '17 at 05:30
  • @Jasen That is true however many shells have workarounds for a missing #! line. So if execve fails with ENOEXEC because of a missing #! line the shell may have other ways of deciding which interpreter to use. Those shouldn't be relied upon however, since different shells may choose different interpreters for the same script. – kasperd Nov 25 '17 at 12:07
4

I don't know if this is the use-case you have in mind, but one thing you can do is

#!/usr/bin/env python

so your script uses the first python in $PATH instead of needing to hard-code /usr/bin/python, which might not work well on some system where the "good" version of python is in /usr/local/bin or /opt or whatever.

Why is it better to use "#!/usr/bin/env NAME" instead of "#!/path/to/NAME" as my shebang?


This doesn't create an infinite loop because the Python interpreter (which env invokes on the file) just treats the #! line as a comment. This is kind of the point of the #! design, because many languages use # as a comment character.

To extend this, you could write a polyglot program that initially runs under #!/bin/sh, but then figures out what interpreter to actually use, and invokes that on the same script.

It turns out there's already a web page with multiline shebang tricks for many languages, including for compiled languages (the script feeds part of itself to a compiler and runs the output).


The perlrun man page gives this example as an alternative to #!/usr/bin/env perl:

    #!/bin/sh
    #! -*-perl-*-
    eval 'exec perl -x -wS $0 ${1+"$@"}'
        if 0;

    your perl script goes here

When run as a /bin/sh script, eval runs perl -x -wS on your script, passing along your args.

When Perl runs it, the eval is controlled by the if 0; on the next line, so it never runs. Unlike sh, Perl statements are not ended by a newline.

You could make this more complex, using "$SHEBANG" instead of perl, or maybe even running multiple sh commands to select a perl interpreter to use.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
Peter Cordes
  • 6,466
1

I think this is what you want:

xb@dnxb:/tmp$ cat /tmp/hole.sh
#!/usr/bin/$shell_var
echo 1
readlink "/proc/$$/exe"
echo 2
function f { echo hello world; } 
echo 3 
xb@dnxb:/tmp$ chmod +x /tmp/hole.sh
xb@dnxb:/tmp$ sudo vi /usr/bin/\$shell_var 
xb@dnxb:/tmp$ cat /usr/bin/\$shell_var 
#!/bin/bash
/bin/dash -- "$@"
xb@dnxb:/tmp$ sudo chmod +x /usr/bin/\$shell_var
xb@dnxb:/tmp$ ./hole.sh
1
/bin/dash
2
./hole.sh: 5: ./hole.sh: function: not found
3
xb@dnxb:/tmp$ sudo vi /usr/bin/\$shell_var 
xb@dnxb:/tmp$ cat /usr/bin/\$shell_var 
#!/bin/bash
/bin/bash -- "$@"
xb@dnxb:/tmp$ ./hole.sh 
1
/bin/bash
2
3
xb@dnxb:/tmp$ 

Explanation:

  1. Use \$shell_var file act as a "variable" to defined the shell you want.

  2. readlink "/proc/$$/exe" will print current shell.

  3. Since dash does not support function f { echo hello world; } syntax, so it produced error function: not found, but change shell to bash in \$shell_var file will no more error.

  4. The full file path (or relative path if ./hole.sh run in /tmp) will pass as $@ to the \$shell_var file, and /tmp/hole.sh will execute inside \$shell_var.

林果皞
  • 5,156
  • 3
  • 33
  • 46