Let's start with the second of your cases, namely where you do have #!/bin/bash, because it is actually the easier one to deal with first.
With the #!/bin/bash
When you execute a script which starts with #!/path/to/interpreter, the Linux kernel will understand this syntax and will invoke the specified interpreter for you in the same way as if you had explicitly added /path/to/interpreter to the start of the command line. So in the case of your script starting with #!/bin/bash, if you look using ps ux, you will see the command line /bin/bash ./sample.sh.
Without the #!/bin/bash
Now turning to the other one where the #!/bin/bash is missing. This case is more subtle.
A file which is neither a compiled executable nor a file starting with the #! line cannot be executed by the Linux kernel at all. Here is an example of trying to run the sample.sh without the #!/bin/bash line from a python script:
>>> import subprocess
>>> p = subprocess.Popen("./sample.sh")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.7/subprocess.py", line 394, in __init__
errread, errwrite)
File "/usr/lib/python2.7/subprocess.py", line 1047, in _execute_child
raise child_exception
OSError: [Errno 8] Exec format error
And to show that this is not just a python issue, here is a demonstration of exactly the same thing, but from a C program. Here is the C code:
#include <stdio.h>
#include <unistd.h>
int main() {
execl("./sample.sh", "sample.sh", NULL);
/* exec failed if reached */
perror("exec failed");
return 1;
}
and here is the output:
exec failed: Exec format error
So what is happening here case when you run the script is that because you are invoking it from a bash shell, bash is giving some fault tolerance by running the commands directly after the attempt to "exec" the script has failed.
What is happening in more detail is that:
bash forks a subshell,
inside the subshell it straight away does a call to the Linux kernel to "exec" your executable, and if successful, that would end that (subshell) process and replace it with a process running the executable
however, the exec is not successful, and this means that the subshell is still running
at that point the subshell just reads the commands inside your script and starts executing them directly.
The overall effect is very similar to the #!/bin/bash case, but because the subshell was just started by forking your original bash process, it has the same command line, i.e. just bash, without any command line argument. If you look for this subshell in the output of ps uxf (a tree-like view of your processes), you will see it just as
bash
\_ bash
whereas in the #!/bin/bash case you get:
bash
\_ /bin/bash ./sample.sh
sample.sh's PID either way with bash 5.0.11. But if I putprintf 'boogaloo' > /proc/self/comminsample.sh's first line, I can get its PID bypgrep boogaloo.sh, but this still does not explain for me the differnce. What happens if you start the script explicitly bysh sample.sh, compared tobash sample.sh?