shell脚本学习笔记:基本语法、流程控制、自定义函数

尽管shell脚本功能很弱,但常常用来组织其他脚本(如awk, gnuplot),所以决定较为系统地学习下。本文整理shell文本基本语法,流程控制,自定义函数。

1. 基本语法规则

1.1 创建与运行脚本

(1)创建

第一行#!/bin/sh指定解释器,尽管这个不是必须的(如用source命令执行脚本),建议还是加上。文件后缀名(通常是.sh)也不是必须的,但为了管理方便,建议加上。

(2)bash与sh的区别[9]

指定解释器语句#/bin/sh和#!/bin/bash的区别在于是否开启了bash的–POSIX标准模式,/bin/sh相当于/bin/bash –posix。所以,bash的一些特性在sh是没法使用的,比如用sh运行echo -e “TTT”会输出-e TTT,还有用sh运行array=(1 2 3)会提示Syntax error: “(” unexpected的错误。

(3)运行[2]

./test.sh, 值得注意的是,执行前需给脚本加上执行权限(chmod u+x test.sh),在子shell执行
sh test.sh, 与上一种情况类似,但加执行权限不是必须的,在子shell执行,脚本执行完子shell自动退出
source test.sh, 在当前shell执行,脚本设置的变量将成为当前shell的一部分
. test.sh, 点是source的另一个名称,与source一样,在当前shell执行

注:用export定义的变量会被复制到其子shell中。

1.2 注释和here文档

(1)注释

单行注释:用井号#。shell不支持多行注释,但存在多种变通方法[6]。如下:

#方法1:用here文档(冒号":"是指什么都不做),详情见后面
:<<BLOCK
...注释的代码
BLOCK

#方法2:if语句
if false ; then
...注释的代码
fi

(2)here文档

here文档格式如下(我的理解是,类似于管道的功能):

命令<<标识符
…给命令提供输入内容
相同的标识符

注:标识符用单引号或双引号括起来,表示禁用变量替换或者命令替换。

here文档在shell脚本有个重要的应用,在同一个shell脚本插入其他脚本(类似于内联函数),以下以插入awk脚本为例,相关源代码如下:

#定义awk脚本的临时文件名,shell脚本退出时删除该临时文件
awk_script_file="/tmp/scf_tmp.$$"
trap "rm -f $awk_script_file" EXIT

#利用here文档编写awk脚本内容
cat<<'EOF' > $awk_script_file
...awk脚本内容
EOF

#执行awk脚本
awk -f $awk_script_file > result.txt

1.3 空格的使用[3]

shell脚本对空格要求很严格,shell会认为空格前是一个命令,空格是命令解析的重要分隔符。主要限定如下:

(1)定义变量,等号”=”两边不能留空格,如age=22
(2)if条件测试语句,中括号”[“或”]”的两边都要留空格(除非接分号),如if [ $name = ‘Jelline’ ]
(3)if条件测试的内容,若比较的是字符串, 比较符号两边要留空格
(4)取变量值符号”$”,与其后的的变量或括号不能有空格,如$(age)

1.4 打印信息

(1)echo

我在使用过程中,发现在shell命令行运行echo,需要加-e选项,转义字符才能生效。但在shell脚本,不用加(也不能加)-e选项,转义字符照样生效。谁能解释下?如果输出含有转义字符的字符串,建议使用printf命令,可移植性好。

(2)printf
shell没有print语句(awk脚本有)? 格式化打印数据printf命令是echo命令的增强版,是C语言printf()库函数的一个有限的变形,但在语法上有些不同[7]。使用方法如下:

printf FORMAT [ARGUMENT]…

注:printf需要显式指定换行(不像echo自动换行)。

①printf格式指示符如下[7]

%%         表示百分号%
​​%b         处理对应字符串的转义字符,如printf "%b" "age\n"

%c         字符,若参数是字符串,只显示第一个字符
%s         字符串

%d(%i)     十进制整数,%i是老式写法
%u         无符号十进制整数,%ul表示长整型
%o         八进制整数
x(%X)     十六进制整数

%f         十进制浮点数,默认小数点后6位,不足用0填充
%e(%E)     浮点数指数,如8.9e+11
%g(%G)     选择%e(%E)和%f较短的来表示

②printf输出格式控制

格式控制在上述的格式指示符中间,如%-20s。其格式如下(中括号表示可选项):

[标志][域宽][.精度][l或h]

标志

       左对齐
+        右对齐(默认情况),当格式指示符是数值时,若为正数,在数值前面加上”+”(加号),并且右对齐
空格    若为正数,显示空格;若为负数,显示”-“
#        对于八进制%o,输出时加前缀”0″;对于十六进制%x,输出时加前缀0x

域宽m

输出项所占的字符数,若m小于实际字符数,则按实际字符数输出。

.精度n

如果是数值,表示保留小数点后几位;若是字符串,则只取字符串前n个字符,如printf “%.2” “abc”结果为ab。

注:m.n中的m/n可以用星号”*”代替,此时,作为一个参数输入,如printf “%*s”, 20

l或h

在C语言中,l对整型指长整型(如%ld),对实型指double型(%lf)。h将整型修正为短整型。但shell的printf不支持l或h。

1.5 变量赋值及使用

shell的变量没有数据类型,所有变量均视为字符串,但其数据类型会自动转换(对变量整数运算,视变量为整数;对变量字符串操作,视变量为字符串)。变量赋值,有两种,如下:

显示赋值,如age=13,值得注意的是,等号两边不能留空格
用语句给变量赋值,如for name in ‘Jelline’

使用变量,在变量名前加取值符”$”,如$age,当变量名后面还有其他字符时,需要给变量加上花括号以帮助解释器识别变量边界,如${age}th. 否则$ageth被解释成字符串$ageth

(1)数组定义

shell只支持一维数组(bash支持,sh不支持),但元素个数没有限制,数组下标从0开始。有多种形式定义数组,如下:

array=(val0 val1 val2 val3 ...)

array=(
val0
val1
...
)

array[0]=val0
array[1]=val1
...

(2)数组使用

取得数组特定元素:val_i=${array[i]}
取得数组所有元素:${array[@]} 或 ${array[*]}
取得数组元素个数:${#array[@]} 或 ${#array[*]}
取得数组单个元素的长度:${#array[i]}

1.6 单引号、双引号、反引号、反斜扛[4][5]

单引号

单引号间的内容全部解释为普通字符,忽略所有特殊符号(如$,\,`)。如’$age’被视为$age。

双引号

双引号忽略大多数特殊字符,但不忽略三种特殊字符(即$,\,`)。

反引号

反引号“,用于命令替换,等同于$(…),如`pwd`等同于$(pwd)。这样的话,shell命令执行的结果就可以保存在一变量中,如lines=$(wc -l filename).

反斜扛

反斜扛用于转义字符和续行,echo要让转义字符生效,需加-e选项,并且转义字符使用双引号括起来,如echo -e “\n”。echo常用转义字符如下(因版本而异):

\a     响铃 alert(BEL)
\\     反斜扛 backslash
\'     单引号
\"     双引号
\0     空字符

\b     退格 backspace
\c     不换行 produce no further output
\n     换行 new line
\r     回车 carriage return
\t     水平制表 horizontal tab
\v     垂直制表 vertical tab

1.7 内部参数[8]和参数传递

(1)内部参数

shell内置了一些参数,如下:

$#    传递给程序的总参数数目
$0    当前程序名称,如./test.sh
$n    表示第几个参数,从$1开始,大于$9的,数字用花括号括起,如${11}
$*    传递给程序的所有参数组成的字符串
$@    以"参数1" "参数2" ...形式保存所有参数

$?    上一个shell命令或程序在shell中退出的情况,若正常退出返回0,否则为1
$$    本程序的进程ID号PID,可用于创建临时文件
$!    上一个命令的PID

(2)参数传递

shell脚本参数就可以使用诸如./test.sh para1 para2 …进行传递,在脚本中用$i进行取用;
shell命令或者其他脚本,运行结果传给shell,用命令替换将结果保存在变量中,如n=$(wc -l filename)

1.8 数值运算

shell脚本进行数值运算需要借助expr命令或者(())。

(1)expr命令

expr命令对表达式求值(evaluate expressions)并打印出来,命令格式如下:

expr EXPRESSION

#注:加号+两边需要空格
echo $( expr $i + $j )

expr只支持整数(不支持符点数)的一些简单运算,字符串比较和匹配的一些简单功能,详情可以man expr。

(2)(())

(())像是一个算术运算命令,也不支持符点数,使用方法如下(加号前后,可加也可不加空格):

echo $(( i+j ))

2 流程控制

2.1 if

if语句的格式如下(只有第一行和最后一行是必须的),值得注意的是执行语句不能为空,但可以用冒号:(表示什么都不做)代替。

if TEST-COMMANDS; then 
    ... 
elif TEST-COMMANDS; then
    ...
else
    ...
fi

TEST-COMMANDS测试命令test expression可以用 [ expression ] 来代替([是一个命令,后面expression是参数,所以用空格隔开。[]命令实际上是test命令的另一种形式[10])。test命令用于比较数值和检查文件类型(check file and compare values)。

(1)多个条件

! EXPRESSION
EXPRESSION1 -a EXPRESSION2    相当于&&,或者是 [ 条件1 ] && [ 条件2 ]
EXPRESSION1 -o EXPRESSION2    相当于||,或者是 [ 条件1 ] || [ 条件2 ]

(2)比较数值

下面列出test支持的比较符,前上部分针对整数(integer,那符点数怎么办?),后半部分针对字符串。

-eq    等于 equal to 
-ne    不等于 not equal to
-gt    大于 less than
-lt    小于 less than or equal to 
-le    小于等于 greater than
-ge    大于等于 greater than or equal to

=      两个字符串相等
!=     两个字符串不相等
-z     空串(-z在变量之前, 如 if [ -z $str ])
-n     非空串(-z在变量之前)

(3)检查文件类型

F1 -ef F2    have the same device and inode numbers
F1 -nt F2    F1 is newer (modification date) than F2
F1 -ot F2    F1 is older than F2

-b FILE        FILE exists and is block special
-c FILE        FILE exists and is character special
-d FILE        FILE exists and is a directory
-e FILE      FILE exists

-f FILE        FILE exists and is a regular file
-g FILE        FILE exists and is set-group-ID
-G FILE        FILE exists and is owned by the effective group ID
-h FILE        FILE exists and is a symbolic link (same as -L)
-L FILE        FILE exists and is a symbolic link (same as -h)
-k FILE        FILE exists and has its sticky bit set
-O FILE        FILE exists and is owned by the effective user ID
-p FILE        FILE exists and is a named pipe
-r FILE        FILE exists and read permission is granted
-s FILE        FILE exists and has a size greater than zero
-S FILE        FILE exists and is a socket
-u FILE        FILE exists and its set-user-ID bit is set
-w FILE        FILE exists and write permission is granted
-x FILE        FILE exists and execute (or search) permission is granted
-t FD          file descriptor FD is opened on a terminal

TEST-COMMANDS除了可以使用[]命令,还可以使用[[]]。[[]]是[]的扩展命令(bash内建命令),基本上与[]用法一致,并且更强大也更符合程序员的使用习惯[10]

2.2 for

常用的for循环有以下两种:

#变量i顺序访问列表中的变量,如for file in *.txt
for 变量i in 列表 
do
    ...  
done  

#在(( ))中引用shell变量不需要加$
sum=10  
for ((i=0; i<sum; i++))
do  
    echo $i  
done

2.3 while

#如读取文件cat $file | while read buffer
while 条件
do  
    ...
done  

2.4 until

#条件为假时执行
until 条件
do
    ...
done

3. 函数

可以在shell脚本自定义函数,直接给例子吧[11]。不需要在函数定义声明参数,在函数内部直接使用$i地参数进行操作。

function copyfile()
{
    local srcfile=$1
    local dstfile=$2
    cp $srcfile $dstfile
    return 0   #返回值不是必须的
}

#函数调用
copyfile /tmp/myconf /etc/sysconf


参考资料:

[1]Shell脚本编程30分钟入门
[2]SHELL脚本运行的几种方法以及区别
[3]shell 编程中空格的使用 
[4]shell中单引号、双引号、反引号的使用
[5]shell中单引号、双引号、反引号、反斜杠的使用
[6]Shell注释多行的方法
[7]shell printf命令:格式化输出语句
[8]博文《linux shell 脚本之【1】参数
[9]博文《深入了解bash与sh的区别
[10]博文《Shell脚本基本流程控制语句总结
[11]博文《Linux新手生存笔记[10]——shell脚本基础3-函数及常用命令

Leave a Reply

Your email address will not be published. Required fields are marked *

2 thoughts on “shell脚本学习笔记:基本语法、流程控制、自定义函数