Ubuntu – What are the differences between executing shell scripts using “source file.sh”, “./file.sh”, “sh file.sh”, “. ./file.sh”

bashcommand linescripts

Have a look at the code:

#!/bin/bash
read -p "Eneter 1 for UID and 2 for LOGNAME" choice
if [ $choice -eq 1 ]
then
        read -p "Enter UID:  " uid
        logname=`cat /etc/passwd | grep $uid | cut -f1 -d:`
else
        read -p "Enter Logname:  " logname
fi
not=`ps -au$logname | grep -c bash`
echo  "The number of terminals opened by $logname are $not"

This code is used to find out the number of terminals opened by a user on the same PC.
Now there are two users logged on, say x and y. I am currently logged in as y and there are 3 terminals open in user x. If I execute this code in y using different ways as mentioned above the results are :

$ ./file.sh
The number of terminals opened by x are 3

$ bash file.sh
The number of terminals opened by x are 5

$ sh file.sh
The number of terminals opened by x are 3

$ source file.sh
The number of terminals opened by x are 4

$ . ./file.sh
The number of terminals opened by x are 4

Note: I passed 1 and uid 1000 to all these executables.

Now can you please explain the differences among all these?

Best Answer

The only major difference is between sourcing and executing a script. source foo.sh will source it and all the other examples you show are executing. In more detail:

  1. ./file.sh

    This will execute a script called file.sh that is in the current directory (./). Normally, when you run command, the shell will look through the directories in your $PATH for an executable file called command. If you give a full path, such as /usr/bin/command or ./command, then the $PATH is ignored and that specific file is executed.

  2. ../file.sh

    This is basically the same as ./file.sh except that instead of looking in the current directory for file.sh, it is looking in the parent directory (../).

  3. sh file.sh

    This equivalent to sh ./file.sh, as above it will run the script called file.sh in the current directory. The difference is that you are explicitly running it with the sh shell. On Ubuntu systems, that is dash and not bash. Usually, scripts have a shebang line that gives the program they should be run as. Calling them with a different one overrides that. For example:

    $ cat foo.sh
    #!/bin/bash  
    ## The above is the shebang line, it points to bash
    ps h -p $$ -o args='' | cut -f1 -d' '  ## This will print the name of the shell
    

    That script will simply print the name of the shell used to run it. Let's see what it returns when called in different ways:

    $ bash foo.sh
    bash
    $ sh foo.sh 
    sh
    $ zsh foo.sh
    zsh
    

    So, calling calling a script with shell script will override the shebang line (if present) and run the script with whatever shell you tell it.

  4. source file.sh or . file.sh

    This is called, surprisingly enough, sourcing the script. The keyword source is an alias to the shell builtin . command. This is a way of executing the script within the current shell. Normally, when a script is executed, it is run in its own shell which is different than the current one. To illustrate:

    $ cat foo.sh
    #!/bin/bash
    foo="Script"
    echo "Foo (script) is $foo"
    

    Now, if I set the variable foo to something else in the parent shell and then run the script, the script will print a different value of foo (because it is also set within the script) but the value of foo in the parent shell will be unchanged:

    $ foo="Parent"
    $ bash foo.sh 
    Foo (script) is Script  ## This is the value from the script's shell
    $ echo "$foo"          
    Parent                  ## The value in the parent shell is unchanged
    

    However, if I source the script instead of executing it, it will be run in the same shell so the value of foo in the parent will be changed:

    $ source ./foo.sh 
    Foo (script) is Script   ## The script's foo
    $ echo "$foo" 
    Script                   ## Because the script was sourced, 
                             ## the value in the parent shell has changed
    

    So, sourcing is used in the few cases where you want a script to affect the shell you are running it from. It is typically used to define shell variables and have them available after the script finishes.


With all that in mind, the reason you get different answers is, first of all, that your script does not do what you think it does. It counts the number of times that bash appears in the output of ps. This is not the number of open terminals, it is the number of running shells (in fact, it isn't even that, but that's another discussion). To clarify, I simplified your script a little to this:

#!/bin/bash
logname=terdon
not=`ps -au$logname | grep -c bash`
echo  "The number of shells opened by $logname is $not"

And run it in the various ways with only a single terminal open:

  1. Direct launching, ./foo.sh.

    $ ./foo.sh
    The number of shells opened by terdon is 1
    

    Here, you are using the shebang line. This means that the script is executed directly by whatever is set there. This affects the way that the script is shown in the output of ps. Instead of being listed as bash foo.sh, it will only be shown as foo.sh which means that your grep will miss it. There are actually 3 bash instances running: the parent process, the bash running the script and another one which runs the ps command. This last is important, launching a command with command substitution (`command` or $(command)) results in a copy of the parent shell being launched and that runs the command. Here, however, none of these are shown because of the way that ps shows its output.

  2. Direct launching with explicit (bash) shell

    $ bash foo.sh 
    The number of shells opened by terdon is 3
    

    Here, because you're running with bash foo.sh, the output of ps will show bash foo.sh and be counted. So, here we have the parent process, the bash running the script and the cloned shell (running the ps) all shown because now ps will show each of them because your command will include the word bash.

  3. Direct launching with a different shell (sh)

    $ sh foo.sh
    The number of shells opened by terdon is 1
    

    This is different because you are running the script with sh and not bash. Therefore, the only bash instance is the parent shell where you launched your script. All the other shells mentioned above are being run by sh instead.

  4. Sourcing (either by . or source, same thing)

    $ . ./foo.sh 
    The number of shells opened by terdon is 2
    

    As I explained above, sourcing a script causes it to run in the same shell as the parent process. However, a separate subshell is started to launch the ps command and that brings the total to two.


As a final note, the correct way to count running processes is not to parse ps but to use pgrep. All of these problems would have been avoided had you just run

pgrep -cu terdon bash

So, a working version of your script that always prints the right number is (note the absence of command substitution):

#!/usr/bin/env bash
user="terdon"

printf "Open shells:"
pgrep -cu "$user" bash

That will return 1 when sourced and 2 (because a new bash will be launched to run the script) for all other ways of launching. It will still return 1 when launched with sh since the child process is not bash.

Related Question