一个shell脚本永远是以 "#!" 开头的,是一个脚本开始的标记,后面告诉系统执行该脚本需要使用什么解释器。shell脚本文件的后缀是 ".sh" ,脚本中以 "#" 开头的行都是注释。
shell脚本的执行有几种方式:一种是直接 bash hello.sh,该种方式指定了解释器,脚本中的第一行就可以不要了;一种是赋予脚本可执行权限,然后在当前目录下 ./hello.sh执行;还有一种方式是将脚本复制到系统$PATH变量所包含的目录中,赋予其可执行权限,使其成为系统命令。
点号 "." 可用于执行某个脚本,甚至脚本没有执行权限也可执行: . ./hello.sh 。与点号类似,source命令也可读取并在当前环境中执行脚本,同时还可返回脚本中最后一个命令的返回状态;如果没有返回值则返回0,代表执行成功;如果未找到指定的脚本则返回false。
注意source命令或者点号,不会为脚本新建shell,而只是将脚本包含的命令在当前shell执行。
1 shell的内建命令
shell的内建命令就是shell本身提供的命令,而不是某个可执行文件。判断一个命令是不是内建命令只需要借助于命令 type 即可。
1.1 变量声明 declare typeset
declare typeset 都是用来声明变量的,作用完全相同。shell弱类型编程语言,声明变量时不需要指定变量类型。不过使用 declare 声明变量时,可以使用 -i 声明整型变量。
#!/bin/bash
#使用 -i 声明整型变量,变量名 等号 和 值 之间没有空格
declare -i num=5
echo $num
#使用 -r 声明变量为只读
declare -r ro="hello"
echo $ro
#使用 -a 声明数组变量,元素间不需要逗号分隔
declare -a arr=("a" "b" "c")
echo ${arr[2]}
声明局部变量 local
local 命令用于在脚本中声明局部变量。典型用法是在函数中声明局部变量,其作用域限制于函数体内。如果试图在函数外使用 local 声明变量,则会提示错误。
1.2 别名 alias unalias
alias用于创建命令的别名,若该命令不带任何参数,则显示系统当前使用的所有别名命令。但命令行中命名的别名只在当前shell环境中有效,若想使别名一直生效,则应写入用户家目录下的 .ashrc 文件中。
unalias用于删除当前shell环境中的别名命令。一种方法是后接要删除的别名命令;另一种方法是接 -a 参数,删除当前shell环境中所有的别名命令。
1.3 打印 echo
echo用于打印字符,典型用法是使用echo命令并跟上使用双引号括起的内容(即需要打印的内容),该命令会打印出引号中的内容,并在最后默认加上换行符。
#!/bin/bash
#打印 Hello World 并换行
echo "Hello World"
#打印 Hello World 且不换行
echo -n "Hello World"
#打印字符 \n
echo "\n"
#打印一个空行
echo -e "\n"
1.4 执行命令来取代当前的shell exec
内建命令 exec 并不启动新的shell,而是用要被执行的命令替换当前的shell进程,并将老进程的环境清理掉,且进程PID保持不变。当被执行的命令退出时,这个进程也随之推出了,所以exec命令后的其他命令将不再执行。
一般将 exec 命令放到一个shell脚本里面,由主脚本调用这个脚本,主脚本在调用子脚本执行时,当执行到exec后,该子脚本进程就被替换成相应的exec的命令。exec典型的用法是与find联合使用,用find找出符合匹配的文件,然后交给exec处理。
1.5 退出shell exit
使用 exit 退出当前脚本,后可接一个参数n表示退出时的状态,默认退出状态为0。
1.6 声明shell环境变量 export
在父shell中创建变量时,这些变量并不会被其子shell进程所知,也就是说变量默认情况下是“私有”的,或称“局部变量”。使用 export 命令可以将变量导出,使得该shell的子shell都可以使用该变量,这个过程称为变量输出。
注意这种传递是值传递,若在子shell中尝试改变变量的值,也只会在子shell中生效,父shell中的变量值并不受影响。
1.7 发送信号给指定的PID或进程 kill
Linux包含三种不同类型的进程:第一种是交互进程,这是由一个shell启动的进程,既可以在前台运行,也可以在后台运行;第二种是批处理进程,与终端没有联系,是一个进程序列;第三种是监控进程,也称系统守护进程,它们往往在系统启动时启动,并保持在后台运行。
1.8 从标准输入读取一行到变量:read
read 是按行读取的,用回车符区分一行。如果不指定变量,read 命令会将读取到的值放入环境变量 REPLY 中。
#!/bin/bash
declare name
echo -n "What's our name?"
read name
echo "Hi, $name"
#使用参数 -p 指定提示信息
read -p "What's our name?" name
echo "Hi, $name"
1.9 向左移动位置参数 shift
假设一个脚本在运行时可以接受参数,那么从左到右第一个参数被记作 $1 ,第二个参数为 $2 ,以此类推,第n个参数为 $N。所有参数记作 $@ 或 $* ,参数的总个数记作 $# ,而脚本本身记作 $0 。
shift命令可以对脚本的参数做“偏移”操作。假设脚本有A、B、C这3个参数,那么$1为A,$2为B,$3为C;shift一次后,$1为B,$2为C;再次shift后$1为C。
1.10 显示并设置进程资源限度 ulimit
ulimit 可以控制进程对可用资源的访问。默认情况下Linux系统的各个资源都做了软硬限制,其中硬限制的作用是控制软限制(软限制不能高于硬限制)。使用 ulimit -a 可以查看当前系统的软限制,使用命令ulimit -a -H 可查看系统的硬限制。
使用ulimit直接调整参数,只会在当前运行时生效,一旦系统重启,所有调整过的参数就会变回系统默认值。所以建议将所有的改动放在ulimit的系统配置文件(/etc/security/limits.conf)中。
1.11 任务前后台切换 bg fg jobs
该命令用于将任务放置后台运行,一般会与Ctrl+z、fg、&符号联合使用。典型的使用场景是运行比较耗时的任务。
1.12 测试表达式 test
test expression
1.13 其他内建命令
cd pwd break continue return set shopt readonly 等
eval 将所跟的参数作为Shell的输入,并执行产生的命令
let 整数运算
2 shell编程基础
2.1 变量
2.1.1 变量的声明
shell中的变量必须以字母或者下划线开头,后面可以跟数字、字母和下划线,变量长度没有限制。但是,变量是区分大小写的。
shell变量的作用域是在本shell内,属于本shell的全局变量,也就是从定义该变量的地方开始到shell结束,或到主动使用unset删除了该变量的地方为止。
变量的定义非常简单:变量名=变量值。注意,变量名和变量值之间使用等号紧密相连,没有任何空格。当变量值中有空格(字符串)时,必须使用引号引起来,单引号、双引号都可以。
变量的取值也很简单,只需要在变量名前加上 $ 符号既可,严谨一点的写法是 ${} 。
由于shell弱变量的特性,即使没有预先声明的变量也是可以引用的,并且没有任何提示和报错。为避免不必要的麻烦,可以在设置脚本在运行时必须遵循“先声明再使用”的原则,这样一旦出现使用为声明的变量时就立即报错。
shopt -s -o nounset
unset 命令可以取消变量,即将变量从内存中释放。用法为 unset 后面跟变量名。unset后面也可以接函数名,用于取消函数。
内建命令 readonly 可以创建只读变量,效果和 declare -r 是一样的。
2.1.2 bash的环境变量
BASH bash shell的全路径
BASH_VERSION bash shell的版本
HOSTNAME 主机名
HOSTTYPE 主机架构
MATCHTYPE 主机类型的GNU标识
LANG 设置系统的语言环境
PWD 记录当前目录
OLDPWD 记录之前的目录
PATH 命令的搜索路径
FUNCNAME 当前函数的函数名,只能在函数体中使用
HISTCMD 记录下一条命令在history命令中的编号
HISTFILE 记录history命令记录文件的位置
HISTFILESIZE 设置HISTFILE文件记录命令的行数
HISTSIZE 设置命令缓冲区的大小
PS1 命令提示符,默认值是[\u@\h \W]\$,其中\u是用户名、\h是主机名、\W是当前工作目录的basename、\$是用户UID的替换字符:如果UID是0则替换成“#”,否则替换成“$”,所以此处具体显示出来就是“[root@localhost ~]#”。
2.1.3 特殊变量
shell中有一些预先定义的特殊只读变量,它们的值只有在脚本运行时才能确定。
1.位置参数
位置参数的命名简单直接:脚本本身为$0,第一个参数为$1,第二个参数为$2,第三个为$3,以此类推。当位置参数的个数大于9时,需要用 ${} 括起来标识,比如说第10个位置参数应该记为${10}。另外,$# 表示脚本参数的个数总和,$@ 或$* 表示脚本的所有参数。
2.脚本或命令返回值 $?
Linux中规定正常退出的命令和脚本应该以0作为其返回值,任何非0的返回值都表示命令未正确退出或未正常执行。
$? 永远是上一个命令的返回值,所以要查看某个命令的返回值必须在运行该命令后立即查看$?。在自动化脚本中,也可以通过 $? 变量的值判断之前命令的执行状态,从而采取不同的动作。
2.2 数组
shell中的数组对元素个数没有限制,但只支持一维数组。因为shell是弱类型的,所以没有要求数组元素类型必须一致。
#!/bin/bash
#使用declare关键字声明数组
declare -a arr
arr=(2 3 4)
arr[3]="hello, world!"
echo ${arr[0]}
echo ${arr[3]}
#取出所有元素
echo ${arr[@]}
echo ${arr[*]}
#不使用关键字声明数组
name=("Chris" "Bin")
echo ${name[1]}
#对特定元素赋值
declare -a arr2=([1]=2 [3]=9)
echo ${arr2[3]}
数组长度:即数组元素个数。利用“@”或“*”字符,可以将数组扩展成列表,然后使用“#”来获取数组元素的个数。
#!/bin/bash
name=('Chris' 'Bin')
#获取元素个数
echo ${#name[@]}
#获取第二个元素字符串的长度
echo ${#name[1]}
数组的截取、连接和替换:
#!/bin/bash
name=('Jonh' 'Chris' 'Bob' 'Bin' 'Andy' 'Dave')
#数组截取:取出数组的第2 3个元素 Chris Bob
echo ${name[*]:1:2}
#数组截取:取出数组第2个元素的从第3个字符开始的连续2个字符 ri
echo ${name[1]:2:2}
#连接数组
arr=(1 2 3)
con=(${arr[@]} ${name[*]})
echo ${con[@]}
#元素替换
name=(${name[*]/Bob/Bin})
echo ${name[@]}
2.3 转义和引用
2.3.1 转义
shell中的转义符是反斜线 “\” 。
2.3.2 引用
引用是指将字符串用某种符号括起来,以防止特殊字符被解析为其他意思。shell中一共有4种引用符:双引号、单引号、反引号(在键盘上和波浪号位于同一个键)和转义符。其中,双引号又叫“部分引用”或“弱引用”,可以引用除$符、反引号、转义符之外的所有字符;单引号又叫“全引用”或“强引用”,可以引用所有字符,仅表示字面意思;反引号则会将反引号括起的内容解释为系统命令。
注意,单引号中不可再使用单引号引用。
2.3.3 命令替换
命令替换是指将命令的标准输出作为值赋给某个变量。shell中有两种方式可以完成命令替换,一种是反引号,一种是 $() 。且 $() 支持嵌套,而反引号不行。$()仅在Bash Shell中有效,而反引号可在多种UNIX Shell中使用。
2.4 运算符
注意,shell只支持整数运算,且运算符和操作数之间紧密相连,不能有空格。常见的算术运算大多需要结合shell的内建命令let来使用。
2.4.1 使用 $[ ] 做运算
$[] 和 $(()) 一样,可用于简单的算术运算:
#!/bin/bash
echo $[2+4]
echo $[2**3]
echo $((2%5))
echo $((8/4))
2.4.2 使用 expr 运算
expr命令也可用于整数运算。和其他算术运算方式不同,expr要求操作数和操作符之间使用空格隔开(否则只会打印出字符串),所以特殊的操作符要使用转义符转义(比如*)。
expr支持的算术运算符有加、减、乘、除、余等。
2.4.3 使用 declare 命令运算
虽然shell在创建变量时可以不使用 declare ,但使用不使用的区别蛮大,如下:
#!/bin/bash
a=2+3
echo $a # "2+3"
declare -i b
b=3+5
echo $b # 8
2.4.4 算数扩展
算数扩展是shell提供的整数运算机制,是shell的内建命令,语法: $((算术表达式)) 。
其中的算术表达式由变量(此时变量前不需要 $)和运算符组成,常见的用法是显示输出和变量赋值。若表达式中的变量没有定义,则在计算时,其值会被假设为0(但是并不会真的因此赋0值给该变量)。
#!/bin/bash
a=2
echo $((a+3)) # 5
echo $((2*(j+3))) # 6
echo $j # 空
2.4.5 使用 bc 做运算
bc 是Linux下一款专门用于高精度计算的工具,可用于浮点数的计算。bc 还支持逻辑运算、比较运算。最简单的使用方式是直接键入 bc 命令进入交互界面。
默认情况下 bc 并不显示小数部分,可以通过设置 scale 决定要显示的小数位数。
2.5 特殊字符
2.5.1 通配符
通配符用于模式匹配,常见的通配符有 *、? 和 用[]括起来的字符序列:
* 代表任意长度的字符串,但是不包括点号和斜线号;
? 可用于匹配任一单个字符;
[ ] 代表匹配其中的任意一个字符。
2.5.2 位置参数
详见 2.1.3节。
3 测试和判断
3.1 测试
测试的第一种方式是直接使用test命令: test expression
其中expression是一个表达式,可以是算术比较、字符串比较、文本和文件属性比较等。
测试的第二种方式是使用 [ ] : [ expression ]
注意,在[ ] 和 expression之间必须有空格。这种方式方便和 if case while 等关键字连用。
3.1.1 文件测试符
文件测试 | 说明 |
-b file | 当文件存在且是个块文件时返回真,否则为假 |
-c file | 当文件存在且是个字符设备文件时返回真,否则为假 |
-d file | 当文件存在且是个目录时返回真,否则为假 |
-e file | 当文件或者目录存在时返回真,否则为假 |
-f file | 当文件存在且为普通文件时返回真,否则为假 |
-x file | 当文件存在且为可执行文件时返回真,否则为假 |
-w file | 当文件存在且为可写文件时返回真,否则为假 |
-r file | 当文件存在且为可读文件时返回真,否则为假 |
-l file | 当文件存在且为连接文件时返回真,否则为假 |
-p file | 当文件存在且为管道文件时返回真,否则为假 |
-s file | 当文件存在且大小不为0时返回真,否则为假 |
-S file | 当文件存在且为Socket文件时返回真,否则为假 |
-g file | 当文件存在且设置了SGID时返回真,否则为假 |
-u file | 当文件存在且设置了SUID时返回真,否则为假 |
-k file | 当文件存在且设置了sticky属性时返回真,否则为假 |
-G file | 当文件存在且属于有效用户组时返回真,否则为假 |
-O file | 当文件存在且属于有效用户时返回真,否则为假 |
file1 -nt file2 | 当 file1 比 file2 新时返回真,否则为假(newer than) |
file1 -ot file2 | 当 file1 比 file2 旧时返回真,否则为假(older than) |
3.1.2 字符串测试符
字符串测试 | 说明 |
-z "string" | 字符串string为空时返回真,否则为假 |
-n "string" | 字符串string为非空时返回真,否则为假 |
"string1"="string2" | 两字符串相同时返回真,否则为假 |
"string1"!="string2" | 两字符串不相同时返回真,否则为假 |
"string1">"string2" | 按照字典排序,字符串1排在字符串2之前时返回真,否则为假 |
"string1"<"string2" | 按照字典排序,字符串1排在字符串2之后时返回真,否则为假 |
3.1.3 整数比较符
整数比较 | 说明 |
num1 -eq num2 | 如果num1等于num2则返回真,否则为假。equal |
num1 -gt num2 | 如果num1大于num2则返回真,否则为假。great than |
num1 -lt num2 | 如果num1小于num2则返回真,否则为假。less than |
num1 -ge num2 | 如果num1大于等于num2则返回真,否则为假。great equal |
num1 -le num2 | 如果num1小于等于num2则返回真,否则为假。less equal |
num1 -ne num2 | 如果num1不等于num2则返回真,否则为假。not equal |
3.1.4 逻辑测试与逻辑运算
逻辑运算 | 说明 |
!expression | 若expression为真,则测试结果为假 ! |
expression1 -a expression2 | 两个表达式都为真,则测试结果为真 && |
expression1 -o expression2 | 两个表达式任意一个为真,则测试结果为真 || |
shell中的逻辑运算符同C语言一致: 与&&、或||、非!。
3.2 判断
3.2.1 if 判断
#!/bin/bash
if expression; then
...
fi
if expression; then
...
else
...
fi
if expression; then
...
elif expression2; then
...
elif expression3; then
...
fi
3.2.2 case 判断
注意,case判断结构中的var1、var2、var3等这些值只能是常量或正则表达式。
#!/bin/bash
case VAR in
var1) command1 ;;
var2) command2 ;;
var3) command3 ;;
...
*) command ;;
esac
4 循环
for循环在读取文件时,任何空白字符都可以作为其读取的分隔符。
while循环在读取文件时,是按行读取的,因为while使用的是换行符作为行标记。
4.1 for
4.1.1 带列表的for循环
#!/bin/bash
fruits="apple orange pear banana"
for fruit in ${fruits}
do
echo "${fruit} is Chris favorite."
done
echo "Hello World!"
for var in {1..20}
do
echo "loop: ${var}"
done
sum=0
for var in $(seq 1 2 100)
do
let "sum+=var"
done
echo "Total: ${sum}"
4.1.2 类C的for循环
#!/bin/bash
for ((i=1; i<=10; ++i))
do
echo -n "${i} "
done
4.2 while
#!/bin/bash
loop=5
i=0
while [ $loop -gt 0 ]
do
let "loop--"
let "i++"
echo " loop ${i} "
done
4.3 until
until采用的是测试假值的方式,当测试结果为假时执行循环体,直到测试为真时才停止循环。
4.4 select
select是一种菜单扩展循环方式,当程序运行到select语句时,会自动将列表中的所有元素生成为可用1、2、3等数选择的列表,并等待用户输入。用户输入并回车后,select可判断输入并执行后续命令。如果用户在等待输入的光标后直接按回车键,select将不会退出而是再次生成列表等待输入。
select有判断用户输入的功能,所以select经常和case语句合并使用。
#!/bin/bash
echo "today is ?"
select today in Mon Tue Wed Thu Fri Sat Sun
do
case $today in
Mon) echo "Today is Monday";;
Tue) echo "Today is Tuesday";;
Wed) echo "Today is Wednesday";;
Thu) echo "Today is Thursday";;
Fri) echo "Today is Friday";;
Sat|Sun) echo "You can have a rest today";;
*) echo "Unknown input,exit now" && break;;
esac
done
5 函数
5.1 函数定义
shell中函数定义使用关键字 function,不过关键字 function 也可省略。
#!/bin/bash
function sayHi(){
echo "Hello World!"
}
sayHi
5.2 函数返回值
#!/bin/bash
file = "./test.txt"
checkFileExist(){
if [ -f $file ];then
return 0
else
return 1
fi
}
checkFileExist
result=$?
if [ result -eq 0 ];then
echo "$file exist"
else
echo "$file not exist"
fi
5.3 函数的参数
shell中函数的参数传递,一种是通过位置参数传递,在执行脚本时后接参数,在脚本中通过 $1 $2 等位置参数获取;另一种是使用内置命令 set 来设置参数,一旦使用了 set 设置参数后,将忽略执行脚本时传入的参数,实际上是 set 重置了位置参数的值;当然还可以配合 shift 命令使位置参数左移。
#!/bin/bash
set hello world
for i in $@
do
echo -n "$i "
done
5.4 函数库
当要引用别的脚本中的函数时,可以直接加载那个脚本。有两种加载方法:点(.) 和 source 命令。
很多Linux发行版中都有 /etc/init.d 目录,这是系统中放置所有开机启动脚本的目录,这些开机脚本在脚本开始运行时都会加载 /etc/init.d/functions 或 /etc/rc.d/init.d/functions 函数库(实际上这两个函数库的内容是完全一样的),functions函数库中定义了27个函数。
6 重定向
在Linux中,文件标识符也是一种“设备”,这个标识符会出现在 /dev/fd 目录中。
Linux使用0到9的整数指明了与特定进程相关的数据流,系统在启动一个进程的同时会为该进程打开三个文件:标准输入(stdin)、标准输出(stdout)、标准错误输出(stderr),分别用文件标识符0、1、2来标识。如果要为进程打开其他的输入输出,则需要从整数3开始标识。默认情况下,标准输入为键盘,标准输出和错误输出为显示器。
I/O重定向可以将任何文件、命令、脚本、程序或脚本的输出重定向到另外一个文件、命令、程序或脚本。
符号 | 含义 |
> | 标准输出覆盖:将命令的输出重定向输出到其他文件中,同时覆盖文件中已有的内容 |
>> | 标准输出追加:将命令的输出重定向输出到其他文件中,以追加而不是覆盖的方式 |
>& | 标识输出重定向:将一个标识的输出重定向到另一个标识的输入 |
< | 标准输入重定向:命令将从指定文件中读取输入,而不是从键盘 |
| | 管道:从一个命中读取输出并作为另一个命令的输入 |