语法规则_bash笔记1

一.常识

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种循环:forwhileuntil,语法规则如下:

# 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脚本了

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code