Shell 脚本语言入门
一、概念认识
命令行是架设在用户和内核之间的一座桥梁。在Linux下,这个命令行程序叫做 Shell。
- Shell 是一种脚本语言,我们编写完源码后不用编译,直接运行源码即可。
- Shell 本身支持的命令并不多,但是它可以调用其他的程序,每个程序就是一个命令,这使得 Shell 命令的数量可以无限扩展。
- Shell 脚本很适合处理纯文本类型的数据,而 Linux 中几乎所有的配置文件、日志文件(如 NFS、Rsync、Httpd、Nginx、MySQL 等),以及绝大多数的启动文件都是纯文本类型的文件。
- Shell 必学内容,Linux 正则表达式以及三剑客 grep、awk、sed 等命令。
Shell是一个程序,一般都是放在/bin
或者/user/bin
目录下,当前 Linux 系统可用的 Shell 都记录在/etc/shells
文件中。
不同的组织机构开发了不同的 Shell,它们各有所长,有的占用资源少,有的支持高级编程功能,有的兼容性好,有的重视用户体验。常见的 Shell 有 sh、bash、csh、tcsh、ash、zsh 等。
sh
是 UNIX 上的标准 shell。bash
是 Linux 的默认 shell。bash 由 GNU 组织开发,保持了对 sh shell 的兼容性。ash
一个简单的轻量级的 Shell,占用资源少,适合运行于低内存环境,与 bash shell 完全兼容。
(以下内容均基于 bash 编写)
查看当前 Linux 的默认 Shell,$环境变量:
$ echo $SHELL
此外 $PS1
和 $PS2
是提示符的当前格式。
对于普通用户,Base shell 默认的提示符是美元符号 $
;对于超级用户(root 用户),Bash Shell 默认的提示符是井号 #
。
在图形桌面环境(例如 GNOME、KDE、Unity 等),原生 Shell 入口被隐藏了。进入 Shell 的方法是让 Linux 系统退出图形界面模式,进入 Linux 控制台(Console)。 现代 Linux 系统在图形界面启动时会自动创建几个虚拟控制台(Virtual Console),其中一个供图形桌面程序使用,其他的保留原生控制台的样子。虚拟控制台其实就是 Linux 系统内存中运行的虚拟终端(Virtual Terminal)。
二、基础
(一)脚本文件
“脚本”是把Shell集中放在一个文件中,按顺序逐句执行。运行效果和一句句输入一样。
- 脚本的扩展名不影响脚本执行,没有都可,但一般用
.sh
。 - 脚本文件需要可执行权限,即需要
chmod +x ./test.sh
。 - 脚本内容的第一行必须是
#!
约定,告诉系统这个脚本需要什么解释器来执行。
#!/bin/bash
单个#
是注释,到行尾将被忽略。另,可以把大段要注释的代码用一对花括号 { } 括起来,定义成一个函数,不去调用它,就达到注释效果。
Shell可以将外部脚本的内容合并到当前脚本:
. filename
# 或者
source filename
- 两种方式的效果相同。注意:点号
.
和文件名中间有一个空格! - 被包含脚本不需要有执行权限。
(二)变量
脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。
在 Bash shell 中,全部变量的值都是字符串。
无论给变量赋值时有没有使用引号,值都会以字符串的形式存储。
Shell 使用 \
转义,特殊字符。
变量类型:
(1)局部变量:在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问。
(2)环境变量:所有的程序,都能访问环境变量。有些程序需要环境变量来保证其正常运行。shell脚本中可以定义环境变量。
(3)shell变量:由shell程序设置的特殊变量。特殊变量不遵循命名规则。
- 定义变量
variable=value # 不用引号时,value中不能包含任何空白符(例如空格、Tab缩进等)
variable='value' # 单引号、双引号有区别。
variable="value"
注意,赋值号的周围不能有空格!例如ABC = 123
不对。 这和大部分编程语言都不一样。
但 Shell 变量的命名规范和大部分编程语言一样:变量名由数字、字母、下划线组成;必须以字母或者下划线开头;不能使用关键字(通过 help 命令查看保留关键字)。
- 变量的值
改变变量值时,和新定义变量形式一样。
- 以单引号
' '
包围变量的值时,纯字符串,单引号里面是什么就输出什么,即使内容中有转义、变量和命令(命令需要反引)也会把它们原样输出。 - 以双引号
" "
包围变量的值时,会解析里面的变量和命令(命令需要反引)。 - 单引号中不能嵌套单引号;双引号中可以随便嵌套。
建议: 数字不加引号;字符串都建议加上双引号;除非必须用纯字符串,否则不用单引号。
Shell 支持将命令的执行结果赋值给变量,两种方式:
variable=$(command) # 推荐
variable=`command` # 巨坑!强烈不建议!反引号极容易与单引号混淆。
例如:
$ log=$(cat log.txt)
$ echo $log
- 调用变量值
使用一个定义过的变量,只要在变量名前面加美元符号
$
即可;变量名外面可以加花括号{ }
,推荐加。
有时必须加花括号,是为了让解释器识别变量的边界。例如:$ echo "I am good at ${skill}Script
- 声明只读
定义变量之后,可以使用
readonly
命令将变量声明为只读(尝试更改只读变量,报错)。例如:
#!/bin/bash
myUrl="http://see.xidian.edu.cn/cpp/shell/"
readonly myUrl
-
删除变量
使用unset
命令可以删除变量。调用删除变量的值没有任何输出。unset
命令不能删除只读变量。 -
变量替换(重点)
变量可以根据状态(是否为空、是否定义等)来改变值。
变量替换形式:
# 变量本来的值。
${var}
# 如var被定义且不空,返回 "abc",但不改变var原值;否则返回var值。
${var:+"abc"}
# 如var为空或已被删除(unset),返回 "abc",但不改变var的原值;否则返回var值。
${var:-"abc"}
# 如var为空或已被删除(unset),返回 "abc",并改变var值为 "abc";否则返回var值。
${var:="abc"}
# 如var被定义,返回var的值;如果为空或已被删除(unset),将消息 "message" 送到标准错误输出,脚本停止运行。
${var:?"message"}
- 特殊变量(重点)
-
$0
当前脚本的文件名。即从终端输入的第一个word。输入的第二word作为$1
。 -
$n
传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1
,第二个参数是$2
。 -
$#
传递给脚本或函数的参数个数。 -
$*
传递给脚本或函数的所有参数。 -
$@
传递给脚本或函数的所有参数。被双引号(" “)包含时,与 $* 稍有不同,下面将会讲到。 -
$?
上个命令的退出状态或函数的返回值。一般情况下,成功0
,失败1
,但没有硬性规定。 -
$$
当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。
注意$*
和$@
的区别:$*
和$@
不被双引号包裹时,都将各参数分开视之,即”$1" “$2” … “$n”。- 被双引号包裹时,
"$@"
将各个参数分开,即"$1" "$2" … "$n"
;而"$*"
将所有的参数作为一个整体,即"$1 $2 … $n"
。
-
${#str}
获取字符串str的长度。例如:str="abcdefg"
则$ echo ${#str}
输出7 -
${str:1:4}
提取子字符串。例如:$ echo ${str:1:4}
输出bcde
(三)运算
原生bash不支持数学运算,但是可以通过其他命令来实现,例如 awk
和 expr
。
expr 是一款表达式计算工具:加+、减-、乘*(注意必须 \*
)、除/、取余%
var=$(expr 1 + 1)
var2=$(expr 2 \* 2)
echo $var $var2
注意: 表达式和运算符之间必须要有空格,必须写成 2 + 2
,而2+2
不对,这与熟悉的大多数编程语言不一样。
(四)数组
bash支持一维数组,不支持多维数组。
下标由0开始编号。
获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于等于0。
- 定义数组
- 用小括号 () 定义一个数组,数组元素用空格分开。例如:
rray_name=(value0 value1 value2 value3)
- 也可以用中括号 [] 单独定义数组的各个分量:
array_name[0]=value0
array_name[1]=value1
array_name[5]=value2 # 可以不使用连续的下标。
- 读取数组元素
- 单个:
valuen=${array_name[2]}
- 全部:使用
@
或*
可以获取数组中的所有元素,例如:${array_name[*]}
和${array_name[@]}
- 获取数组长度 获取数组长度的方法与获取字符串长度的方法相同。例如:
length=${#array_name[@]} # 取得数组元素的个数
length=${#array_name[*]} # 取得数组元素的个数
lengthn=${#array_name[2]} # 取得数组单个元素的长度
三、控制
(一)关系判断if
注意: 必须方括号!且必须全部有空格隔开!例如 [$a-le$b]
或者 [$a -le $b]
都是错误的。
1. 变量检测
# shell变量值都是字符串:
[ $a ] # 检测字符串不为空。
[ -z $a ] # 检测字符串长度为0。
[ -n $a ] # 检测字符串长度不为0。
[ $a = $b ] # 检测两个字符串相同。
[ $a != $b ] # 检测两个字符串不同。
# 可以将shell的变量值视作数字:
[ $a -eq $b ] # 检测两个数字相等。
# -ne 不等于;-gt 大于;-lt 小于;-ge 大于等于;-le 小于等于。
2. 文件检测
假设 file="/var/www/tutorialspoint/unix/test.sh"
[ -r $file ] # 检测文件可读。-w 可写;-x 可执行。
[ -s $file ] # 检测文件不空(文件大小大于0)。
[ -e $file ] # 检测文件(包括目录)是否存在。
[ -f $file ] # 检测文件是普通文件(既不是目录,也不是设备文件)。
# -d 目录;-b 块设备文件;-c 字符设备文件。
[ -g $file ] # 检测文件设置了 SGID 位。-u 设置了 SUID 位;-k 设置了粘着位(Sticky Bit)。
[ -p $file ] # 检测文件是具名管道。
3. 逻辑组合
- 或:
-o
,例如[ $a -lt 20 -o $b -gt 100 ]
- 与:
-a
,例如[ $a -lt 20 -a $b -gt 100 ]
- 与
-a
优先级高,或-o
优先级低。(shell 没有“非”)
IF语句:
#!/bin/sh
a=10
b=20
if [ $a -ne $b ]
then
echo "a is not equal to b"
else
echo "a is equal to b"
fi
IF可以写成一行,以命令的方式来运行:
if test $[2*3] -eq $[1+5]; then echo 'The two numbers are equal!'; fi;
# 注意用分号;连接。 test 命令用于检查某个条件是否成立,与方括号 [ ] 类似。
复杂IF语句:
if [ expression 1 ]
then
Statement(s) to be executed if expression 1 is true
elif [ expression 2 ]
then
Statement(s) to be executed if expression 2 is true
else
Statement(s) to be executed if no expression is true
fi
CASE语句:
#!/bin/bash
option="${1}"
case ${option} in
-f) FILE="${2}"
echo "File name is $FILE"
;;
-d) DIR="${2}"
echo "Dir name is $DIR"
;;
*)
echo "`basename ${0}`:usage: [-f file] | [-d directory]"
exit 1 # Command to come out of the program with status 1
;;
esac
- 右括号,值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行,直至
;;
。 ;;
与其他语言中的 break 类似,不再继续其他模式。- 如果无一匹配模式,使用星号
*)
捕获该值,再执行后面的命令。
(二)循环
1. FOR循环
for 变量 in 列表
do
command1
command2
done
- 列表是一组值(数字、字符串等)组成的序列。每个值通过空格分隔。
- 每循环一次,就将列表中的下一个值赋给变量。 例如:
#!/bin/bash
# 显示主目录下以 .bash 开头的全部文件:
for FILE in $HOME/.bash*
do
echo $FILE
done
结果输出:
/root/.bash_history
/root/.bash_logout
/root/.bash_profile
/root/.bashrc
2. WHILE循环
while command
do
Statement(s) to be executed if command is true
done
例如:
COUNTER=0
while [ $COUNTER -lt 5 ]
do
COUNTER='expr $COUNTER + 1'
echo $COUNTER
done
3. UNTIL循环
until command
do
Statement(s) to be executed until command is true
done
4. 循环干预
break
命令跳出循环。在嵌套循环中,后面跟一个整数,表示跳出第几层循环。例如:break 2
表示跳出第几层循环。continue
命令仅跳出当前循环。同样,continue 3
后面也可跟一个数字,表示跳出第几层循环。
(三)函数
1. 先定义函数
function function_name () {
list of commands
return value
}
- 关键字
function
可以省略。 - 函数返回
return
也可省略,此时最后一条命令的运行结果将作为返回值。 - shell函数返回值只能是整数,一般表示成功与否,
0
表示成功,其他值表示失败。
2. 再调用:
function_name
- 调用函数只需要给出函数名,不需要加括号。
- 函数返回值在调用该函数之后如果还想取,可以通过
$?
来获得。
3. 可删除:
unset .f function_name
- 删除函数要加
.f
选项。
4. 传递参数:
- 在函数体内部可直接通过
$n
的形式来获取参数的值,例如,$1
表示第一个参数,$2
表示第二个参数。 - 建议
${n}
。如果n>=10,必须加{},否则$10
视为$1
接0
。
四、输出输入
Linux命令默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是终端,即显示器。
一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:
- 标准输入文件(
stdin
):文件描述符为0
,默认从stdin读取数据。 - 标准输出文件(
stdout
):文件描述符为1
,默认向stdout输出数据。 - 标准错误文件(
stderr
):文件描述符为2
,会向stderr流中写入错误信息。
(一)输出重定向
Linux默认标准输出文件(stdout),文件描述符为 1
。
Linux输出不仅可以是显示器,还可以很容易的转移向到文件。
- 命令输出重定向:
command > file
- 显示器上不会看到任何输出。
- 单
>
输出重定向会覆盖文件内容。 - 如果不希望文件内容被覆盖,可以使用
>>
追加到文件末尾。
- stderr 重定向到 file :
command 2 > file
或追加到文件末尾command 2 >> file
- 注意:
2
表示标准错误文件(stderr)。
- stdout 和 stderr 合并后重定向到 file :
command > file 2>&1
n >& m
将输出文件 m 和 n 合并。n <& m
将输入文件 m 和 n 合并。
/dev/null
/dev/null
是一个特殊的文件,写入到它的内容都会被丢弃,command > /dev/null
起到“禁止输出”的效果。 如果尝试从该文件读取内容,那么什么也读不到。
- 具体应用:
> /dev/null
不写默认为1(stdout)。即标准输出抛弃;异常才会打印。
2 > /dev/null
2代表(stderr),只有命令返回错误信息才会抛弃,正常信息打印出来。
> /dev/null 2>error.log
stdout标准输出抛弃;错误信息写入error.log。
> /dev/null 2>&1
标准输出和错误,都抛弃。2>&1
代表redirect the error stream into the output stream。注意如不写&
,1
就成为文件名。
(二)输入重定向
可以从文件获取输入:command < file
- 本来需要从键盘获取输入的命令会转移从文件读取内容。
- 如果希望对 stdin 和 stdout 都重定向:
command < file1 > file2
<< tag
将开始标记 tag 和结束标记 tag 之间的内容作为输入。
如下:
command << tag
document
tag
- 将两个
tag
之间的document
内容作为输入传递给command
。 - 结尾的
tag
一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。
(三)输出的指令
1. echo
echo "aaa"
- 在 stdout 输出。
-n
不在结尾自动换行。-e
若字符串中出现转义格式符,则转义。默认echo "Value of a is $a \n"
将输出Value of a is $a \n
;加-e
后,输出Value of a is $a
echo $var1 $var2
不论命令中用什么间隔,输出后,不同变量间仅用一个空格隔开。
2. printf
printf format-string arguments...
- 不加括号!
format-string
为格式控制字符串,可以没有引号,但最好加上,单双引号都行,效果一样。arguments
为参数列表,用空格分隔,不用逗号。arguments
没有或不够,%s
为空。- 参数多于格式控制符(%)时,format-string 重复调用,例如:
printf "%s %s %s\n" a b c d e f g h i j
,输出:
a b c
d e f
g h i
j
注意: printf 不像 echo 那样会自动换行,必须显式添加换行符 \n
。如:printf "Hello, Shell\n"
(四)输入的指令
read ABC
从 stdin 获取输入,赋值给 ABC 变量(不用之前声明)。
五、正则表达式
Linux正则表达式一般是以 “行” 为单位处理。
\ # 转义字符。 例如:',' 是一个特殊字符,在正则表达式中有特殊含义。必须要先转义 '\,'
^word # 以word开头的行
word$ # 以word结束的行
# 例子:搜寻以 # 开头的脚本注释行:grep –n '^#' regular.txt
. # 匹配任意1个字符,必须1个
# 例子:grep –n 'e.e' regular.txt 可匹配eee,eae,eve,但是不匹配ee。
? # 匹配0个或1个在其之前的那个普通字符
# 例子:grep –nE 'go?d' regular.txt 可匹配 gd,god
* # 前面的字符重复0个到多个
# 例子:grep –n 'go*gle' regular.txt 可匹配 gle,gogle,google,gooogle 等等
+ # 匹配1个或多个在其之前的那个普通字符,重复前面字符1到多次
# 例子:grep –nE 'go+d' regular.txt 可匹配 god,good,goood 等等
() # 表示一个字符的集合。 例如:(abcdefg) 匹配的是 abcdefg
# ()用在expr中,仅匹配括号内的字符串。例如 /\d+(\w+)/ 用在 123abc 将捕获到 abc
| # 表示“或”,匹配一组可选的字符,或(or)的方式匹配多个字符串
# 例子:grep –nE 'god|good' regular.txt 可匹配 god或者good
# 例子:grep –nE 'g(oo|la)' regular.txt 可匹配 good或者glad
[list] # 匹配一系列字符中的1个
# 例子:grep –n 'g[lf]' regular.txt 可匹配 gl,gf
[n1-n2] # 匹配一个字符范围中的1个字符 例如 [a-zA-Z]
# 例子:grep –n '[0-9]' regular.txt 可匹配单个数字字符
[^list] # 匹配字符集以外的字符
# 例子:grep –n '[^o]' regular.txt` 匹配非o字符
\<abc # 匹配单词是abc开头的
# 例子:grep –n '\<g' regular.txt 匹配以g开头的单词
abc\> # 匹配单词是abc结尾的
# 例子:grep –n 'tion\>' regular.txt 匹配以tion结尾的单词
\{n1\} # 前面的字符重复n1个,包括前面的
# 例子:grep –n 'go\{2\}gle' regular.txt 匹配 google
\{n1,\} # 前面的字符至少重复n1个
# 例子:grep –n 'go\{2\}gle' regular.txt 匹配 google,gooogle
\{n1,n2\} # 前面的字符重复n1,n2次
# 例子:grep –n 'go\{2,3\}gle' regular.txt 匹配 google,gooogle
例子
#!/bin/sh
# 通过pppoe-wan提取拨号返回的IPv4和IPv6地址
ADD4=""
TMP=$(ifconfig pppoe-wan | grep 'inet addr:[1-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[1-9][0-9]*' | sed 's/:/ /g')
if [ ${#TMP} -gt 20 ]
then
HEAD=$(echo $TMP | awk '{print $1$2}')
if [ ${#HEAD} -eq 8 ]
then
if [ ${HEAD} = 'inetaddr' ]
then
ADD4=$(echo $TMP | awk '{print $3}')
fi
fi
fi
if [ ${#ADD4} -lt 8 ]; then exit 1; fi
TMP=""
TMP=$(ifconfig pppoe-wan | grep 'inet6 addr: 2...\:.*\:.*\:.*\:.*\:.*\/.*Global' | sed 's#\/# #g')
if [ ${#TMP} -gt 35 ]
then
HEAD=""
HEAD=$(echo $TMP | awk '{print $1$2}')
if [ ${#HEAD} -eq 10 ]
then
if [ ${HEAD} = 'inet6addr:' ]
then
ADD6=$(echo $TMP | awk '{print $3}')
if [ ${#ADD6} -gt 20 ]
then
echo $ADD4
echo $ADD6
exit 0
fi
fi
fi
fi
echo $ADD4
exit 0
最后修改于 2024-02-24