接触过一些 shell 脚本,做服务端运维时也时常用到,是时候专门学习一下了。

基础

Here Script

使用 _EOF_ 将多行语句作为单句,避免转义字符的麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# echos 
echo "<html>"
echo "<head>"
echo "</head>"
echo "</html>"

echo "<html>
<head>
</head>
</html>"

cat << _EOF_
<html>
<head>
</head>
</html>
_EOF_

Variables

Rules:

  1. Names must start with a letter.
  2. A name must not contain embedded spaces. Use underscores instead.
  3. You cannot use punctuation marks.
1
2
3
#!/bin/bash
title="System Information for"
echo $title

Environment Variables

脚本文件启动前,系统已预设一些环境变量,在命令行中使用 printenv 查看这些变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@bhc004 ~]# printenv
XDG_SESSION_ID=87
HOSTNAME=bhc004
TERM=linux
SHELL=/bin/bash
HISTSIZE=10000
SSH_CLIENT=125.117.180.232 57544 22
SSH_TTY=/dev/pts/0
USER=root
LS_COLORS=rs=0:di=01;34:ln=01;``````.spx=01;36:*.xspf=01;36:
MAIL=/var/spool/mail/root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/jdk/bin
PWD=/root
LANG=en_US.UTF-8
HISTCONTROL=ignoredups
SHLVL=1
HOME=/root
LOGNAME=root
SSH_CONNECTION=125.117.180.232 57544 192.168.0.182 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/0
HISTTIMEFORMAT=%F %T root
_=/usr/bin/printenv

在 shell 中可以使用这些环境变量 echo $HOSTNAME

命令替换和常量

Command Substitution and Constants

命令替换

1
echo Updated on $(date +"%x %r %Z") by $USER

$(date +"%x %r %Z")$() 告诉 shell 替换附带命令的执行结果,即插入 date +"%x %r %Z 的执行结果(当前时间)。

Be aware that there is an older, alternate syntax for “$(command)” that uses the backtick character “`“. This older form is compatible with the original Bourne shell (sh). I tend not to use the older form since I am teaching modern bash here, not sh, and besides, I think backticks are ugly. The bash shell fully supports scripts written for sh, so the following forms are equivalent:

$(command) `command`

把命令执行结果赋值给变量:

1
right_now=$(date +"%x %r %z")

常量

1
2
RIGHT_NOW=$(date +"%x %r %Z")
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

很少用,使用大写字母定义常量名(非强制)。

函数

自定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

#### Functions
show_uptime() {
echo "<h2>System uptime</h2>"
echo "<pre>"
uptime
echo "</pre>"
}

drive_space() {
echo "<h2>Filesystem space</h2>"
echo "<pre>"
df
echo "</pre>"
}

cat << _EOF_
$(show_uptime)
$(drive_space)
_EOF_

函数参数

1
2
3
4
function generate_instance_conf() {
echo "configuring server $1"
}
generate_instance_conf server1

输出:

1
configuring server server1

date 格式化

Following are the list of available options for date command :

Format option Part of Date Desciption Example Output
date +%a Weekday Name of weekday in short (like Sun, Mon, Tue, Wed, Thu, Fri, Sat) Mon
date +%A Weekday Name of weekday in full (like Sunday, Monday, Tuesday) Monday
date +%b Month Name of Month in short (like Jan, Feb, Mar ) Jan
date +%B Month Month name in full short (like January, February) January
date +%d Day Day of month (e.g., 01) 04
date +%D MM/DD/YY Current Date; shown in MM/DD/YY 02/18/18
date +%F YYYY-MM-DD Date; shown in YYYY-MM-DD 2018-01-19
date +%H Hour Hour in 24-hour clock format 18
date +%I Hour Hour in 12-hour clock format 10
date +%j Day Day of year (001..366) 152
date +%m Month Number of month (01..12) (01 is January) 05
date +%M Minutes Minutes (00..59) 52
date +%S Seconds Seconds (00..59) 18
date +%N Nanoseconds Nanoseconds (000000000..999999999) 300231695
date +%T HH:MM:SS Time as HH:MM:SS (Hours in 24 Format) 18:55:42
date +%u Day of Week Day of week (1..7); 1 is Monday 7
date +%U Week Displays week number of year, with Sunday as first day of week (00..53) 23
date +%Y Year Displays full year i.e. YYYY 2018
date +%Z Timezone Time zone abbreviation (Ex: IST, GMT) IST

You may use any of the above-mentioned format options (first column) for the date command in the aforementioned syntax.

流控制

if

if 语法如下:

1
2
3
4
5
6
7
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi

Exit Status

退出状态,即命令执行后会给系统一个值,0-255,0 代表 success:

1
2
3
4
5
6
[root@bhc004 ~]# true
[root@bhc004 ~]# echo $?
0
[root@bhc004 ~]# false
[root@bhc004 ~]# echo $?
1

test

1
2
3
4
# First form
test expression
# Second form
[ expression ]

test 命令和 if 命令一起完成 true / false 判断,当表达式为 true,test 以 0 退出;为 false,test 以 1 退出。

例如:

1
2
3
4
5
if [ -f .bash_profile ]; then
echo "You have a .bash_profile. Things are fine."
else
echo "Yikes! You have no .bash_profile!"
fi

表达式“-f .bash_profile”表示 .bash_profile 是一个文件,若为 true,则执行 then 后的命令;否则执行 else 后的命令。

test 可以评估的表达式如下:

Expression Description
-d file True if file is a directory.
-e file True if file exists.
-f file True if file exists and is a regular file.
-L file True if file is a symbolic link.
-r file True if file is a file readable by you.
-w file True if file is a file writable by you.
-x file True if file is a file executable by you.
file1 -nt file2 True if file1 is newer than (according to modification time) file2
file1 -ot file2 True if file1 is older than file2
-z string True if string is empty.
-n string True if string is not empty.
string1 = string2 True if string1 equals string2.
string1 != string2 True if string1 does not equal string2.

不同的书写格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Alternate form
if [ -f .bash_profile ]
then
echo "You have a .bash_profile. Things are fine."
else
echo "Yikes! You have no .bash_profile!"
fi

# Another alternate form
if [ -f .bash_profile ]
then echo "You have a .bash_profile. Things are fine."
else echo "Yikes! You have no .bash_profile!"
fi

exit

exit 命令可以立即结束此脚本,并设置退出状态:

1
exit 0

test for superuser

id 命令可以查看当前用户信息:

1
2
3
4
[root@bhc004 ~]# id
uid=0(root) gid=0(root) groups=0(root)
[root@bhc004 ~]# id -u
0
1
2
3
4
5
6
7
8
if [ $(id -u) != "0" ]; then
# >&2 输出到标准错误
echo "You must be the superuser to run this script" >&2
# 1 向操作系统表示脚本执行不成功
exit 1
else
echo "superuser"
fi

Watching your script

在第一行加 +x 监控脚本执行状态:

1
#!/bin/bash -x

或者,使用 set -xset +x监控一段代码:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

number=1

set -x
if [ $number = "1" ]; then
echo "Number equals 1"
else
echo "Number does not equal 1"
fi
set +x

键盘输入和算术

用户交互。

read

从键盘输入并赋值给变量:

1
2
3
4
5
#!/bin/bash

echo -n "Enter some text > "
read text
echo "You entered: $text"

-n 使输入和字符串在同一行。read 有 -t-s 等参数:

  • -t 表示在指定时间内获得响应,当无论用户是否输入都要继续执行时使用;
  • is 表示不显示输入的内容;当输入密码时使用;
1
2
3
4
5
6
7
8
#!/bin/bash

echo -n "Hurry up and type something! > "
if read -t 3 response; then
echo "Great, you made it in time!"
else
echo "Sorry, you are too slow!"
fi

算术

使用双括号计算算术表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

first_num=0
second_num=0

echo -n "Enter the first number --> "
read first_num
echo -n "Enter the second number -> "
read second_num

echo "first number + second number = $((first_num + second_num))"
echo "first number - second number = $((first_num - second_num))"
echo "first number * second number = $((first_num * second_num))"
echo "first number / second number = $((first_num / second_num))"
echo "first number % second number = $((first_num % second_num))"
echo "first number raised to the"
echo "power of the second number = $((first_num ** second_num))"

括号内的变量不需要 $ 就可以引用,

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

number=0

echo -n "Enter a number > "
read number

echo "Number is $number"
if [ $((number % 2)) -eq 0 ]; then
echo "Number is even"
else
echo "Number is odd"
fi

流控制2

更过分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

echo -n "Enter a number between 1 and 3 inclusive > "
read character
if [ "$character" = "1" ]; then
echo "You entered one."
elif [ "$character" = "2" ]; then
echo "You entered two."
elif [ "$character" = "3" ]; then
echo "You entered three."
else
echo "You did not enter a number between 1 and 3."
fi

使用 case 优化分支结构

case

case 语句格式如下:

1
2
3
case word in
patterns ) commands ;;
esac

改造如上语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

echo -n "Enter a number between 1 and 3 inclusive > "
read character
case $character in
1 ) echo "You entered one."
;;
2 ) echo "You entered two."
;;
3 ) echo "You entered three."
;;
* ) echo "You did not enter a number between 1 and 3."
esac

case 也可以匹配表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

echo -n "Type a digit or a letter > "
read character
case $character in
# Check for letters
[[:lower:]] | [[:upper:]] ) echo "You typed the letter $character"
;;

# Check for digits
[0-9] ) echo "You typed the digit $character"
;;

# Check for anything else
* ) echo "You did not type a letter or a digit"
esac

* 通常用来检测无效输入。

循环

while

1
2
3
4
5
6
7
#!/bin/bash

number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done

util

1
2
3
4
5
6
7
#!/bin/bash

number=0
until [ "$number" -ge 10 ]; do
echo "Number = $number"
number=$((number + 1))
done

自定义菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

selection=
until [ "$selection" = "0" ]; do
echo "
PROGRAM MENU
1 - Display free disk space
2 - Display free memory

0 - exit program
"
echo -n "Enter selection: "
read selection
echo ""
case $selection in
1 ) df ;;
2 ) free ;;
0 ) exit ;;
* ) echo "Please enter 1, 2, or 0"
esac
done

位置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

echo "Positional Parameters"
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3

if [ "$1" != "" ]; then
echo "Positional parameter 1 contains something"
else
echo "Positional parameter 1 is empty"
fi

命令行选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interactive=
filename=~/sysinfo_page.html

while [ "$1" != "" ]; do
case $1 in
-f | --file ) shift
filename=$1
;;
-i | --interactive ) interactive=1
;;
-h | --help ) usage
exit
;;
* ) usage
exit 1
esac
shift
done

shift

shift 是内建命令,每次调用 shift 使所有参数的索引减一,即 $2 变成 $1,$3 变成 $2:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

echo "You start with $# positional parameters"

# Loop until all parameters are used up
while [ "$1" != "" ]; do
echo "Parameter 1 equals $1"
echo "You now have $# positional parameters"

# Shift all the parameters down by one
shift

done

命令行处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/bin/bash

# sysinfo_page - A script to produce a system information HTML file

##### Constants

TITLE="System Information for $HOSTNAME"
RIGHT_NOW=$(date +"%x %r %Z")
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

##### Functions

system_info()
{
echo "<h2>System release info</h2>"
echo "<p>Function not yet implemented</p>"

} # end of system_info


show_uptime()
{
echo "<h2>System uptime</h2>"
echo "<pre>"
uptime
echo "</pre>"

} # end of show_uptime


drive_space()
{
echo "<h2>Filesystem space</h2>"
echo "<pre>"
df
echo "</pre>"

} # end of drive_space


home_space()
{
# Only the superuser can get this information

if [ "$(id -u)" = "0" ]; then
echo "<h2>Home directory space by user</h2>"
echo "<pre>"
echo "Bytes Directory"
du -s /home/* | sort -nr
echo "</pre>"
fi

} # end of home_space


write_page()
{
cat <<- _EOF_
<html>
<head>
<title>$TITLE</title>
</head>
<body>
<h1>$TITLE</h1>
<p>$TIME_STAMP</p>
$(system_info)
$(show_uptime)
$(drive_space)
$(home_space)
</body>
</html>
_EOF_

}

usage()
{
echo "usage: sysinfo_page [[[-f file ] [-i]] | [-h]]"
}


##### Main

interactive=
filename=~/sysinfo_page.html

while [ "$1" != "" ]; do
case $1 in
-f | --file ) shift
filename=$1
;;
-i | --interactive ) interactive=1
;;
-h | --help ) usage
exit
;;
* ) usage
exit 1
esac
shift
done


# Test code to verify command line processing

if [ "$interactive" = "1" ]; then
response=

echo -n "Enter name of output file [$filename] > "
read response
if [ -n "$response" ]; then
filename=$response
fi

if [ -f $filename ]; then
echo -n "Output file exists. Overwrite? (y/n) > "
read response
if [ "$response" != "y" ]; then
echo "Exiting program."
exit 1
fi
fi
fi
echo "output file = $filename"


# Write page (comment out until testing is complete)

# write_page > $filename

for 循环

处理位置参数

for 命令可以处理命令行参数列表:

1
2
3
4
5
#!/bin/bash

for i in "$@"; do
echo $i
done

处理文件列表

比较两个文件夹中的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash

# cmp_dir - program to compare two directories

# Check for required arguments
if [ $# -ne 2 ]; then
echo "usage: $0 directory_1 directory_2" 1>&2
exit 1
fi

# Make sure both arguments are directories
if [ ! -d $1 ]; then
echo "$1 is not a directory!" 1>&2
exit 1
fi

if [ ! -d $2 ]; then
echo "$2 is not a directory!" 1>&2
exit 1
fi

# Process each file in directory_1, comparing it to directory_2
missing=0
for filename in $1/*; do
fn=$(basename "$filename")
if [ -f "$filename" ]; then
if [ ! -f "$2/$fn" ]; then
echo "$fn is missing from $2"
missing=$((missing + 1))
fi
fi
done
echo "$missing files missing"

home_space

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
home_space()
{
echo "<h2>Home directory space by user</h2>"
echo "<pre>"
format="%8s%10s%10s %-s\n"
printf "$format" "Dirs" "Files" "Blocks" "Directory"
printf "$format" "----" "-----" "------" "---------"
if [ $(id -u) = "0" ]; then
dir_list="/home/*"
else
dir_list=$HOME
fi
for home_dir in $dir_list; do
total_dirs=$(find $home_dir -type d | wc -l)
total_files=$(find $home_dir -type f | wc -l)
total_blocks=$(du -s $home_dir)
printf "$format" $total_dirs $total_files $total_blocks
done
echo "</pre>"

} # end of home_space

使用 printf 格式化输出,参考:

其它

鲁棒性

提高 shell 脚本的鲁棒性,处理 exit status,否则会发生意想不到的事情。

1
2
cd $some_directory
rm *

若 $somedirectory 不存在,进入指定目录失败,则删除当前工作文件夹下的所有文件,--

clean_up 功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

# Program to print a text file with headers and footers

TEMP_FILE=/tmp/printfile.txt

clean_up() {

# Perform program exit housekeeping
rm $TEMP_FILE
exit
}

trap clean_up SIGHUP SIGINT SIGTERM

pr $1 > $TEMP_FILE

echo -n "Print file? [y/n]: "
read
if [ "$REPLY" = "y" ]; then
lpr $TEMP_FILE
fi
clean_up

结束一个脚本时,应清理一些文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/bin/bash

# Program to print a text file with headers and footers

# Usage: printfile file

# Create a temporary file name that gives preference
# to the user's local tmp directory and has a name
# that is resistant to "temp race attacks"

if [ -d "~/tmp" ]; then
TEMP_DIR=~/tmp
else
TEMP_DIR=/tmp
fi
TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM
PROGNAME=$(basename $0)

usage() {

# Display usage message on standard error
echo "Usage: $PROGNAME file" 1>&2
}

clean_up() {

# Perform program exit housekeeping
# Optionally accepts an exit status
rm -f $TEMP_FILE
exit $1
}

error_exit() {

# Display error message and exit
echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
clean_up 1
}

trap clean_up SIGHUP SIGINT SIGTERM

if [ $# != "1" ]; then
usage
error_exit "one file to print must be specified"
fi
if [ ! -f "$1" ]; then
error_exit "file $1 cannot be read"
fi

pr $1 > $TEMP_FILE || error_exit "cannot format file"

echo -n "Print file? [y/n]: "
read
if [ "$REPLY" = "y" ]; then
lpr $TEMP_FILE || error_exit "cannot print file"
fi
clean_up

安全的临时文件

/tmp 会有很多程序放置临时文件在里面,文件名重复会覆盖内容(可预测,predictable);

建议创建本地临时文件夹 ~/tmp,即用户目录下的子目录;

1
TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM
  • $TEMP_DIR 是 /tmp~/tmp
  • printfile 是应用程序的名字
  • $$ 是 shell 变量,将 process id(pid) 嵌入文件名
  • $RANDOM 是追加随机数

这样就生成了唯一且不易重复的文件名,例如 tomcat

1
2
3
4
5
tomcat.5141431142496395497.9000
tomcat.6486338835837423954.9000
tomcat.8674130370990688323.9000
tomcat-docbase.2197772788338189182.9000
tomcat-docbase.30706145223021005.9000

参考

http://linuxcommand.org/lc3_writing_shell_scripts.php

(完)