Shell脚本
Shell脚本
一个 shell 脚本就是一个包含一系列命令的普通的文本文件。shell 读取这个文件,然后执行 文件中的所有命令
- 能看懂
- 能改
- 能写
- 能优化

shell是C语言编写的一个二进制程序,Shell 不仅是一个功能强大的命令行接口,也是一个脚本语言解释器。内核认识二进制,shell是命令解释器,
shell 会自动地搜索某些目录。为了最大程度的方便,会把脚本放到这些目录当中。
入门案例
脚本文件格式:
<font style="color:#3C3C3C;">#!</font>字符序列是一种特殊的结构叫做 shebang。- 告诉操作系统将执行此脚本所用的解释器的名字。
- 每个 shell 脚本都应该把这一文本行 作为它的第一行。
#!/bin/bash
# This is our first script.
echo 'Hello World!'
对于脚本文件,有两个常见的权限设置;权限为755的脚本,则每个人都能执行,和权限为700的 脚本,只有文件所有者能够执行。
chmod 755 hello_world
大多数的 Linux 发行版会配置 PATH 变量,让其包含 一个位于用户家目录下的 bin 目录,从而允许用户能够执行他们自己的程序。所以如果我们创建了 一个 bin 目录,并把我们的脚本放在这个目录下,那么这个脚本就应该像其它程序一样开始工作了。如果这个 PATH 变量不包含这个目录,我们能够轻松地添加它,通过在我们的.bashrc 文件中包含下面 这一行文本:
export PATH=~/bin:"$PATH"
# 为了把这个修改应用到当前的终端会话中,必须让 shell 重新读取这个 .bashrc 文件。
# 这可以通过 “sourcing”.bashrc 文件来完成,
# 这个点(.)命令是 source 命令的同义词,一个 shell 内建命令,用来读取一个指定的 shell 命令文件, 并把它看作是从键盘中输入的一样。
. .bashrc
这个 ~/bin 目录是存放为个人所用脚本的好地方。如果我们编写了一个脚本,系统中的每个用户都可以使用它, 那么这个脚本的传统位置是 /usr/local/bin。不管是脚本还是编译过的程序,都应该放到 /usr/local 目录下,
而不是在 /bin 或 /usr/bin 目录。这些目录都是由 Linux 文件系统层次结构标准指定,只包含由 Linux 发行商所提供和维护的文件。
语法
通过使用行继续符(反斜杠-回车符序列)和缩进,这个复杂命令的逻辑会被更清楚地描述给读者。
多行字符串
编写的程序是一个报告生成器。它会显示系统的各种统计数据和它的状态,并将产生 HTML 格式的报告。
#!/bin/bash
# Program to output a system information page
echo "<HTML>
<HEAD>
<TITLE>System Information Report</TITLE>
</HEAD>
<BODY>
<H1>System Information Report</H1>
</BODY>
</HTML>"
补充:在命令行里面,输入多行的时候,一个带引号的字符串可能包含换行符。
变量和常量
https://www.bilibili.com/video/BV1qi4y137NM?p=3
看到这里了,后面再来改。。。没时间现在
- 变量名可由字母数字字符(字母和数字)和下划线字符组成。
- 变量名的第一个字符必须是一个字母或一个下划线。
- 变量名中不允许出现空格和标点符号。
shell 不会 在乎变量值的类型;它把它们都看作是字符串。通过使用带有-i 选项的 declare 命令,你可以强制 shell 把 赋值限制为整数
#!/bin/bash
# Program to output a system information page
title="System Information Report"
# 惯例是指定大写字母来表示常量,小写字母表示真正的变量。
TITLE="System Information Report For $HOSTNAME"
declare -r TITLE=”Page Title”
# -r(只读)选项的内部命令declare来强制常量的不变性.随后所有给 TITLE 的赋值都会被 shell 阻止
CURRENT_TIME=$(date +"%x %r %Z")
TIME_STAMP="Generated $CURRENT_TIME, by $USER"
echo "<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIME_STAMP</P>
</BODY>
</HTML>"
Here Documents与echo
echo 命令通过有引号的字符串可以输入,here document 或者 here script也可以实现echo 的功能。一个 here document 是另外一种 I/O 重定向形式,我们 在脚本文件中嵌入正文文本,然后把它发送给一个命令的标准输入。它这样工作:
command << token
text
token
# command 是一个可以接受标准输入的命令名
# token 是一个用来指示嵌入文本结束的字符串
#!/bin/bash
# Program to output a system information page
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIME_STAMP="Generated $CURRENT_TIME, by $USER"
cat << _EOF_
<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIME_STAMP</P>
</BODY>
</HTML>
_EOF_
取代 echo 命令,现在我们的脚本使用 cat 命令和一个 here document。这个字符串_EOF_(意思是“文件结尾”, 一个常见用法)被选作为 token,并标志着嵌入文本的结尾。注意这个 token 必须在一行中单独出现,并且文本行中 没有末尾的空格。
[me@linuxbox ~]$ foo="some text"
[me@linuxbox ~]$ cat << _EOF_
> $foo
> "$foo"
> '$foo'
> \$foo
> _EOF_
some text
"some text"
'some text'
$foo
正如我们所见到的,shell 根本没有注意到引号。它把它们看作是普通的字符。这就允许在一个 here document 中可以随意的嵌入引号。对于我们的报告程序来说,这将是非常方便的。Here documents 可以和任意能接受标准输入的命令一块使用。
#!/bin/bash
# Script to retrieve a file via FTP
FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/lenny/main/installer-i386/current/images/cdrom
REMOTE_FILE=debian-cd_info.tar.gz
ftp -n << _EOF_
open $FTP_SERVER
user anonymous me@linuxbox
cd $FTP_PATH
hash
get $REMOTE_FILE
bye
_EOF_
ls -l $REMOTE_FILE
如果我们把重定向操作符从 “<<” 改为 “<<-”,shell 会忽略在此 here document 中开头的 tab 字符。 这就能缩进一个 here document,从而提高脚本的可读性:
#!/bin/bash
# Script to retrieve a file via FTP
FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/lenny/main/installer-i386/current/images/cdrom
REMOTE_FILE=debian-cd_info.tar.gz
ftp -n <<- _EOF_
open $FTP_SERVER
user anonymous me@linuxbox
cd $FTP_PATH
hash
get $REMOTE_FILE
bye
_EOF_
ls -l $REMOTE_FILE
shell函数
function name {
commands
return
}
name () {
commands
return
}
#!/bin/bash
# Program to output a system information page
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIME_STAMP="Generated $CURRENT_TIME, by $USER"
report_uptime () {
cat <<- _EOF_
<H2>System Uptime</H2>
<PRE>$(uptime)</PRE>
_EOF_
return
}
report_disk_space () {
cat <<- _EOF_
<H2>Disk Space Utilization</H2>
<PRE>$(df -h)</PRE>
_EOF_
return
}
report_home_space () {
cat <<- _EOF_
<H2>Home Space Utilization</H2>
<PRE>$(du -sh /home/*)</PRE>
_EOF_
return
}
cat << _EOF_
<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIME_STAMP</P>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</BODY>
</HTML>
_EOF_
正如我们所看到的,通过在变量名之前加上单词<font style="color:#3C3C3C;">local</font>,来定义局部变量。这就创建了一个只对其所在的 shell 函数起作用的变量。
#!/bin/bash
foo=0
funct_1 () {
local foo
foo=1
echo "funct_1: foo = $foo"
}
echo "global: foo = $foo"
funct_1
echo "global: foo = $foo"
Shell 函数完美地替代了别名,并且实际上是创建个人所用的小命令的首选方法。别名 非常局限于命令的种类和它们支持的 shell 功能,然而 shell 函数允许任何可以编写脚本的东西。 例如,如果我们喜欢 为我们的脚本开发的这个 report_disk_space shell 函数,我们可以为我们的 .bashrc 文件 创建一个相似的名为 ds 的函数:
Shell 函数中使用位置参数
如果一个包含 shell 函数 file_info 的脚本调用该函数,且带有一个文件名参数,那这个参数会传递给 file_info 函数。这些函数不仅能在脚本中使用,也可以用在 .bashrc 文件中。
位置参数 $0 总是包含命令行中第一项的完整路径名(例如,该程序的名字), 但不会包含这个我们可能期望的 shell 函数的名字。file_info () {
if [[ -e $1 ]]; then
echo -e "\nFile Type:"
file $1
echo -e "\nFile Status:"
stat $1
else
echo "$FUNCNAME: usage: $FUNCNAME file" >&2
return 1
fi
}
read - 从标准输入读取单词
基本上,read 会把来自标准输入的字段赋值给具体的变量。如果没有提供变量名,shell 变量 REPLY 会包含数据行。
read [-options] [variable...]
读取单个参数
#!/bin/bash
echo -n "Please enter an integer -> "
# -n 选项会删除输出结果末尾的换行符,来显示提示信息,然后使用read来读入变量 int 的数值。
read int
if [[ "$int" =~ ^-?[0-9]+$ ]]; then
if [ $int -eq 0 ]; then
echo "$int is zero."
else
if [ $int -lt 0 ]; then
echo "$int is negative."
else
echo "$int is positive."
fi
if [ $((int % 2)) -eq 0 ]; then
echo "$int is even."
else
echo "$int is odd."
fi
fi
else
echo "Input value is not an integer." >&2
exit 1
fi
读取多个参数
#!/bin/bash
# read-multiple: read multiple values from keyboard
echo -n "Enter one or more values > "
read var1 var2 var3 var4 var5
echo "var1 = '$var1'"
echo "var2 = '$var2'"
echo "var3 = '$var3'"
echo "var4 = '$var4'"
echo "var5 = '$var5'"
如果 read 命令接受到变量值数目少于期望的数字,那么额外的变量值为空,而多余的输入数据则会 被包含到最后一个变量中。如果 read 命令之后没有列出变量名,则一个 shell 变量,REPLY,将会包含 所有的输入
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
read 选项
| 选项 | 说明 |
|---|---|
| -a array | 把输入赋值到数组 array 中,从索引号零开始。我们 将在第36章中讨论数组问题。 |
| -d delimiter | 用字符串 delimiter 中的第一个字符指示输入结束,而不是一个换行符。 |
| -e | 使用 Readline 来处理输入。这使得与命令行相同的方式编辑输入。 |
| -n num | 读取 num 个输入字符,而不是整行。 |
| -p prompt | 为输入显示提示信息,使用字符串 prompt。 |
| -r | Raw mode. 不把反斜杠字符解释为转义字符。 |
| -s | Silent mode. 不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这会很有帮助。 |
| -t seconds | 超时. 几秒钟后终止输入。若输入超时,read 会返回一个非零退出状态。 |
| -u fd | 使用文件描述符 fd 中的输入,而不是标准输入。 |
#!/bin/bash
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
#!/bin/bash
if read -t 10 -sp "Enter secret pass phrase > " secret_pass; then
echo "\nSecret pass phrase = '$secret_pass'"
else
echo "\nInput timed out" >&2
exit 1
fi
多个由一个或几个空格 分离开的单词在输入行中变成独立的个体,并被 read 赋值给单独的变量。这种行为由 shell 变量__IFS__ (内部字符分隔符)配置。IFS 的默认值包含一个空格,一个 tab,和一个换行符,每一个都会把 字段分割开。可以调整 IFS 的值来控制输入字段的分离。例如,这个 /etc/passwd 文件包含的数据行 使用冒号作为字段分隔符。通过把 IFS 的值更改为单个冒号,我们可以使用 read 读取 /etc/passwd 中的内容,并成功地把字段分给不同的变量。
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a user name > " user_name
file_info=$(grep "^$user_name:" $FILE)
# grep 命令的输入结果赋值给变量 file_info
# grep 命令使用的正则表达式 确保用户名只会在 /etc/passwd 文件中匹配一行
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
# 这一行由三部分组成:对一个变量的赋值操作,一个带有一串参数的 read 命令,和重定向操作符。
# Shell 允许在一个命令之前给一个或多个变量赋值。这些赋值会暂时改变之后的命令的环境变量。
# 在这种情况下,IFS 的值被改成一个冒号
# <<< 操作符指示一个 here 字符串。一个 here 字符串就像一个 here 文档,只是比较简短,由 单个字符串组成。
# 来自 /etc/passwd 文件的数据发送给 read 命令的标准输入。
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
不能把管道用在 read 上
校正输入
#!/bin/bash
# read-validate: validate input
invalid_input () {
echo "Invalid input '$REPLY'" >&2
exit 1
}
read -p "Enter a single item > "
# input is empty (invalid)
[[ -z $REPLY ]] && invalid_input
# input is multiple items (invalid)
(( $(echo $REPLY | wc -w) > 1 )) && invalid_input
# is input a valid filename?
if [[ $REPLY =~ ^[-[:alnum:]\._]+$ ]]; then
echo "'$REPLY' is a valid filename."
if [[ -e $REPLY ]]; then
echo "And file '$REPLY' exists."
else
echo "However, file '$REPLY' does not exist."
fi
# is input a floating point number?
if [[ $REPLY =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
echo "'$REPLY' is a floating point number."
else
echo "'$REPLY' is not a floating point number."
fi
# is input an integer?
if [[ $REPLY =~ ^-?[[:digit:]]+$ ]]; then
echo "'$REPLY' is an integer."
else
echo "'$REPLY' is not an integer."
fi
else
echo "The string '$REPLY' is not a valid filename."
fi
输出一个菜单
#!/bin/bash
# read-menu: a menu driven system information program
clear
echo "
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"
read -p "Enter selection [0-3] > "
if [[ $REPLY =~ ^[0-3]$ ]]; then
if [[ $REPLY == 0 ]]; then
echo "Program terminated."
exit
fi
if [[ $REPLY == 1 ]]; then
echo "Hostname: $HOSTNAME"
uptime
exit
fi
if [[ $REPLY == 2 ]]; then
df -h
exit
fi
if [[ $REPLY == 3 ]]; then
if [[ $(id -u) -eq 0 ]]; then
echo "Home Space Utilization (All Users)"
du -sh /home/*
else
echo "Home Space Utilization ($USER)"
du -sh $HOME
fi
exit
fi
else
echo "Invalid entry." >&2
exit 1
fi
读取命令行参数
shell 提供了一个称为位置参数的变量集合,这个集合包含了命令行中所有独立的单词。这些变量按照从0到9给予命名
#!/bin/bash
# posit-param: script to view command line parameters
echo "
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"
结果
$ posit-param a b c d
$0 = /home/me/bin/posit-param
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =
获取命令行参数个数
<font style="color:#3C3C3C;">$#</font>,可以得到命令行参数个数的变量
#!/bin/bash
# posit-param: script to view command line parameters
echo "
Number of arguments: $#
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"
$ posit-param a b c d
Number of arguments: 4
$0 = /home/me/bin/posit-param
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =
shift - 访问多个参数的利器
执行一次 shift 命令, 就会导致所有的位置参数 “向下移动一个位置”。
#!/bin/bash
count=1
while [[ $# -gt 0 ]]; do
echo "Argument $count = $1"
count=$((count + 1))
shift
done
每次 shift 命令执行的时候,变量 $2 的值会移动到变量 $1 中,变量 $3 的值会移动到变量 $2 中,依次类推。 变量 $# 的值也会相应的减1。
只要参数个数不为零就会继续执行的 while 循环。 我们显示当前的位置参数,每次循环迭代变量 count 的值都会加1,用来计数处理的参数数量, 最后,执行 shift 命令加载 $1,其值为下一个位置参数的值。这里是程序运行后的输出结果:$ posit-param2 a b c d
Argument 1 = a
Argument 2 = b
Argument 3 = c
Argument 4 = d
PROGNAME 变量。它的值就是 basename $0 命令的执行结果。basename 命令清除一个路径名的开头部分,只留下一个文件的基本名称。程序中,basename 命令清除了包含在 $0 位置参数中的路径名的开头部分,$0 中包含着示例程序的完整路径名。当构建提示信息正如程序结尾的使用信息的时候, basename $0 的执行结果就很有用处。按照这种方式编码,可以重命名该脚本,且程序信息会自动调整为包含相应的程序名称。
#!/bin/bash
# file_info: simple file information program
PROGNAME=$(basename $0)
if [[ -e $1 ]]; then
echo -e "\nFile Type:"
file $1
echo -e "\nFile Status:"
stat $1
else
echo "$PROGNAME: usage: $PROGNAME file" >&2
exit 1
fi
处理集体位置参数
创建一个脚本或 shell 函数,来简化另一个程序的执行。包裹程序提供了一个神秘的命令行选项 列表,然后把这个参数列表传递给下一级的程序。为此 shell 提供了两种特殊的参数。他们二者都能扩展成完整的位置参数列表,但以相当微妙的方式略有不同。
* 和 @ 特殊参数
| 参数 | 描述 |
|---|---|
| $* | 展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候,展开成一个由双引号引起来 的字符串,包含了所有的位置参数,每个位置参数由 shell 变量 IFS 的第一个字符(默认为一个空格)分隔开。 |
| $@ | 展开成一个从1开始的位置参数列表。当它被用双引号引起来的时候, 它把每一个位置参数展开成一个由双引号引起来的分开的字符串。 |
#!/bin/bash
print_params () {
echo "\$1 = $1"
echo "\$2 = $2"
echo "\$3 = $3"
echo "\$4 = $4"
}
pass_params () {
echo -e "\n" '$* :'; print_params $*
echo -e "\n" '"$*" :'; print_params "$*"
echo -e "\n" '$@ :'; print_params $@
echo -e "\n" '"$@" :'; print_params "$@"
}
pass_params "word" "words with spaces"
在这个相当复杂的程序中,我们创建了两个参数: “word” 和 “words with spaces”,然后把它们传递给 pass_params 函数。这个函数,依次,再把两个参数传递给 print_params 函数, 使用了特殊参数 $* 和 $@ 提供的四种可用方法。脚本运行后,揭示了这两个特殊参数存在的差异:
$ posit-param
$* :
$1 = word
$2 = words
$3 = with
$4 = spaces
"$*" :
$1 = word words with spaces
$2 =
$3 =
$4 =
$@ :
$1 = word
$2 = words
$3 = with
$4 = spaces
"$@" :
$1 = word
$2 = words with spaces
$3 =
$4 =
通过我们的参数,$* 和 $@ 两个都产生了一个有四个词的结果:
word words with spaces
"$*" produces a one word result:
"word words with spaces"
"$@" produces a two word result:
"word" "words with spaces"
这个结果符合我们实际的期望。我们从中得到的教训是尽管 shell 提供了四种不同的得到位置参数列表的方法, 但到目前为止, “$@” 在大多数情况下是最有用的方法,因为它保留了每一个位置参数的完整性。
一个更复杂的应用
- 输出文件。 我们将添加一个选项,以便指定一个文件名,来包含程序的输出结果。 选项格式要么是 -f file,要么是 --file file
- 交互模式。这个选项将提示用户输入一个输出文件名,然后判断指定的文件是否已经存在了。如果文件存在, 在覆盖这个存在的文件之前会提示用户。这个选项可以通过 -i 或者 --interactive 来指定。
- 帮助。指定 -h 选项 或者是 --help 选项,可导致程序输出提示性的使用信息。
usage () {
echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
return
}
interactive=
filename=
while [[ -n $1 ]]; do
case $1 in
-f | --file) shift
filename=$1
;;
-i | --interactive) interactive=1
;;
-h | --help) usage
exit
;;
*) usage >&2
exit 1
;;
esac
shift
done
首先,我们添加了一个叫做 usage 的 shell 函数,以便显示帮助信息,当启用帮助选项或敲写了一个未知选项的时候。当位置参数 $1 不为空的时候,这个循环会持续运行。在循环的底部,有一个 shift 命令, 用来提升位置参数,以便确保该循环最终会终止。
处理 -f 参数的方式很有意思。当监测到 -f 参数的时候,会执行一次 shift 命令,从而提升位置参数 $1 为伴随着 -f 选项的 filename 参数。
我们下一步添加代码来实现交互模式:
if [[ -n $interactive ]]; then
while true; do
read -p "Enter name of output file: " filename
if [[ -e $filename ]]; then
read -p "'$filename' exists. Overwrite? [y/n/q] > "
case $REPLY in
Y|y) break
;;
Q|q) echo "Program terminated."
exit
;;
*) continue
;;
esac
elif [[ -z $filename ]]; then
continue
else
break
fi
done
fi
若 interactive 变量不为空,就会启动一个无休止的循环,该循环包含文件名提示和随后存在的文件处理代码。 如果所需要的输出文件已经存在,则提示用户覆盖,选择另一个文件名,或者退出程序。如果用户选择覆盖一个 已经存在的文件,则会执行 break 命令终止循环。
为了实现这个输出文件名的功能,首先我们必须把现有的这个写页面(page-writing)的代码转变成一个 shell 函数,
write_html_page () {
cat <<- _EOF_
<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIMESTAMP</P>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</BODY>
</HTML>
_EOF_
return
}
# output html page
if [[ -n $filename ]]; then
if touch $filename && [[ -f $filename ]]; then
write_html_page > $filename
else
echo "$PROGNAME: Cannot write file '$filename'" >&2
exit 1
fi
else
write_html_page
fi
解决 -f 选项逻辑的代码出现在以上程序片段的末尾。在这段代码中,我们测试一个文件名是否存在,若文件名存在, 则执行另一个测试看看该文件是不是可写文件。为此,会运行 touch 命令,紧随其后执行一个测试,来决定 touch 命令 创建的文件是否是个普通文件。这两个测试考虑到了输入是无效路径名(touch 命令执行失败),和一个普通文件已经存在的情况。程序调用 write_html_page 函数来生成实际的网页。函数输出要么直接定向到标准输出(若 filename 变量为空的话)要么重定向到具体的文件中。
posted on 2025-10-12 20:14 chuchengzhi 阅读(23) 评论(0) 收藏 举报
浙公网安备 33010602011771号