As developers, it’s important to know the languages and tools we use. Also, becoming acquainted with the behavior of our system is essential for day-to-day activities. This knowledge enables us to effectively predict the outcomes of our operations.
Shell scripts, in particular, can be seemingly unpredictable since most shell languages often work differently than other interpreted languages.
In this tutorial, we’ll look at what happens when we edit a shell script while it’s running. In particular, we’ll learn how to make changes successfully and how to avoid affecting the execution while editing the source code.
The commands presented here are written with the Bash shell in mind, so they might not work with other shells.
2. Editing a Shell Script During Execution
2.1. Editing Script With Text Editors
Let’s see how changing the argument of an echo command affects our output.
First, we check our initial script with the cat command and execute it:
$ cat script.sh #!/bin/bash echo 1 sleep 10 echo 2 sleep 10 echo 3 sleep 10 $ bash script.sh
$ nano script.sh
We modify the text and press Ctrl+X, followed by Y to save the file and exit.
Let’s now check what the output of the script is in the first terminal:
1 20 3
As we can see, our modification was successful and we managed to change the value.
Now, let’s try to revert the change with the vim text editor. First, we rerun the script:
$ bash script.sh
While the script is sleeping, we use vim to edit the 20 back to a 2:
$ vim script.sh
To do this, we press I (Insert) to start editing. After we’re done, we press Esc, followed by :, W, and Q. The : is used to enter commands, the W to save the file (write) and the Q to quit the editor.
Let’s analyze the output of the script to see if our change was successful:
1 20 3
As we can see, this time the output didn’t reflect the changes. Although we changed the file, Bash still executed the old version.
2.2. Differences in Text Editors
This happens because of the different ways the editors handle the file saving operation:
- nano overwrites the file
- vim actually creates a new file
This can be verified through the inode number, which changes after saving with vim but not nano.
inodes are data structures where file information is stored. For example, permissions, the storage device, and the location of the file data are all part of an inode.
Therefore, editing the file with vim has the same outcome as deleting the original file and creating a new one with the modified contents. When we delete the file, the script continues to execute as it can still access the inode.
3. Modifying a Script Without Errors During Execution
We’ve seen the reason why different tools have different effects on the output of the script. To make sure our changes affect the output during execution, we should use tools that overwrite the file while preserving the inode instead of creating a new one.
However, the place where we make our edits within the script also matters for our modifications to have the desired effects.
3.1. Editing the Current Line of Execution
Previously, we managed to affect the output with small changes to future commands or values. Nonetheless, the outcome can be different when we make a more complex change – changing the instruction that’s currently executing may present challenges.
For example, let’s look at what happens when we replace our script with a different one. Here’s the new script, script_new.sh:
$ cat script_new.sh #!/bin/bash echo "start" echo 1 sleep 10 echo 2 sleep 10 echo 3 sleep 10
We can see that our new script has a different first instruction. This changed the location of each instruction by shifting them forward.
Now, let’s execute the first script:
$ bash script.sh
After this, we wait while our program executes the first sleep instruction. Meanwhile, in a new terminal window, we use cp to copy the new script to the location of the one we’re currently executing:
$ cp script_new.sh script.sh
Like nano, cp overwrites files instead of creating new ones at a different location.
Let’s look at the output to see what happened:
1 script.sh: line 4: o: command not found 2 3
As we can see, although the program continued executing, there was an error after the first sleep.
3.2. Understanding the Shell Instruction Pointer
The previous error occurred because Bash tried to execute the command o, which didn’t exist in our system. This happened because the shell saves the current edit location in the file by keeping track of the number of characters already interpreted and executed.
Let’s replace the newlines with spaces and look at both scripts side-by-side to see where the error originated:
#!/bin/bash echo 1 sleep 10 echo 2 sleep 10 echo 3 sleep 10 #!/bin/bash echo "start" echo 1 sleep 10 echo 2 sleep 10 echo 3 sleep 10
The shell saved the place in the script where echo 2 was supposed to be executed next. However, as we changed the file, this character number is now in the middle of the echo 1 instruction.
Since the interpreter saved it as the place where the next command resided, it tries to execute o 1, which is the remainder of the echo 1 command. As it fails, it then continues to execute the next instructions properly.
Hence, we have to be careful when changing the lines of the script that come before the next instruction to execute. The instruction pointer of the shell doesn’t change accordingly, which results in errors if the length of the commands isn’t the same.
4. Avoiding the Effects of Script Modifications
If we want to safely edit our scripts without affecting the current execution, there are still some techniques we can use.
4.1. Using Functions
In Bash, commands usually run one at a time. However, there are some structures that group various commands.
In this case, the shell reads the whole structure at once. Any changes made during execution to the commands inside these won’t affect the output, since the function has already been defined. There are different such structures:
Nonetheless, these aren’t ideal, as they change the meaning of the code and in some edits, might still raise errors.
4.2. Using source
By using source, variables manage to retain the values assigned in the script, which can be useful for subsequent instructions.
It works by reading the file and executing the list of commands. In practice, source only reads the file once, which means we can safely edit without making changes to the execution.
Let’s see how we can use source:
$ source script.sh
Alternatively, we can also call the . built-in:
$ . script.sh
Nevertheless, this solution isn’t optimal. Since it runs in the current shell, sourcing a script can cause instability, due to the interactions between variables, code, and current environment.
Thus, isolating the execution in a subshell is a better option. To do this, we use source in a script and execute that:
$ cat source.sh #!/bin/bash source script.sh $ bash source.sh
Although it looks counterintuitive, we take advantage of the isolation of variables and functions, while avoiding the effects of modifications in the output.
In this article, we analyzed how running scripts are affected by edits. We looked at how to modify our script successfully, as well as what errors can arise and how to avoid them. For example, editing commands that haven’t been executed yet is usually easier than changing the instructions that are currently running.
Modifying code during execution is a common occurrence, whether we want to add another feature or fix a bug. While it can be complicated to understand how scripts will behave, we examined several ways of changing them, such as using different editors and manual copying.
Finally, we also learned how to avoid modifications while executing scripts. This can be useful when we want to keep editing the file, without affecting the current execution.