恩,这是由奇技淫巧组成的语言,没有之一。奇技淫巧到,他的语法出现在你无数个灵光一现时刻~
我:蒋哥你shell好厉害!
蒋哥:恩
我:要多久啊
蒋哥:(基于Java,python,js等)要一段时间
一、理解shell、变量、环境变量
shell,壳,计算机命令终端,每个unix体系的机器最原始都是使用这个与计算机操作系统进行交互的,类似的,windows操作系统下满就是bat(批处理文件)。每次Linux系统启动,登陆之后,都会默认启动几个这种命令终端,如果图形界面有的话,一般会默认进入图形界面。图形界面之上,我们也可以使用Terminal这种模拟的命令终端,进行命令操作,这东西类似于windows的cmd。
1、变量
对于编程这么多年,变量很好理解,无非就是一个瞎几把命名的一个单词,赋值一个value嘛~真的吗?奇技淫巧来了,请看下面
# ①
var1=123
# ②
var2='jicheng${var1}'
# ③
var3="jicheng${var1}"
# ④
var4=`pwd`
# ⑤
var4 = `pwd`
# ⑥
var4=$(pwd)
①shell变量没类型,弱类型,万物皆字符串,即使是这个,也是var1值是"123"
②③单双引号不一样,单引号内部不转义,写啥是啥;双引号会转义,会替换变量值
④这种上引号是执行引用起来的字符串,所以被引用的要能被执行,结果赋值给等号左面的变量
⑤这种是错误语法。赋值操作的等号两边不能有任何空格,这种有空格的,后面会将
⑥和④一样
完了?远远还没,来点晋级的:
# ①
var4=${var1:-value}
var4=${var1-value}
# ②
var5=${var1:=value}
var5=${var1=value}
# ③
var6=${var1:?value}
var6=${var1?value}
# ④
var7=${var1:+value}
var7=${var1+value}
解说:不带冒号代表var1是否为未赋值,带了代表var1是否为未赋值或是空值
①var1未赋值(或空值)将"value"赋值给var4变量
②var1未赋值(或空值)将"value"赋值给var4,且同样赋值给var1
③var1未赋值(或空值)将"value"作为标准错误输出,用于进行变量判空用的
④var1未赋值(或空值),什么都不做,否则value代替var1的值,赋值给var7,var1的值不变
2、shell脚本内部变量
一个真正的shell脚本在执行过程中,会有一些特殊的变量,这个对理解shell程序至关重要,来战:
#!/bin/bash
# shell_test.sh脚本
echo $0
echo $(basename $0)
echo $1 $2
echo $#
echo $*
echo "$*"
echo $@
echo "$@"
-
$0:
获取执行脚本自己的文件名,执行的输入全字符串,例如./shell.sh、/etc/profile_test.sh,如果想单纯获取名字,要用:
$(basename $0)
-
$1,$2,$3……..$n这种
回去执行名称后面对应的输入参数,$1代表第一个,$n代表第n个
-
$#
获取输入参数一共有多少个
-
$*与$@
不加引号,两个一样:都是将入参使用IFS定义的分隔符进行拆分;加了就有区别:$*会把所有入参当做一个整体的字符串,而$@会将使用双引号的入参当做一个,不进行IFS分隔符拆分操作。下面代码是两个变量的对比试验:
#!/bin/bash
# 对$*与$@做对比试验
# 将带引号的入参进行IFS分割拆分
for i in $*;
do
echo $i
done
# 恩,这就是一个大的字符串
for i in "$*";
do
echo $i
done
# 将带引号的入参进行IFS分割拆分
for i in $@;
do
echo $i
done
# 不将带引号的入参进行IFS分割拆分
for i in "$@";
do
echo $i
done
3、系统+环境变量
每次运行shell脚本,或者我们开启一个Terminal,或者我们远程登陆一个命令终端,其实都是有预先设置好的变量存在的,这个就叫:环境变量。如果是全局的环境变量,就是说,每个用户登录都能看到的,可以说是全局变量,类似于windows里面的系统变量的概念;如果环境变量只是当前用户能够看到的,那就是局部环境变量,类似于windows里面的环境变量的概念。内部有几个很关键的脚本文件,专门用来设置全局与局部变量的,大致总结下:
a、全局环境变量设置(系统变量)
- /etc/profile:最早启动,在系统登录的时候就进行调用,其后都不会调用的设置环境变量的脚本。其中会调用执行下面的下面的每一个文件
- /etc/profile.d/*.sh:被1文件调用的各个文件,如果要设置全局的系统环境变量,最好分类别分别写个.sh可执行文件放入这个文件夹,系统启动之时会被调用执行,设置环境变量
下面是profile的源代码:
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
if [ "`id -u`" -eq 0 ]; then
# id -u 显示登陆用户id,0是root用户
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
else
PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
fi
export PATH
# 下面的判断语法是不是很熟悉呢?
# 这里主要进行PS1这个环境变量的设置,主要影响交互式命令号行头标示符
if [ "${PS1-}" ]; then
if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
if [ -f /etc/bash.bashrc ]; then
# 到这里就是判断当前是不是不是使用sh且存在bash.bashrc文件
. /etc/bash.bashrc
fi
else
# sh模式(不是bash模式),就是所谓的POSIX兼容模式
if [ "`id -u`" -eq 0 ]; then
PS1='# '
else
PS1='$ '
fi
fi
fi
if [ -d /etc/profile.d ]; then
# 第一个判断文件夹是否存在
for i in /etc/profile.d/*.sh; do
# 遍历文件夹中的每个文件
if [ -r $i ]; then
# 判断单个文件是否存在,执行
. $i
fi
done
unset i
fi
b、局部用户环境变量设置
下面针对用户环境变量设置是按顺序,找到的就会执行,没找到就不执行:
- $HOME/.bash_profile:有可能存在,针对个人用户设置用户级别的环境变量
- $HOME/.bash_login:有可能存在,针对个人用户设置用户级别的环境变
- $HOME/.profile:有可能存在,针对个人用户设置用户级别的环境变
一般情况下,会只存在一个。例如,Debian系统下面,用户目录下面只有.profile。可是内部又调用执行类另外一个同目录的文件:.bashrc。下面是真是的源码:
# ~/.profile: executed by Bourne-compatible login shells.
if [ "$BASH" ]; then
# 如果BASH变量存在就执行这个代码块
if [ -f ~/.bashrc ]; then
# 如果.bashrc这个文件存在在用户目录中,就执行这个代码块
. ~/.bashrc
# 这种的另一种写法是:source ~/.bashrc
fi
fi
mesg n || true
# ~/.bashrc: executed by bash(1) for non-login shells.
# 这里都注释掉了,认真阅读可见,我们平时常用的命令简写都出自这里,只不过这个系统定制性的给去掉了
# Note: PS1 and umask are already set in /etc/profile. You should not
# need this unless you want different defaults for root.
# PS1='${debian_chroot:+($debian_chroot)}\h:\w\$ '
# umask 022
# You may uncomment the following lines if you want `ls' to be colorized:
# export LS_OPTIONS='--color=auto'
# eval "`dircolors`"
# alias ls='ls $LS_OPTIONS'
# alias ll='ls $LS_OPTIONS -l'
# alias l='ls $LS_OPTIONS -lA'
#
# Some more alias to avoid making mistakes:
# alias rm='rm -i'
# alias cp='cp -i'
# alias mv='mv -i'
二、字符串与数字运算
对于日常的使用,最常见的莫过于字符串拼接,与数字的运算,下面分别来说说这两方面
1、字符串拼接
#!/bin/bash
str1=hello
str2=shell
str3=world
echo $str1$'\t'$str2$'\t'$str3
# 输出为:hello shell world
- shell中没有所谓的“+”连接符,直接用啥放啥,直接放!
- 中间不能有空格,否则字符串会被分割拆分
- 拆分字符串的环境变量是IFS,默认值为:IFS=$' \t\n'
- 奇技淫巧又来了:上面介绍过单引号里面不转义,而这里又转义了呢?f**c!关键点在于这里的单引号前面多了个$。下面是bash官方文档针对这一段的解释,我翻译了一下
$'string'这种类型的值会被特殊解析。这种值,内部使用反斜杠加一个字符,都会按照标准c的模式进行转义。如果存在这种反斜杠转义序列, 映射关系如下:
/a 警告(铃声)
/b 退格
/e 一个转义字符
/f 换页
/n 换行
/r 回车
/t 水平制表符
/v 垂直制表符
// 反斜杠
/' 单引号
/nnn 八进制
/xHH 十六进制
/cx 控制x字符(没弄明白这个什么时候用)
2、算术运算
shell本身就是万物皆字符串,所以对于纯数学计算支持的不是很好。大体上分两大类:整数运算与浮点数运算。实践的方式有三种,前两种是整数运算,最后一种是浮点数运算
a、使用expr
#!/bin/bash
# An example of using the expr command
var1=10
var2=20
var3=$(expr $var2 / $var1)
echo The result1 is $var3
var3=$(expr $var2 \* $var1)
echo The result2 is $var3
这东西不好使用,而且是非常。其中还会支持如下的一些操作:
- ARG1 | ARG2:两个参数谁不为空(被赋了值),结果就是谁,都不为空,结果为ARG1,否则结果为0
- ARG1 & ARG2:两个参数都不为空(被赋值了),结果为ARG1,否则为0
- ARG1 < ARG2
- ARG1 <= ARG2
- ARG1 = ARG2
- ARG1 != ARG2
- ARG1 >= ARG2
- ARG1 > ARG2
这东西,如果是取结果值,赋值给其他变量的话,经过实际的测验,还有一些猫腻,请仔细看下面代码与注释:
#!/bin/bash
# 测试expr取值操作
expr $var \| 3 #如果是没被赋值参与了运算的,这时候会报错
$var=
expr $var \| 3 #这种被赋了个空值,也会报错
$var=1
expr $var \| 3 #这种才不会报错,按照正常逻辑返回值:1
$var=
expr "$var" \| 3 #这种也不会报错,直接取了var的空值,结果为:3
expr 3 | 2 #不转义“或”符号报错,shell会当做管道符号处理,而2并不是命令
expr 4\*3 #这种直接返回结果:4*3(字符串)
expr 4\* 3 #这种语法错误,操作符的两边必须要有空格
可见shell下面的算术运算操作并不容易,非常多的小细节,恩就是所谓的“奇技淫巧”!
b、使用中括号:$[ARG1*ARG2]
中括号主要是为了取代expr而出现的,主要也是用于整数的算术运算与逻辑运算,并且能够方便的取值操作。毕竟,使用expr有太多奇技淫巧,太多要注意的语法细节了,而中括号,恰恰能很好的解决这些。下面是一些语法举例:
#!/bin/bash
# 中括号算术运算举例
[4*4] #单独这样会有语法错误,因为中括号语法要配合$来使用,报错信息:[4*4]:未找到命令
[ 4*4 ] #这种其实没有语法错误,中括号里面两边有了空格这种,属于判断语句,下面会介绍到
[4 * 4] #这种操作符有了空格,会被命令解释器拆分成三个命令:[4、*、4],而[4并不是命令,所以报错
$[4*6] #这种表达式本身是没问题的,能够求结果,可是结果求出来了shell会运行结果,显然24不是命令,报错
var=$[3 *4] #这种是最正确的,操作符左右不用担心空格、也不用担心转义问题,求结果,并赋值操作
仔细阅读上面注释,就可以了。是不是感觉shell非常死扣一些细节?这个脚本语言就这个德行!
c、浮点数解决方案:bc
恩,最后了,一步步闯关之后,这东西是“公主”!最好控制的东西(就是奇技淫巧少)。好控制不代表好用,来看看bc两种模式
①交互模式
bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该 表达式,最后返回结果。bash计算器能够识别:
- 数字(整数和浮点数)
- 变量(简单变量和数组)
- 注释(以#或C语言中的/* */开始的行)
- 表达式
- 编程语句(例如if-then语句)
- 函数
下面是我一顿瞎几把搞的代码:
jicheng:~/Project/shell_test$ bc -q #q是不显示软件介绍
34.5
23*34.645645
796.849835
234*3-(3/ 234)#支持混合运算
702
3/ 234
0
3.0/ 234.023
0
scale= 6 #默认是0,不设置,小于1的除法结果显示0
3.0/ 234.023
.012819
3/ 234
.012820
var1=10 #内部支持变量
var1 * 4
40
var2 = var1 / 5
print var2
2
quit
jicheng:~/Project/shell_test$
②脚本中使用bc
基本语法为:variable=$(echo "options; expression" | bc)
#!/bin/bash
var1=100
var2=45
var3=$(echo "scale=4; $var1 / $var2" | bc) # 看到如何设置精确值
echo The answer for this is $var3
三、输入输出与重定向
这两个话题也是一大难。哎~难点在于很多很多的细节,还是那样,难在正所谓的"奇技淫巧",我们分开2小节来讲:
- 输入
- 输出与重定向
1、输入
对于一个计算机语言来说,输入,无非就是:用户交互式输入与文件内容的输入。Shell里面也一样。针对用户的输入,可以从命令传入,也可实时读取用户的外设输入。而针对这些个输入,shell会有相应的读取与保存方式,下面分开来介绍
a、命令传参式输入
在执行命令时候,默认以空格分割,紧接着在命令后面进行传入的连续字符串。分割方式是空格,如果入参本身带空格,要使用引号或者双引号,进行引用包裹
#!/bin/bash
# ./shell_test var1 var2 var3
#正确获取文件名的真实写法
echo `basename $0`
echo $1
echo $2
echo $3
# 一共有多少个入参
echo $#
# 最后一个入参
echo ${!#}
大部分知识都在上面变量小节介绍过了,上面关键的是最后两行代码:
- $#返回当前入参是多少个,如果没有命令执行后面的入参,那值为0
- 虽然目测可以通过${$#}获取最后一个参数,可是,问题是不行,要通过使用${!#}获取
b、"一波带走":处理输入的方式
有时候,我们会动态获取输入参数,并循环做处理,例如下面的方式:
#!/bin/bash
# shell_test.sh文件
count=1
for var in "$@"
do
echo "Parameter #$count: $var"
count=$[ $count + 1 ]
done
# 运行:./shell_test var1 var2
# 结果:
# Parameter #1: var1
# Parameter #2: var2
很简单。再高一个难度:如果我们想对入参进行分类,区分配置选项与输入参数两种,例如我们经常使用的
ls -al
不就是有配置选项al。看下面我们怎么用实际的代码进行实现:
#!/bin/bash
# shell_test.sh文件
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option";;
-b) echo "Found the -b option";;
-c) echo "Found the -c option";;
*) echo "$1 is not an option";;
esac
shift
done
<<COMMENT
输入 /shell_test.sh -a -c -b -d
结果:
Found the -a option
Found the -c option
Found the -b option
-d is not an option
COMMENT
shift是shell内部的一个命令,可以左移一个输入参数,被移动的入参,将会消失,例如,入参有var1、var2,shift之后,入参只有var2。shift可以有参数,后面加上一个数字,表示要左移多少个参数。上面的代码用到了结构化的语句,后面章节会进行介绍。接下来我们再高一个级别:我不仅仅想要配置选项的入参,我还想要配置参数的入参,那我们怎么定义呢?针对输入方式,我们可以用一个特殊字符来隔离选项与参数,例如'—',看下面:
#!/bin/bash
# shell_test.sh
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option";;
-b) echo "Found the -b option";;
-c) echo "Found the -c option";;
# 这里做了特殊处理:shift掉了--入参,并结束while循环
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
count=1
for var in "$@"
do
echo "Parameter #$count: $var"
count=$[ $count + 1 ]
done
<<COMMENT
输入:./shell_test.sh -a -b -c -- var1 var2 var3
输出:
Found the -a option
Found the -b option
Found the -c option
Parameter #1: var1
Parameter #2: var2
Parameter #3: var3
COMMENT
已经有点规模了,哈哈。接下来我们还想进一步,ls -al
类似于这种,人家成熟的命令可是可以合并配置选项的,可是上面的代码并不支持。接下来就是最终的解决方案,要用到一个很好的命令:getopts。下面是代码,边写边解说:
#!/bin/bash
while getopts :ab:c opt
do
case "$opt" in
a) echo "Found this -a option";;
b) echo "Found this -b option,with value $OPTARG";;
c) echo "Found this -c option";;
*) echo "Unknow";;
esac
done
shift $[ $OPTIND -1 ]
count=1
for param in "$@"
do
echo "Parameter $count: $param"
count=$[ $count+1 ]
done
<<COMMENT
输入:./shell_test -ab var1 -c var2 var3
输出:
Found this -a option
Found this -b option,with value var1
Found this -c option
Parameter 1: var2
Parameter 2: var3
COMMENT
一波解释:
- getopts能够保存所有的入参,根据指定的选项来对入参进行格式化:getopts optstring variable
- optstring配置选项的,选项后面加冒号代表选项有参数要解析。最开始加冒号表示去掉错误信息
- getopts每次都保存一个配置选项,我们要循环处理
- 如果这个选项入参有参数值的话,$OPTARG就是用来存储这个的
- $OPTIND用来保存当前getopts正在处理的参数位置
至此我们对于命令传参式输入就全介绍完了,下面开始说用户交互式输入
c、交互式输入
这种比较简单一些,就是用read这个命令一顿瞎几把搞~直接上代码,直接解释:
#!/bin/bash
# 最基本的read,阻塞式的输入,用echo的n(不换行)参数配合打印提示行
echo -n "Enter your name: "
read name
echo "Hello $name, welcome to my program. "
# 使用read的p参数,直接打印提示航
read -p "Please enter your age: " age
days=$[ $age * 365 ]
echo "That makes you over $days days old! "
# 使用read的t参数,来进行超时控制,单位是秒
if read -t 5 -p "Please enter your name: " name
then
echo "Hello $name, welcome to my script"
else
echo "Sorry, too slow! "
fi
# 使用read的s参数,可以隐藏输入的字符,比如密码输入
read -s -p "Enter your password: " pass
echo "Is your password really $pass? "
d、文件中输入
文件中的输入,主要还是运用read这个命令,也是直接代码:
#!/bin/bash
# reading data from a file #
count=1
# test是一个文本文件
cat test | while read line
do
echo "Line $count: $line"
count=$[ $count + 1]
done
echo "Finished processing the file"
line是记录了每一行的记录,我们可以根据需求,继续对每行的数据进行切割处理
2、输出与重定向
在shell中,输出永远和重定向脱不开干系,因为在shell中无论是输出到屏幕(标准输出)或是输出到文件,都是要靠重定向标准输出来 实现的,所以掌握各种重定向的方式,很关键,同样,这里也是好几种“奇技淫巧”的方式进行重定向。下面我分别说说几个方面。
a、文件描述符
在运行shell脚本的时候,会有一系列的文件描述符,用数字表示,最多有9个,最常见的就是系统设定好的前三个:
- 0:标准输入(STDIN):默认从键盘读取数据
- 1:标准输出(STDOUT):默认输出到屏幕
- 2:标准错误输出(STDERR):默认输出到屏幕
我们可以重定向这几个,同时也可以自己创建新的文件描述符:
#!/bin/bash
# 默认情况的echo,直接输出到标准输出
echo "echo 2 STDOUT"
# 我们可以进行重定向标准输出,这样就会输出到具体的文件当中去
echo "echo 2 other" >test.out
## 双大于号的输出表示追加到一个文件,不是覆盖
echo "echo 2 other" >>test.out
# 下面的jiad是没有的目录,标准错误会输出到屏幕
ls /jiad
# 对标准错误输出进行重定向,直接在大于号前面加数字
ls /jiad 2>test.out
# 全局性永久重定向,使用exec命令原理是启动一个新的shell然后进行重定向
exec 2>test.out
ls /jiad #这种错误会直接输出到test.out文件,因为标准错误全局性的重定向了
# 这种就是自定义文件描述符3,然后重定向到test.out文件
exec 3>test.out
# 引用文件描述符的方式,这样输出的结果就覆盖到了test.out文件里面了
echo "echo 2 other" >&3
上面主要的语法都已经列出来,仔细阅读注释就可以了。另外,这地方对重定向的箭头左右是否有空格,要求不是特别的严格,有没有功能都不会受影响,请放心使用。
b、“暂存”文件描述符
当我们使用exec命令永久性的重定向了标准输出的话,如何再重定向回来原本的呢?也很简单,就如我们最开始学习编程时候学的“三段式”交换数字一样,先暂存,再操作,最后再反赋值就可以了,就如下面:
#!/bin/bash
exec 3 >&1
exec 1 >test.out
echo "temp save file description"
# 看到没?直接把1再重定向会3,3里面,最开始是重定向到标准输出的
exec 1 >&3
c、输入重定向
输入的文件 标示符是0,重定向使用小于号,简单的一个重定向代码如下:
#!/bin/bash
# 0和符合之间不能有空格,其实他们是一体的,表示:0文件描述符输入重定向
exec 0< out
# 这种情况,cat并不会进行交互式输入了,而会从out文件进行输入
cat
同样,我们可以通过暂存的方式进行恢复重定向的标准输入
#!/bin/bash
# 下面的小于号左右不能有空格存在,否则语法报错,&符号与前面不能有空格
exec 3<&0
exec 0<out
# 这里的cat的输入已经变成了out文件
cat
# 这个命令可以查询0,1,2,3几个文件描述符的指向文件
lsof -a -p $$ -d 0,1,2,3
# 恢复标准输入的文件描述符
exec 0<&3
lsof -a -p $$ -d 0,1,2,3
# 这个时候会变成交互式输入
cat
在输入重定向这里,有个"奇技淫巧",就是我们能够脚本中动态设置标准输入(重定向),而不用非得使用原生的标准输入或是文件输入,语法如下:
command << delimiter
document
delimiter
具体我们举一个例子:
#!/bin/bash
# read file and create INSERT statements for MySQL
outfile='members.sql'
IFS=','
while read lname fname address city state zip
do
# 注意这个地方
cat >> $outfile << EOF
INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('$lname', '$fname', '$address', '$city', '$state', '$zip');
EOF
done < ${1}
上面的cat,其实是直接在命令行执行cat这种模式的,只不过在执行的时候,重定向了两个东西:
- 输出:使用双大于号方式,进行追加式到文件member.sql文件中
- 输入:使用了上述的语法,进行了脚本命令式重定向,输入就是两个EOF中间的内容
很有意思,这种方式,很广泛被运用,例如:shell脚本对mysql数据库执行一条sql语句
四、结构化语句
说到了最后,这一章说完,几乎shell的语法部门就结束了。剩下的,就是深入大海般的”命令海“!这些命令,有系统自带的,也有我们自己安装的工具带的命令,或是我们安装的软件带来的命令,例如git、docker等。针对结构化的问题,也算是放轻松了,因为结构化的语法,是平时我们最最常见的了。
1、ifelse
这两个东西,在任何计算机语言体系中,可谓是老生常谈了。不多说,下面是基本语法,有两种模式:
# 第一种模式
if command
then
command1
command2
......
fi
# 第二种模式
if command; then
command1
command2
......
fi
# 多级别的ifelse
if command1
then
commands
elif command2
then
more commands
else
more commands
fi
特别注意点:shell中没有所谓的判断真(true)假(false)一说,这里的if语句中的判断语句是一个具体的命令(command)。根据命令的执行退出的状态值来判断是否要执行if里面的代码块: 如果这个命令正确执行,那退出值为0,if代码块就被执行;如果命令执行过程中发生错误,退出值就不为0,那if代码块就不被执行。这就是shell中的语法。后面的while、for、util等都是这种规则
下面就是基本的shell中if语句的代码:
#!/bin/bash
# Testing nested ifs - use elif & else
#
testuser=NoSuchUser
if grep $testuser /etc/passwd
then
echo "The user $testuser exists on this system."
elif ls -d /home/$testuser
then
echo "The user $testuser does not exist on this system."
echo "However, $testuser has a directory."
else
echo "The user $testuser does not exist on this system."
echo "And, $testuser does not have a directory."
fi
2、ifelse常规使用
虽然ifelse的判断语句是运行一个命令,然后根据退出值来判断的,但是能不能让我们还是像以往那样,根据语句的真假来判断是否执行语句块呢?恩,test命令就是解决这个问题的:
if test condition
then
commands
fi
#!/bin/bash
my_var='jicheng'
if test $my_var
then
# 这个会被执行,因为test命令会让退出值为0
echo "myvar"
fi
my_null_var=""
if test
then
# 空的test的退出值不为0,所以这个地方不会被执行
echo "1"
elif test my_null_var
then
# test空串也是会退出值不为0的,所以这里也不会执行
echo "2"
else
echo "3"
fi
当然,shell中不会傻呵呵让你一直写test的,提供了一个替换语法:
# 注意这里:中括号内部的condition两边必须要有空格,否则语法有错,上面立的flag,在这里可以拔下来了
if [ condition ]
then
commands
fi
针对性的,test的语法,可以对三种类型进行条件判断下面分三个小节
a、数值比较
由于shell中万物皆字符串,所以对于数值的比较,要使用关键字进行,不难,下面就是:
- n1 -eq n2:相等
- n1 -ge n2:大于等于
- n1 -gt n2:大于
- n1 -le n2:小于等于
- n1 -lt n2:小于
- n1 -ne n2:不相等
下面是简单的例子:
#!/bin/bash
value1=10
value2=11
#
if [ $value1 -gt 5 ]
then
echo "The test value $value1 is greater than 5"
fi
#
if [ $value1 -eq $value2 ]
then
echo "The values are equal"
else
echo "The values are different"
fi
注意shell中只能处理整数,对于浮点数的test,会报错。不支持
b、字符串比较
其实也不难,主要注意和sort的区别。下面是基础的一些字符串比较表达式:
- str1 = str2
- str1 != str2
- str1 < str2
- str1 > str2
- -n str1:检查str1的长度是否非0
- -z str1:检查str1的长度是否为0
注意点有几个:
- 注意符号的两边要有空格,否则就是赋值操作了!(前面的flag又拔了)
- 大于和小于要进行转义,否则就是重定向啦!
[ "test" \> "Test" ]
- sort命令对字符排序是根据本地系统的语言设置排序的,小写出现在大写前面
- 这里的test比较,是根据ASCII顺序的,大写是小于小写的
下面是简单的测试代码:
#!/bin/bash
# testing string sort order
val1=Testing
val2=testing
#
if [ $val1 \> $val2 ]
then
echo "$val1 is greater than $val2"
else
echo "$val1 is less than $val2"
fi
c、文件比较
最后一类比较测试很有可能是shell编程中最为强大、也是用得最多的比较形式。它允许你测 试Linux文件系统上文件和目录的状态:
- -e file:存在
- -s file:存在且非空
- -f file:存在且是一个文件
- -d file:存在且是一个目录
- -r file:存在且可读
- -w file:存在且可写
- -x file:存在且可执行
- -O file:存在且属于当前用户
- -G file:存在且属于当前用户组
- file1 -nt file2:file1比file2新
- file1 -ot file2:file1比file2旧
d、可以使用符合条件表达式
- [ condition1 ] && [ condition2 ]
- [ condition1 ] || [ condition2 ]
#!/bin/bash
if [ -d $HOME ] && [ -w $HOME/testing ]
then
echo "The file exists and you can write to it"
else
echo "I cannot write to the file"
fi
e、双小括号与双中括号
双小括号模式是(用于算术运算比较):
(( expression ))
支持:var++、var--、++var、--var、!(非)、~(位求反)、**(幂运算)、<<(左移)、>>(右移)、&(位与)、|(位或)、&&(逻辑与)、||(逻辑或)。另外这种模式下大于小于号不用转义!
双中括号的模式是(用于字符串比较):
[[ expression ]]
双方括号里的expression使用了test命令中采用的标准字符串比较。但它提供了test命 令未提供的另一个特性——模式匹配(pattern matching)。
#!/bin/bash
if [[ $USER == r* ]]
then
echo "Hello $USER"
else
echo "Sorry, I do not know you"
fi
多说两句:在这里,对于这两个东西,可以玩的花样有点多,日常使用的也会总去使用这两个运算符的花样,这个我在后面单独开文章细讲”奇技淫巧“!
3、case使用
一般高级静态语言中,都有switch的语法,shell中也有。不过这里的使用方式,有点"诡异":
case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac
和Java中不同,这里每个匹配了但括号里面的模式并执行了command之后,就结束case语句了,而且使用两个;符号结尾。而Java中不使用break的话,会一直匹配下面所有的模式并执行里面代码块。
#!/bin/bash
# using the case command
#
case $USER in
rich | barbara)
echo "Welcome, $USER"
echo "Please enjoy your visit";;
testing)
echo "Special testing account";;
jessica)
echo "Do not forget to log off when you're done";;
*)
echo "Sorry, you are not allowed here";;
esac
<<COMMENT
输出:
Welcome,
rich Please enjoy your visit
COMMENT
4、for语句
for语句在shell中,有两种方式,一种是原生的,一种是C语言风格的。但是C风格的for语句,在一些bash中并不是很支持(ubuntu16中的dash就不支持),所以我看,大部分还是使用原生的for语句为主,我们这里就先不讲C风格的for。原生的for,在一些小细节上面还是要注意的,下面是基本的语法:
for var in list
do
commands
done
注意引号问题
#!/bin/bash
for var in I don't know if this'll work
do
echo "$var"
done
结果是:
I
dont know if thisll
work
显然不符合我们的预期,因为shell把两个引号中间的部分当做一个字符串了,有两种解决方法:
#!/bin/bash
# 一种进行转义,一种使用双引号括起来
for var in I don \'t know if "this'll" work
do
echo "$var"
done
注意字符串整体性
#!/bin/bash
for var in Nevada New Hampshire New Mexico New York
do
echo "Now going to $var"
done
结果是:
Now going to Nevada
Now going to New
Now going to Hampshire
Now going to New
Now going to Mexico
Now going to New
Now going to York
Now going to North
Now going to Carolina
显然就不符合。我们可以使用双引号括起来:
#!/bin/bash
for var in Nevada "New Hampshire" "New Mexico" "New York"
do
echo "Now going to $var"
done
注意使用IFS拆列表
#!/bin/bash
# reading values from a file
file="states"
IFS=$'\n'
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
这样,对于文件中的分割,只会使用换行符
注意常用的读取目录文件
#!/bin/bash
for file in /home/rich/test/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done
注意使用双引号括起来了文件名的引用变量,是因为有可能文件名有空格,如果不括起来,语法就会有错误了!
5、while循环
经历了前面的if与for的历练,对于while这里的语法相对来说就没有那么难理解了:
while test command
do
other commands
done
简单的例子:
#!/bin/bash
var1=10
while [ $var1 -gt 0 ]
do
echo $var1
var1=$[ $var1 - 1 ]
done
6、until循环
这个和while的逻辑相反:while是知道test退出码为非零的时候,结束循环,而这个是当test循环退出码为零的时候结束循环,语法也是类似:
until test command
do
other commands
done
例子不举了
五、结束语
到此几乎最最基本的shell语法就差不多都讲了。剩下的,就是如"汪洋"的命令大军了。哪怕是这些基础的语法,我都感觉,是由命令组成的,这就是shell编程,疯狂的奇技淫巧~接下来我会短平快的写几个小案例和shell中常用的命令,例如awk,sed,find等,如若有具体的语法”灵光一现“,都会update到本篇幅中,恩就这样。