In many script files, we can see the following code on the <span>first line</span>:
#!/bin/bash
或
#!/usr/bin/env python
Carefully observe that the first line of the script file starts with the characters <span>#!</span>.
This character sequence consisting of <span>hash</span> and <span>exclamation mark</span> is called <span>Shebang</span> (also known as <span>Hashbang</span>).
Its purpose is to tell the system which <span>interpreter</span> should be used to execute this script file.
<span>Basic syntax:</span>
#!/path/to/interpreter [optional-arg]
<span>#!</span>: Fixed starting symbol. For portability, there should not be any characters (including spaces) before<span>#!</span>.<span>/path/to/interpreter</span>: The absolute path of the interpreter program. For example, /bin/bash, /usr/bin/python3, /bin/sh, etc.<span>[optional-arg]</span>: An optional argument. It is usually not commonly used, but a classic example is<span>#!/bin/sh -ex</span>, where<span>-ex</span>enables detailed execution logging for shell scripts.
<span>Key Point</span>: Must be located on the<span>first line</span>of the script file. Based on my tests, to make the<span>-ex</span>option effective in<span>#!/usr/bin/env -S bash -ex</span>, that line of code must be on the first line; even an empty line will not work. Therefore, there is no need to innovate here; just write it honestly on the first line.
1. Examples of Shebang Executing Scripts
Taking <span>bash</span> as an example, on my computer:
# Actual path of bash
$ which bash
/usr/bin/bash
Save the following code in <span>test.sh</span>:
#!/usr/bin/bash -ex
radius=2.5
result=$(awk -v r=$radius 'BEGIN {printf "%.3f", 3.14159 * r * r}')
echo "area: $result"
Grant execution permission to the script:
chmod 755 ./test.sh
# or
chmod +x ./test.sh
Execute the script:
$ ./test.sh
+ radius=2.5
++ awk -v r=2.5 'BEGIN {printf "%.3f", 3.14159 * r * r}'
+ result=19.635
+ echo 'area: 19.635'
area: 19.635
If the <span>Shebang</span> mechanism is not used, you have to specify the interpreter yourself:
bash ./test.sh
Now let’s change the content of <span>test.sh</span>, adding a space before <span>#!</span>:
#!/usr/bin/bash -ex
radius=2.5
result=$(awk -v r=$radius 'BEGIN {printf "%.3f", 3.14159 * r * r}')
echo "area: $result"
$ ./test.sh
area: 19.635
# The -ex option seems to be ignored!!! So do not add any characters before #!.
This is not that the <span>-ex</span> option is ignored, but rather that the operating system did not recognize the existence of the <span>Shebang</span>, and the system used the default shell (like <span>/bin/sh</span>) to execute it.
I tested this on <span>macOS</span> as follows:
#!/usr/bin/bash -ex
radius=2.5
result=$(awk -v r=$radius 'BEGIN {printf "%.3f", 3.14159 * r * r}')
echo "area: $result"
# New code
sleep 30
Test it as follows:
# Run in the background
$ ./test.sh &
$ ps -l
UID PID PPID TIME CMD
501 14016 14015 0:03.40 -zsh
501 62761 14016 0:00.03 sh ./test.sh
As shown, the script is executed using <span>sh</span>.
Even if the default shell is <span>bash</span>, it is unrelated to the <span>Shebang</span> specified here; otherwise, the <span>-ex</span> option would definitely take effect.
2. /usr/bin/env
In the above example, we hard-coded the path of the <span>bash</span> interpreter. This makes our code unable to execute when moved to another machine with a different path.
$ which bash
/opt/homebrew/bin/bash
As shown, the path of <span>bash</span> on my other computer is different.
$ ./test.sh
./test.sh: bad interpreter: /usr/bin/bash: no such file or directory
# Error message indicates that the bash interpreter cannot be found.
There are two solutions: one is to modify the path of the bash interpreter, and the other is to use <span>/usr/bin/env</span>.
#!/usr/bin/env bash
set -ex
radius=2.5
result=$(awk -v r=$radius 'BEGIN {printf "%.3f", 3.14159 * r * r}')
echo "area: $result"
Executing it again works correctly, as shown:
$ ./test.sh
+ radius=2.5
++ awk -v r=2.5 'BEGIN {printf "%.3f", 3.14159 * r * r}'
+ result=19.635
+ echo 'area: 19.635'
area: 19.635
<span>#!/usr/bin/env bash</span> means: “Please find the program named <span>bash</span> from the <span>PATH</span> environment variable and use it to execute this script”.
Readers may have noticed that the above <span>#!/usr/bin/env bash</span> does not specify the <span>-ex</span> option anymore, but moves it into the script file as <span>set -ex</span>.
Of course, you can specify the <span>env</span> with the <span>-S</span> option to pass parameters:
#!/usr/bin/env -S bash -ex
radius=2.5
result=$(awk -v r=$radius 'BEGIN {printf "%.3f", 3.14159 * r * r}')
echo "area: $result"
It produces the same result as above:
$ ./test.sh
+ radius=2.5
++ awk -v r=2.5 'BEGIN {printf "%.3f", 3.14159 * r * r}'
+ result=19.635
+ echo 'area: 19.635'
area: 19.635
Here is an actual example in <span>Python</span>:
#!/usr/bin/env python
# coding: utf-8
import sys
print(sys.path)
It specifies both the <span>Shebang</span> and the encoding of the Python source code, see PEP 263 – Defining Python Source Code Encodings[1].
3. Summary
In summary, <span>Shebang</span> has the following considerations:
<span>Must be the first line</span>: The Shebang must be located on the first line of the file. There should not be any spaces or characters before<span>#!</span>.<span>Requires executable permission</span>: To allow the script to run directly like a program (<span>./script</span>), you must give the script file executable permission using the<span>chmod +x script.sh</span>command.
<span>Shebang</span> is a simple yet powerful mechanism that allows text script files written in various languages to be conveniently launched like <span>binary executable files</span>. It automates and standardizes script execution by associating script files with the correct interpreter.
Finally, using <span>#!/usr/bin/env</span> is the best practice for writing portable scripts.
References
[1]
PEP 263 – Defining Python Source Code Encodings: https://peps.python.org/pep-0263/