This is my moves to write bash programs. There's nothing new here, but from my experience people love to abuse bash. They ignore computer science and create "big clay balls" from their programs: software systems that are not clearly structured.
Here I tell you the method to protect your program from obstacles and keep the code neat.
Global variables that cannot be changed
1. Use global variables as little as possible
2. Name in UPPERCASE
3. Read-only Declaration
4. Use global variables to replace obscure $0,$1, etc.
Global variables that are commonly used in my programs:
Copy Code code as follows:
ReadOnly progname=$ (basename $)
ReadOnly progdir=$ (Readlink-m $ (dirname $))
ReadOnly args= "$@"
Second, everything is part of the
All variables should be local.
Copy Code code as follows:
Change_owner_of_file () {
Local filename=$1
Local user=$2
Local group=$3
Chown $user: $group $filename
}
Change_owner_of_files () {
Local user=$1; Shift
Local group=$1; Shift
Local files=$@
Local I
For I in $files
Todo
Chown $user: $group $i
Done
}
1. Parameters for self-annotation (self documenting)
2. It is important to declare it as a local variable, usually as a cyclic variable i.
3. Local variables are not used for global domains.
Copy Code code as follows:
Kfir@goofy ~ $ local A
Bash:local:can is used in a function
Three, Main ()
1. Helps maintain the local nature of all variables
2. Intuitive functional programming
3. The only global command in the code is: main
Copy Code code as follows:
Main () {
Local files= "/tmp/a/tmp/b"
Local I
For I in $files
Todo
Change_owner_of_file Kfir Users $i
Done
}
Main
Four, everything is a function
The only code that runs globally is:
-Immutable global variable declaration
-Main () function
1. Keep the code neat
2. Process becomes clear
Copy Code code as follows:
Main () {
Local files=$ (ls/tmp | grep pid | grep-v daemon)
}
Copy Code code as follows:
Temporary_files () {
Local dir=$1
LS $dir \
| grep pid \
| Grep-v Daemon
}
Main () {
Local files=$ (TEMPORARY_FILES/TMP)
}
1. The second example is much better. The lookup file is a temporary_files () problem rather than main (). This code is also testable with unit tests of Temporary_files ().
2. If you are sure to try the first example, you will get a hodgepodge of finding temporary files and main algorithms.
Copy Code code as follows:
Test_temporary_files () {
Local dir=/tmp
Touch $dir/a-pid1232.tmp
Touch $dir/a-pid1232-daemon.tmp
Returns "$dir/a-pid1232.tmp" Temporary_files $dir
Touch $dir/b-pid1534.tmp
Returns "$dir/a-pid1232.tmp $dir/b-pid1534.tmp" Temporary_files $dir
}
As you can see, this test does not care about main ().
Five, debugging functions
Run program with-X flag:
Copy Code code as follows:
Debugging only a small piece of code, using Set-x and Set+x, will print debugging information only for the current code that is contained by the Set-x and set +x.
Copy Code code as follows:
Temporary_files () {
Local dir=$1
Set-x
LS $dir \
| grep pid \
| Grep-v Daemon
Set +x
}
Print the name of the function and its parameters:
Copy Code code as follows:
Temporary_files () {
Echo $FUNCNAME $@
Local dir=$1
LS $dir \
| grep pid \
| Grep-v Daemon
}
Call Function:
Copy Code code as follows:
will print to standard output:
Copy Code code as follows:
Vi. Clarity of the code
What does this piece of code do?
Copy Code code as follows:
Main () {
Local dir=/tmp
[[z $dir]] \
&& do_something ...
[[-N $dir]] \
&& do_something ...
[[f $dir]] \
&& do_something ...
[[D-$dir]] \
&& do_something ...
}
Main
Let your code speak:
Copy Code code as follows:
Is_empty () {
Local var=$1
[[Z $var]]
}
Is_not_empty () {
Local var=$1
[[-N $var]]
}
Is_file () {
Local file=$1
[[F $file]]
}
Is_dir () {
Local dir=$1
[[D $dir]]
}
Main () {
Local dir=/tmp
Is_empty $dir \
&& do_something ...
Is_not_empty $dir \
&& do_something ...
Is_file $dir \
&& do_something ...
Is_dir $dir \
&& do_something ...
}
Main
Seven, do one thing per line
Use the backslash \ as a separator. For example:
Copy Code code as follows:
Temporary_files () {
Local dir=$1
LS $dir | grep pid | Grep-v Daemon
}
Can be written much more succinctly:
Copy Code code as follows:
Temporary_files () {
Local dir=$1
LS $dir \
| grep pid \
| Grep-v Daemon
}
Sign at the beginning of the indent
Symbol at the end of the bad example: In this example, the Temporary_files () code snippet, suspected to be affixed incorrectly. In conjunction with the context, it should be print_dir_if_not_empty ())
Copy Code code as follows:
Print_dir_if_not_empty () {
Local dir=$1
Is_empty $dir &&
echo "dir is empty" | | \
echo "Dir= $dir"
}
Good example: we can see clearly the connection between the line and the join symbol.
Copy Code code as follows:
Print_dir_if_not_empty () {
Local dir=$1
Is_empty $dir \
&& echo "dir is empty" \
|| echo "Dir= $dir"
}
Eight, print usage
Don't do this:
Copy Code code as follows:
echo "This prog does: ..."
echo "Flags:"
echo "-H print Help"
It should be a function:
Copy Code code as follows:
Usage () {
echo "This prog does: ..."
echo "Flags:"
echo "-H print Help"
}
Echo repeats on each line. So we got this document:
Copy Code code as follows:
Usage () {
Cat <<-EOF
Usage: $PROGNAME Options
program deletes files from filesystems to release spaces.
It gets config file that define Fileystem paths to work on, and whitelist the rules to
Keep certain files.
OPTIONS:
-C--config configuration file containing the rules. Use--help-config to the syntax.
-N--pretend do not really delete, just i what you are to do.
-T--test run unit test to check the program
-V--verbose verbose. Can specify more then one-v to have more verbose
-X--debug Debug
-H--help Show this Help
--help-config Configuration Help
Examples:
Run All tests:
$PROGNAME--test All
Run Specific test:
$PROGNAME--test test_string.sh
Run:
$PROGNAME--config/path/to/config/$PROGNAME. conf
Just show what your are going to do:
$PROGNAME-vn-c/path/to/config/$PROGNAME. conf
Eof
}
Note that there should be a real tab ' t ' at the beginning of each line.
In Vim, if your tab is 4 spaces, you can use this to replace the command:
Copy Code code as follows:
Ix. command-line parameters
Here is an example that completes the use of the above usage function. I got this code from http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/.
Copy Code code as follows:
CmdLine () {
# Got here:
# http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/
Local arg=
For ARG
Todo
Local delim= ""
Case "$arg" in
#translate--gnu-long-options to-g (short options)
--config) args= "${args}-c";;
--pretend) args= "${args}-n";;
--test) args= "${args}-t";;
--help-config) Usage_config && exit 0;;
--HELP) args= "${args}-h";;
--verbose) args= "${args}-v";;
--debug) args= "${args}-x";;
#pass through anything else
*) [[[' ${arg:0:1} ' = = ']] | | | Delim= "\" "
args= "${args}${delim}${arg}${delim}";;
Esac
Done
#Reset the positional parameters to the short options
Eval Set--$args
While getopts "NVHXT:C:" OPTION
Todo
Case $OPTION in
V
ReadOnly verbose=1
;;
H
Usage
Exit 0
;;
X
ReadOnly debug= '-X '
Set-x
;;
T
run_tests= $OPTARG
Verbose vinfo "Running tests"
;;
C
ReadOnly config_file= $OPTARG
;;
N
ReadOnly pretend=1
;;
Esac
Done
if [[$recursive _testing | |-Z $RUN _tests]]; Then
[[!-f $CONFIG _file]] \
&& Eexit "You must provide--config file"
Fi
return 0
}
You like this, using the immutable args variable we defined on the head:
Copy Code code as follows:
Main () {
CmdLine $ARGS
}
Main
Ten, Unit test
1. It is important in a higher level of language.
2. Use Shunit2 to do unit test
Copy Code code as follows:
Test_config_line_paths () {
Local s= ' partition Cpm-all, 80-90, '
Returns "/A" "Config_line_paths ' $s/A, '"
Returns "/A/B/C" "config_line_paths ' $s/a:/b/c, '"
Returns "/a/b/C" "Config_line_paths ' $s/A:/b:/C, '"
}
Config_line_paths () {
Local partition_line= "$@"
echo $partition _line \
| Csv_column 3 \
| Delete_spaces \
| Column 1 \
| Colons_to_spaces
}
Source/usr/bin/shunit2
Here is another example of using the DF command:
Copy Code code as follows:
Df=df
Mock_df_with_eols () {
Cat <<-EOF
FileSystem 1k-blocks Used Available use% mounted on
/very/long/device/path
124628916 23063572 100299192 19%/
Eof
}
Test_disk_size () {
Returns 1000 "DISK_SIZE/DEV/SDA1"
Df=mock_df_with_eols
Returns 124628916 "Disk_size/very/long/device/path"
}
Df_column () {
Local disk_device=$1
Local column=$2
$DF $disk _device \
| Grep-v ' use% '
| Tr ' \ n ' \
| awk "{print \$ $column}"
}
Disk_size () {
Local disk_device=$1
Df_column $disk _device 2
}
I have an exception here, for the sake of testing, I declared DF to be read-only in the global domain. This is because Shunit2 does not allow changes to global domain functions.