Skip to content

Shell Script #3 - The Execution Logic of Shell Scripts

Shell scripts are essentially text files. They consist of a series of instructions that are not compiled. These instructions may be built-in shell commands (type, cd, echo, etc.), or system-installed programs (ls, dpkg, ping, etc.).

A common belief is that the commands written in the shell, when written in a text file and turned into a script, will be executed exactly the same way. This is often true, but not always.

The GitHub repository where I share code examples: Shell Scripting 101

Parent and child shells

We can illustrate the working process of a shell script as follows:

  1. The command is read.
  2. The read command is searched.
    1. Is the command a shell built-in? (built-in, internal)
      1. If it is built-in, the relevant code is executed and the next command is processed.
    2. If the command is not a shell built-in, but an external program, a subprocess is created (Fork. This is very important. It will be needed later.). The parent process waits during this time.
      1. The kernel loads the called program into the CPU and runs it.
      2. The subprocess completes its task and stops. The parent process (shell) resumes.
    3. If the command is neither internal nor external, an error is returned. (Here, all loop controls, decision structures, etc. can be considered as internal commands.)
  3. If the end of the script is not reached, the entire process starts over. After the last command is executed, the script ends.

Now, let's try a test and touch on some points that we think might surprise us.

I have a shell open, GNU Bash. We will give it some commands and observe its responses:

[root@localhost bashscript]# echo $deneme

[root@localhost bashscript]# deneme="Ali"
[root@localhost bashscript]# echo $deneme
Ali

We try to print the "deneme" variable. It returns an empty value because the variable doesn't have a value yet. Then, we assign the value "Ali" to the "deneme" variable and can print its value.

There is no problem up to here. Now, let's try to do this by turning it into a script:

[root@localhost bashscript]# vi degisken.sh
[root@localhost bashscript]# chmod 744 degisken.sh
[root@localhost bashscript]# cat degisken.sh
#!/bin/bash
echo $degisken_kapsami

degisken_kapsami="Test değeri"

echo $degisken_kapsami
[root@localhost bashscript]# ./degisken.sh

Test değeri
[root@localhost bashscript]# echo $degisken_kapsami

[root@localhost bashscript]# echo $deneme
Ali

Let's examine it line by line:

  1. We create the file using the vi editor.
  2. We specify the necessary permissions to run it. (744 = rwxr--r--)
  3. We read the content of the script with cat.
    1. First, the value of the "degisken_kapsami" variable was intended to be printed.
    2. Then, a value was assigned to this variable.
    3. The value of the "degisken_kapsami" variable was printed again.
  4. We run the script.
  5. An empty value is returned because no value had been assigned to the variable yet.
  6. "Test değeri" is printed to the screen. This was the value we assigned to the variable.
  7. We want to print the value of the "degisken_kapsami" variable.
  8. An empty output is returned.
  9. We want to print the value of the "deneme" variable defined in the previous example.
  10. The value "Ali" is printed without issues.

So why is this happening? A variable that we define by typing into the shell is remembered by the shell. Its value is stored. However, a variable defined within a script becomes inaccessible after the script is executed.

Didn't we turn things we write in the shell into a shell script when we save them to a file?

We will do almost the same thing as above. We will only update the part where we run the script from "./degisken.sh" to ". ./degisken.sh":

[root@localhost bashscript]# . ./degisken.sh

Test değeri
[root@localhost bashscript]# echo $degisken_kapsami
Test değeri
[root@localhost bashscript]# echo $deneme
Ali

As expected, first an empty value, then "Test değeri" was printed. However, unlike the previous example, after the script ended, we were still able to read the value of the "degisken_kapsami" variable.

Scripts Run with a Dot (.) in Linux

The explanation for this topic is quite simple. It touches on the point I mentioned earlier as "this is important."

You have a shell open in front of you. Bash. A program. Since it's running, it is also a process. You give it commands. You teach it variables and want it to return their values.

Then, with a command like "./degisken.sh", you point to an executable file. Your main process looks at the file. It sees the shebang. "I need to run this with /bin/bash," it says, and forks a new subshell to run the script. All the commands inside the file are executed within the subshell. When there are no more commands to execute, the child process exits, and the parent process continues where it left off. In other words, it brings the prompt back to you and waits for the next command.

In summary, your script is not being run by the shell you are currently in; it is being run by a new shell that your current shell spawned. Variables are defined within that subshell. The outputs are given by it. Once the script finishes, the subshell dies. Therefore, all the definitions are lost.

The reason why the command to run the script is prefixed with a "dot (.)" is that it tells our shell: "You will run this file, not a new process. Don't fork a new process for this, do it yourself."

About Processes

To learn more about processes at the operating system level, you can check out this article: Operating System 101 - #4 (Process)

Shell vs. Subshell

Let's try to illustrate the topic with an example. What happens when I run the "ps" command in the shell?

According to the flow we summarized earlier, since this is an external command, Bash will fork a new process. Inside this process, it will exec the "ps" program.

When we list the processes to show the parent-child relationship, our output looks like this:

[root@localhost bashscript]# ps --forest
  PID TTY          TIME CMD
 1409 pts/0    00:00:00 bash
 1850 pts/0    00:00:00  \_ ps

As seen, the "ps" program, which gives the output, was executed as a child process of Bash.

Continuing with the examples:

[root@localhost bashscript]# sleep 10000 &
[1] 1852
[root@localhost bashscript]# ps --forest
  PID TTY          TIME CMD
 1409 pts/0    00:00:00 bash
 1852 pts/0    00:00:00  \_ sleep
 1853 pts/0    00:00:00  \_ ps

In the first command, we run a "sleep" command that we want to execute in the background. When we look at the tree, we see that the shell we're using has two child processes: sleep and ps.

Now, let's write this command inside a shell script. Let's switch to another shell and run "ps":

[root@localhost bashscript]# cat sleep.sh
#!/bin/bash

sleep 10000

ps output looks like this:

[root@localhost ~]# ps a --forest
  PID TTY      STAT   TIME COMMAND
 2097 pts/1    Ss     0:00 -bash
 2134 pts/1    R+     0:00  \_ ps a --forest
 1409 pts/0    Ss     0:00 -bash
 2131 pts/0    S+     0:00  \_ /bin/bash ./sleep.sh
 2132 pts/0    S+     0:00      \_ sleep 10000
  956 tty1     Ss+    0:00 -bash

The bash with PID 956 at the bottom is the shell of the virtual machine itself. It's open and waiting. The bash with PID 2097 is the new SSH connection I opened (remember when I said let's switch to another shell?). The process with PID 2134 under 2097 is the "ps" process that gave us the output above.

There's also a bash with PID 1409. That's our first SSH connection. It has run "/bin/bash," just like it says in the shebang. The bash inside it has called the "sleep" program.

Isn't it a nice topic? :)