10

Scripts typically start with a shebang such as #!/usr/bin/env bash, which specifies the shell to be used for execution. The execution behavior when the shebang is not present seems to be up to the calling shell. Either way, the script is run from a "new" shell which doesn't know about the variables and functions defined in the calling shell.

Alternatively, scripts can also be sourced into a shell, which by my understanding is equivalent to a copy-paste of the script's contents into the current shell. If any functions or variables are defined in the script, then they will remain in the calling shell after "execution", which may not be desirable.

Is there something between these two options? Is it possible to execute a script as a subshell of the calling shell, such that we would have access to everything defined in the calling shell, yet would not modify it (unless perhaps with export and such commands)?

Writing (source myscript.sh) seems to be doing what I am after; is this the right way to go about it? Is there an equivalent shebang that would yield the same behavior by calling ./myscript.sh instead?

Jonathan H
  • 2,373

2 Answers2

17
(. ./myscript.sh)

is the right way (the standard form is ., not source, and . with no directory specified searches on the PATH).

Doing this using a shebang would require having a reliable way of finding the binary for the shell running the parent script, without relying on environment variables. On Linux, one might imagine a /proc/parent similar to /proc/self, pointing to the parent process; then one could write

#! /proc/parent/exe -

to have a shell script use whatever binary is running the parent. (This would work even if the original shell were deleted or replaced; /proc/self/exe can be used even if the binary is deleted, and /proc/parent/exe could be made to behave in the same way.) But blindly relying on the parent process’ binary to be able to run a script is itself unreliable (there are races if the parent dies while the child is starting).

Stephen Kitt
  • 434,908
3

POSIX (IEEE Std 1003.1-2017, Section 2.12) specifies:

A subshell environment shall be created as a duplicate of the shell environment, except that signal traps that are not being ignored shall be set to the default action. Changes made to the subshell environment shall not affect the shell environment.

So you are correct that a subshell would have access to everything in the calling shell, but incorrect about commands such as export being able to modify the parent shell's environment.

And of course, the format is also specified by POSIX as follows:

( compound-list )
    Execute compound-list in a subshell environment; ...

Is there an equivalent shebang that would yield the same behavior by calling ./myscript.sh instead?

Assuming you have a shebang present in your script. It will be run using whatever interpreter you specified (e.g. bash), but as a separate process, rather than as a subshell. But this would still achieve your desired result of having a script running that would not modify the parent shells environment.

$ cat script.sh
#!/usr/bin/env bash
VAR=modified
echo "VAR is $VAR"
echo "PID: $$"

$ echo "Parent: $$" Parent: 793915

$ VAR=initial $ ./script.sh VAR is modified PID: 799149

$ echo $VAR initial

$ bash script.sh VAR is modified PID: 799784

$ echo $VAR initial

$ . script.sh VAR is modified PID: 793915

$ echo $VAR modified

Note: The dot . command is equivalent to source.


Update: Forgot to address the fact that running as a script would not export any environment variables over, unless the set -a option is set. Though keep in mind that only variables assigned after the option is on would be exported. If this is not something you desire, then your best bet would be on (. script.sh)

From the manual:

-a

When this option is on, the export attribute shall be set for each variable to which an assignment is performed

$ cat script.sh
#!/usr/bin/env bash
echo "VAR is $VAR"
VAR=modified
echo "VAR now set to $VAR"

$ VAR=original $ (. script.sh) VAR is original VAR now set to modified $ echo $VAR original $ # works as expected, unchanged

$ ./script.sh VAR is VAR now set to modified $ # var not sent over

$ set -a $ VAR=original $ ./script.sh VAR is original VAR now set to modified $ echo $VAR original $ # works, and your variable remains unchanged $ set +a # turn the option off

  • 1
    This explains the difference between sourcing and executing a script, but I don't think it answers my question :) – Jonathan H Jan 05 '23 at 04:52
  • My bad, I didn't read your question that carefully! I see that I didn't address the fact that executing a script wouldn't export over all the variables (which can be done with set -a), let me update my answer. – Zaidhaan Hussain Jan 05 '23 at 05:53