Many people use shell scripts to accomplish simple tasks and become part of their lives. Unfortunately, shell scripts can have a very large impact when they run an exception. It is necessary to minimize this type of problem when writing scripts. In this article I'll introduce some of the techniques that make bash scripts robust.
Using Set-u
How many times have you crashed a script because you didn't initialize the variable? For me, many times.
Copy Code code as follows:
Chroot=$1
...
RM-RF $chroot/usr/share/doc
If the code above does not run on the parameters, you will not just delete the document from the chroot, but remove all the documents from the system. What are you supposed to do about it? The good news is that Bash provides set-u, and when you use an uninitialized variable, let bash automatically exit. You can also use a more readable set-o nounset.
Copy Code code as follows:
david% bash/tmp/shrink-chroot.sh
$chroot =
david% Bash-u/tmp/shrink-chroot.sh
/tmp/shrink-chroot.sh:line 3: $1:unbound variable
david%
Using SET-E
The beginning of every script you write should contain set-e. This tells Bash that if any one of the statements returns a value that is not true, it exits bash. The advantage of using-e is to avoid making the error snowball into a serious error and catching the error as soon as possible. More readable version: Set-o errexit
Use-E to liberate you from inspection errors. If you forget to check, bash will do it for you. But there's no way you can use $? To get the command execution status because bash cannot get any return value that is not 0. You can use a different structure:
Copy Code code as follows:
Command
If ["$?" -ne 0]; Then echo "Command failed"; Exit 1; Fi
You can replace it with:
Copy Code code as follows:
Command | | {echo "command failed"; exit 1;}
or use:
Copy Code code as follows:
if! Command Then echo "Command failed"; Exit 1; Fi
If you have to use a command that returns a value other than 0, or are you not interested in the return value? You can use command | | True, or you have a long code, you can turn off error checking temporarily, but I suggest you use it sparingly.
Copy Code code as follows:
Set +e
Command1
Command2
Set-e
The documentation indicates that bash returns the value of the last command in the pipeline by default, perhaps the one you don't want. Like performing false | True will be considered a successful execution of the command. If you want such a command to be considered an execution failure, you can use the Set-o pipefail
Program Defense-Consider unexpected things
Your script may be run under an "unexpected" account, such as a missing file or a directory that is not created. You can do something to prevent these mistakes. For example, when you create a directory, if the parent directory does not exist, the mkdir command returns an error. If you create the directory with the-P option, it will create the desired parent directory before creating the desired directory. mkdir Another example is the RM command. If you want to delete a nonexistent file, it will "spit" and your script will stop working. (Because you use the-e option, right?) You can use the-f option to solve this problem and let the script continue to work when the file does not exist.
Ready to handle spaces in file names
Some people use spaces from file names or command-line arguments, and you need to remember this when you write a script. You need to always remember to surround variables with quotes.
Copy Code code as follows:
When the $filename variable contains a space, it hangs off. You can solve this:
Copy Code code as follows:
If ["$filename" = "foo"];
When using $@ variables, you also need to use quotes, because two of the arguments separated by a space are interpreted as two separate parts.
Copy Code code as follows:
david% foo () {for-I in $@; doing echo $i; done}; Foo bar "Baz Quux"
Bar
Baz
Quux
david% foo () {For I in ' $@ '; do echo $i; Foo bar "Baz Quux"
Bar
Baz Quux
I didn't think of any time when I couldn't use "$@," so when you have questions, there's no mistake in quotes. If you use Find and Xargs at the same time, you should use-print0 to make the character split the file name instead of the line breaks.
Copy Code code as follows:
david% Touch "Foo bar"
david% Find | Xargs ls
LS:./foo:no such file or directory
Ls:bar:No such file or directory
david% find-print0 | xargs-0 ls
./foo Bar
Set traps
When you write a script that hangs out, the filesystem is in an unknown state. such as lock file status, temporary file status, or update a file before updating the next file to hang up. If you can solve these problems, either delete the lock file, or roll back to a known state when the script encounters a problem, you are great. Fortunately, Bash provides a way to run a command or a function when bash receives a UNIX signal. You can use the Trap command.
Copy Code code as follows:
Trap command signal [signal ...]
You can link multiple signals (lists can be obtained using kill-l), but in order to clean up the mess, we use only three of them: Int,term and exit. You can use-as to get the traps back to its original state.
Signal description
Int:interrupt-triggered when someone uses CTRL-C to terminate a script
Term:terminate-triggered when someone kills a script process using kill
Exit:exit-This is a pseudo signal that is triggered when the script exits normally or when it exits because of an error SET-E
When you use a lock file, you can write this:
Copy Code code as follows:
if [!-e $lockfile]; Then
Touch $lockfile
Critical-section
RM $lockfile
Else
echo "Critical-section is already running"
Fi
What happens when the most important part (Critical-section) is running, and if the script process is killed? The lock file is thrown there, and your script will never run until it is deleted. Workaround:
Copy Code code as follows:
if [!-e $lockfile]; Then
Trap "Rm-f $lockfile; Exit INT TERM Exit
Touch $lockfile
Critical-section
RM $lockfile
Trap-int TERM EXIT
Else
echo "Critical-section is already running"
Fi
Now when you kill the process, the lock file is deleted together. Note that the script is explicitly exited in the trap command, or the script will continue to execute the command behind the trap.
State conditions (Wikipedia)
In the example of the lock file above, there is a state condition that has to be pointed out, and it exists between the decision lock file and the creation of the lock file. A viable solution is to redirect to nonexistent files using IO redirection and bash's noclobber (wikipedia) schema. We can do this:
Copy Code code as follows:
if (Set-o noclobber echo "$$" > "$lockfile") 2>/dev/null;
Then
Trap ' rm-f ' "$lockfile"; Exit $? ' INT TERM EXIT
Critical-section
Rm-f "$lockfile"
Trap-int TERM EXIT
Else
echo "Failed to acquire Lockfile: $lockfile"
Echo "held by $ (cat $lockfile)"
Fi
A more complicated problem is that you have to update a lot of files and if you have problems with them during the update process, you can make the script more elegant. You want to make sure that those updates are correct and that there is no change at all. For example, you need a script to add a user.
Copy Code code as follows:
ADD_TO_PASSWD $user
Cp-a/etc/skel/home/$user
Chown $user/home/$user-R
This script can be problematic when there is insufficient disk space or if the process is killed halfway through. In this case, you may want the user account to not exist, and his files should also be deleted.
Copy Code code as follows:
Rollback () {
DEL_FROM_PASSWD $user
If [-e/home/$user]; Then
rm-rf/home/$user
Fi
Exit
}
Trap rollback INT TERM EXIT
ADD_TO_PASSWD $user
Cp-a/etc/skel/home/$user
Chown $user/home/$user-R
Trap-int TERM EXIT
At the end of the script you need to use the trap to close the rollback call, otherwise when the script exits normally rollback will be called, then the script equals nothing.
Remain atomized
Again you need to update a large pile of files in the directory, such as you need to rewrite the URL to another site's domain name. You might write:
Copy Code code as follows:
For file in $ (find/var/www-type f-name "*.html"); Todo
Perl-pi-e ' s/www.example.net/www.example.com/' $file
Done
If you modify to half the script is a problem, part of the use of www.example.com, and the other part of the use of www.example.net. You can use Backup and trap solutions, but your site URLs are inconsistent during the upgrade process.
The solution is to make this change an atomic operation. Make a copy of the data first, update the URL in the copy, and replace the current working version with a copy. You need to verify that the copy and the working version directory are on the same disk partition so that you can take advantage of the Linux system by moving the directory only to update the Inode node that the directory points to.
Copy Code code as follows:
Cp-a/var/www/var/www-tmp
For file in $ (find/var/www-tmp-type-f-name "*.html"); Todo
Perl-pi-e ' s/www.example.net/www.example.com/' $file
Done
Mv/var/www/var/www-old
Mv/var/www-tmp/var/www
This means that if the update process goes wrong, the online system will not be affected. The time that is affected by the online system is reduced to two MV operation time, which is very short, because the file system only updates the inode without actually replicating all the data.
The disadvantage of this technique is that you need twice times the disk space, and those processes that open files for a long time will take a long time to upgrade to a new file version, and it is recommended that you restart these processes after the update is complete. This is not a problem for the Apache server because it will reopen the file each time. You can use the lsof command to view files that are currently open. The advantage is that you have a previous backup that comes in handy when you need to restore it.