一.常识
bash脚本基本规则:
执行时展开变量,得到命令和参数字符串,执行
一般流程:
# 1.创建/编辑`.sh`文件(道德约束)
vim test.sh
# 2.添可执行权限
chmod +x test.sh
# 3.执行
./test.sh
简单示例:
#!/bin/bash
# 声明变量
str='hoho'
# 输出变量值
echo $str
其中,第一行#!/bin/bash
说明解释器所在路径,可以通过which bash
查看
P.S.#!
叫shebang(释伴,就这么翻译,也这么读),更多信息请查看释伴:Linux 上的 Shebang 符号(#!)
二.变量
1.环境变量
HOME # 当前用户目录的绝对路径
USER # 当前用户名
PWD # 当前工作目录
# ...
# 更多变量用`env`命令查看
不用声明,直接使用,类似于node
里的__dirname
和__filename
创建环境变量的3种方式:
在
bashrc
文件(系统级的/etc/bashrc
和用户级的~/.bashrc
)中添加永久环境变量(每个新创建的shell都拥有)在执行脚本时设置临时环境变量(仅在执行脚本的子shell内有效)
export
环境变量(只对后续创建的子shell有效)
例如:
# 方式1
# 如果是zsh,对应文件名为`~/.zshrc`
echo _ENV=product >> ~/.bashrc
source ~/.bashrc
echo $_ENV
# 方式2
_ENV=product ./test.sh
# 在./test.sh中可以读到_ENV
echo $_ENV
# 方式3
_ENV=product; export _ENV
# 新开一个shell
bash
echo $_ENV
2.全局变量与局部变量
#!/bin/bash
# 默认声明全局变量
VAR="global variable"
function fn() {
VAR="updated global variable"
# 只能在function里通过local关键字声明局部变量
local VAR="local variable"
echo $VAR
}
fn
echo $VAR
# 输出
local variable
updated global variable
即用即声明,默认形式的都是全局变量,只能在函数内部通过local
关键字声明局部变量。还需要注意:
等号两边不能有空格,因为每一行会被当作“命令 空格 参数”
引号不是必须的,与CSS一样,内容包含空格时引号才有必要
没有提升(hosting)一说,局部变量作用域是从变量声明位置到函数体结束,全局变量作用域是从声明位置到文件结束
3.访问变量值
$变量名
取变量值,如$VAR
变量插值规则:在双引号中引用的变量会被展开(expanded),单引号中的不会,与PHP一样
{}
可以隔离变量名,把变量名保护起来:
${VAR}abc # VAR的值后面紧跟着字符串abc
取数组元素时必须这样做,例如:
arr=(aa b ccc)
# 输出aa[1],不符合预期
# 因为用$对arr取值,得到aa($arr返回首元),再给后面接上字符串[1]
echo $arr[1]
# 输出b
echo ${arr[1]}
三.分支和循环
1.条件语句
if 条件 # test命令和[]操作符
then
语句...
else # else if写作elif
语句...
fi
条件部分一般是test
命令或者[]
操作符,例如:
if [ $X -lt $Y ]; # X小于Y
if [ -n $X ]; # 变量非空(字符串长度不为0)
if [ -e $path ]; # 文件存在
# 数值比较
if test 2 -gt 1; then echo "number 2 > 1"; fi
# 等价于
if [ 2 -gt 1 ]; then echo "number 2 > 1"; fi
# 字符串比较
if test 2 > 11; then echo "string 2 > 11"; fi
# 等价于
if [ 2 > 11 ]; then echo "string 2 > 11"; fi
其中有3个细节,操作数类型、分号和空格:
-gt
表示比较数值大于,>
表示比较字符串大于,操作符运算时会自动转换,无法转换就报错;
在单行语句中用来区分块结构,第一个分号表示条件部分结束,第二个分号表示then
部分结束,缺一不可空格用来分隔命令和参数(除了空格,默认分隔符还有制表符和换行,见下面IFS)
P.S.字符串转数值常用方式有((str))
和`expr str`
、$(expr str)
,前者是bash
操作符,后两个是外部命令
空格示例:
# 空格很关键
if [ 1=2 ]; # 把1=2整体当操作数(字符串)了,没看见操作符
# []里两端的空格也很关键
if [-e $path]; then # 报错[-e命令找不着,因为被看成了'[-e' '$path]'
可以通过man test
命令查看其它test
操作符
bash
也提供了类似于switch
的东西,只是语法很奇怪:
case $variable in
pattern1)
command...
;; # break
pattern2|pattern3)
command...
;;
patternN)
command...
;;
*) # default case
command...
esac
2.循环语句
有3种循环:for
、while
和until
,语法规则如下:
# for循环
for f in $( ls /var/ ); do
echo $f
done
# 或者单行的(分号区分结构块)
for f in $( ls /var/ ); do echo $f; done
# while循环
times=6
while [ $times -gt 0 ]; do
echo Value of times is: $times
let times=times-1
done
# 单行形式
times=6; while [ $times -gt 0 ]; do echo Value of times is: $times; let times=times-1; done
# until循环
times=0
until [ $times -gt 5 ]; do
echo Value of times is: $times
let times=times+1
done
除了for...in
,还有C风格的:
arr=(1 '2 3' 4)
len=3
for (( i=0; i<$len; i++)); do
echo ${arr[${i}]}
done
循环的基本规则:
循环会遍历由IFS(’ ‘、’\t’、’\n’)分开的条目
IFS(Internal Field Seprator),内部域分隔符,默认是空格、tab和换行,所以注意这种情况:
for f in $( ls -l /var/ ); do echo $f; done
输出的结果不符合预期:
total
0
drwx------
2
root
wheel
68
8
23
2015
agentx
本来应该是这样:
total 0
drwx------ 2 root wheel 68 8 23 2015 agentx
要循环读整行的话,需要修改IFS:
# 限制分隔符只认换行
IFS=$'\n'; for f in $( ls -l /var/ ); do echo $f; done
另外,循环通常配合通配符*
使用,例如:
# 通配符
echo * # 当前目录下所有文件/文件夹名,空格分隔
echo *.html # 当前目录下所有html格式文件
# 找test目录下所有html文件
for htmlFile in `echo ~/Documents/projs/test/*.html`; do echo $htmlFile; done
# 找test目录下所有html文件,包括子孙目录
for htmlFile in `echo ~/Documents/projs/test/**/*.html`; do echo $htmlFile; done
循环+通配符
操作目录文件非常方便
四.函数
1.函数声明
function function_name {
command...
}
# 或者
function_name () {
command...
}
省略function
关键字的话,函数名后面必须要有()
,否则就被当做命令了,例如:
# 报错,parse error near `}'
fn {echo fn}; fn
不省略function
关键字的话,函数名后面有没有()
无所谓
函数声明顺序不很严格,但要保证先声明后调用,例如:
fn1() {echo fn1:`fn2 $1`}
fn2() {echo fn2:$1}
fn1 hoho
# 输出
# fn1:fn2:hoho
如果在声明fn2
之前就调用fn1
,就会报错找不到fn2
,如下:
# 报错command not found: fn2
fn1() {echo fn1:`fn2 $1`}; fn1 hoho; fn2() {echo fn2:$1}
2.调用与传参
参数通过位置变量获得,不显式声明形参:
# 声明函数
fn() {echo $0$1}
# 无参调用
fn
# 传入一个字符串参数hoho
fn hoho
函数作用域内,提供了一些位置变量(都是只读的):
$0 # 函数名(不算参数,因为$*和$@不包含$0,$#也不计$0)
$n # 第n个参数,参数从1开始
$* # 由所有参数拼成的字符串,用空格分隔
$@ # 同上,区别是每个参数会被双引号保护起来
$# # 参数个数
P.S.注意$10
不是${10}
,前者是$1
后面跟个字符串0
,后者是第10个参数的值
另外,这些位置变量也适用于通过命令行向脚本传参,例如:
# sub.sh
echo $1-$2=`expr $1 - $2`
# 命令行执行
./sum.sh 1 2
# 输出
# 1-2=-1
$*
与$@
的区别很重要,简单理解:
$*=$1 $2 $3...
$@="$1" "$2" "$3"...
示例:
# 没有区别
fn1() {for arg in $*; do echo line:$arg; done}; fn1 "a" "b c" "d"
fn2() {for arg in $@; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 输出
# line:a
# line:b c
# line:d
# 用双引号包起来时能发现区别
fn1() {for arg in "$*"; do echo line:$arg; done}; fn1 "a" "b c" "d"
# 输出
# line:a
# b c
# d
fn2() {for arg in "$@"; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 输出
# line:a
# line:b c
# line:d
用双引号包起来时,循环次数有差异,$*
只循环一次,$@
循环3次,所以一般建议使用$@
3.返回值
3种方式都不好用,没事就不要返回了(直接修改外部变量),非要返回值的话,建议用子shell执行,echo
传回的方式,但注意事项(见代码注释)也很麻烦,示例如下:
# 1.return
fn() {
return -2
}
fn
# 取出上一条命令的返回值,0表示正常,非0不正常
# 实际输出是254,超出范围的会被框进来,256会变成0,-1变成255
echo $?
# 缺点:return表示函数执行状态,只能返回[0, 255]的整数,无法return字符串
# 其次`$?`必须紧跟在函数调用后面
# 2.子shell执行,echo传回
fn() {
echo -2
# 避免错误信息进入标准输出,直接丢掉
cat xxx 2> /dev/null
}
echo $(fn)
# 缺点:给标准输出的结果可能不干净
# (函数体不止一条`echo`语句,或者有`print`、`printf`之类的也输出到标准输出的语句)
# 另外,如果函数执行过程中出错了,错误信息也会混进去(虽然可以避免,但比较麻烦)
# 3.所谓的传引用
fn() {
# 约定第一个参数传入的字符串是返回变量名
local res=$1
# 计算1+2,再按照返回变量名创建全局变量,带回结果
eval $res=$(($2 + $3))
}
fn result 1 2
echo $result
# 缺点:其实就是全局变量传值,只是全局变量名动态传入,没有在函数里写死而已
五.数组
1.声明与赋值
# 空数组
arr=()
# 字符串数组,空格分隔元素,不用逗号
arr=(1 2 3 'we together')
# 直接赋值,没有就新增一个
arr[0]=4
arr[6]='sixth'
数组下标从0开始,赋值时不需要保证连续,没有的话会新增一个
2.遍历
for循环不用知道数组长度:
arr=(1 2 3 '4 5')
for i in "${arr[@]}"; do echo $i; done
特别注意"${arr[@]}"
,与函数里的位置变量类似,$*
与$@
的循环次数不同:
for i in "${arr[@]}"; do echo $i; done
# 输出
# 1
# 2
# 3
# 4 5
for i in "${arr[*]}"; do echo $i; done
# 输出
# 1 2 3 4 5
while和until需要知道数组长度:
# 取数组长度
len=${#arr[@]}
i=1
while [ $i -lt $len ]; do echo $arr[$i]; i=$((i+1)); done
# 或者until
until [ $len -lt 1 ]; do len=$((len-1)); echo ${arr[$len]}; done
注意:${#str}
是取字符串长度,与${#arr[@]}
取数组长度很像,容易弄错
六.命令替换
命令替换是指在bash脚本中执行shell命令,并得到其输出结果(差不多是这意思,没有找到严格定义)
直接执行的话,结果会被输出到标准输出(屏幕),想把结果取出来的话,就需要用到命令替换:
# 直接执行 屏幕输出了ls命令的结果
ls
# 命令替换 屏幕不输出结果,由自定义变量记下
lsResult=`ls`
# 看起来像是丢弃结果(不输出也不记)
# 实际上可能会报错,`ls`命令返回结果字符串,会被当作命令继续执行
`ls`
命令替换常用的方式有2种,反撇号扩展和圆括号扩展,例如:
# 反撇号扩展,不允许嵌套
files=`ls`
# 圆括号扩展,允许嵌套
files="$(ls)"
# 嵌套示例
# 当前目录下文件及文件夹数量+1
$(expr ${#$(ls)[@]} + 1)
有趣的一点:两种命令替换方式都是新建shell(也就是在子shell中)执行命令,不会对当前shell产生任何影响,所以可以方便的隔离操作环境,例如:
# 去新环境执行cd
lsParent="$(cd ../; ls)"
# 执行后pwd不变,不用再cd回来
到这里基本足够完成稍复杂的(有用的)bash脚本了