What is the difference between "source x", ". x" and "./x" in Bash?

I have one bash source run.sh as follows,

#!/bin/bash
if [ $# -ne 1 ]; then
    exit
fi
...

when I execute it in two ways, there are different behaviors. The first way is,

source run.sh

It will close the terminal after execution. The second way is,

./run.sh

this will simply finish running the script, and stay on the terminal. I am asking if there is a command for exiting a bash scripts for both source run.sh and ./run.sh execution. I have tried return too, which does not work well under ./run.sh execution.

More generally, I am interested in why this is happening, and what's difference between using "source" and "." for script execution?


Solution 1:

Before answering, I think some clarifications are needed. Let's analyze the following three lines:

source run.sh
. run.sh
./run.sh

The first two lines are exactly identical: . is in fact an alias for source. What source does is executing the shell script in the current context, hence a call to exit will quit the shell.

The third line (which is the one that confuses you) has however nothing to do with the other lines. ./run.sh is just a path, and is the same as (for example) /home/user/run.sh or /usr/bin/something. Always remember that commands in the shell are separated by a space. So, in this case, the command is not ., but is ./run.sh: this means that a sub-shell will be executed and that the exit will have effect just to the sub-shell.

Solution 2:

Three ways:

You can enclose the script in a function and only use return.

#!/usr/bin/env bash
main() {
    ...
    return 1
    ...
}
main "$@"

You can test if the script is being sourced by an interactive shell.

if [[ $- = *i* ]]; then
    return 1
else
    exit 1
fi

You can try to return, and if it fails, exit.

return 1 2>/dev/null || exit 1

Solution 3:

Think of the command 'source' as in 'include' statement. It takes the content of the argument and runs it as though it were ran directly. In this case your command is 'source' with an argument of 'run.sh' and run.sh is executed exactly as though you had typed the content of run.sh into your command line.

When you run './run.sh', './run.sh' is your command and it has no arguments. Since this file is plain-text and not binary, your shell looks for an interpreter at the shebang ('#!' on the first line) and finds '/bin/bash'. So your shell then starts a new instance of bash and the content of run.sh is run inside this new instance.

In the first instance, when bash reaches the 'exit' command it is executed exactly as though you had typed it into the command line. In the second instances it is executed in bash process your shell started, thus only this instance of bash receives an 'exit' command.

When you type a line into bash, anything before the first space is treated as a command and anything that follows are treated as arguments. The command '.' is an alias of 'source'. When you run '. run.sh' the '.' is a command on it's own as it is separated from it's arguments by a space. When you run './run.sh' your command is './run.sh' and '.' is part of the relative path to run.sh with the '.' representing your current folder.