[Linux] shell编程之基础篇

注意区分:

$xxx
${xxx}

$(xxx)
`xxx`

0 Shell 编程简介

  • Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。
  • Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
  • Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。

shell脚本

Shell 脚本(shell script),是一种为 shell 编写的脚本程序。
业界所说的 shell 通常都是指 shell 脚本,但读者朋友要知道,shell 和 shell script 是两个不同的概念。
由于习惯的原因,简洁起见,本文出现的 "shell编程" 都是指 shell 脚本编程,不是指开发 shell 自身。

Shell 环境

Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。
Linux 的 Shell 种类众多,常见的有:

  • Bourne Shell(/usr/bin/sh或/bin/sh)
  • Bourne Again Shell(/bin/bash)
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)
  • Shell for Root(/sbin/sh)
    ……

在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为 #!/bin/bash
#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell;告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序。

1 shell 函数

1.1 函数格式 & 函数返回值

案例: 参见 case0
linux shell 可以用户定义函数,然后在shell脚本中可以随便调用。

shell中函数的定义格式如下:

[ function ] funtionName [()]
{

    action;

    [return int;]

}
  • 【参数返回】可显示加:return 返回;如果不加return,将以最后一条命令的运行结果作为返回值。 return后跟数值n(0-255)
  • 【获取函数返回值】函数返回值在调用该函数后通过 $? 来获得。

注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。

1.2 函数参数

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数... 带参数的函数示例:

#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

funWithParam(){
    echo "第一个参数为 $1 !"
    echo "第二个参数为 $2 !"
    echo "第十个参数为 $10 !"
    echo "第十个参数为 ${10} !"
    echo "第十一个参数为 ${11} !"
    echo "参数总数有 $# 个!"
    echo "作为一个字符串输出所有参数 $* !"
}

funWithParam 1 2 3 4 5 6 7 8 9 34 73

1.3 获取参数 | 特殊字符参数

  • 注意

$10 不能获取第10个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。
另外,还有几个特殊字符用来处理参数

序号 参数 说明
1 $0 当前的文件名
2 $# 传递到脚本或函数的参数个数 / 位置变量的个数
3 $* 以一个单字符串显示所有向脚本传递的参数 / 所有位置变量的内容
4 $$ 脚本运行的当前进程ID号
5 $! 后台运行的最后一个进程的ID号
6 \(@ | 所有位置变量的内容 / 与\)*相同,但是使用时加引号,并在引号中返回每个参数。
7 $- 显示Shell使用的当前选项,与set命令功能相同。
8 $? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

2 Shell 变量

变量声明 与 赋值

  • a-变量声明 / A-显式赋值
    定义变量时,变量名不加美元符号($,PHP语言中变量需要),如:
yourVar="cnblogs.com"
  • b-变量重新声明(重新定义变量)
    已定义的变量,可被重新定义,如:
your_name="tom"
echo $your_name
your_name="alibaba"
echo $your_name

这样写是合法的,但注意,第二次赋值的时候不能写\(your_name="alibaba",使用变量的时候才加美元符(\))。

  • B-语句赋值
    除了显式地直接赋值,还可以用语句给变量赋值,如:
for file in `ls /etc`
  或
for file in $(ls /etc)

变量命名规范

注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。同时,变量名的命名须遵循如下规则:

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用bash里的关键字(可用help命令查看保留关键字)。

有效的 Shell 变量名示例如下:

RUNOOB
LD_LIBRARY_PATH
_var
var2

无效的变量命名:

?var=123
user*name=runoob

使用变量

使用一个定义过的变量,只要在变量名前面加美元符号即可,如:

your_name="qinjx"
echo $your_name
echo ${your_name}

变量名外面的花括号可选的,加不加都行。加花括号是为了帮助解释器识别变量的边界,比如下面这种情况:

for skill in Ada Coffe Action Java; do
    echo "I am good at ${skill}Script"
done

如果不给skill变量加花括号,写成echo "I am good at \(skillScript",解释器就会把\)skillScript当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。
推荐给所有变量加上花括号{},这是个好的编程习惯。

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

下面的例子尝试更改只读变量,结果报错:

#!/bin/bash

myUrl="https://www.google.com"
readonly myUrl
myUrl="https://www.runoob.com"

运行脚本,结果如下:

/bin/sh: NAME: This variable is read only.

删除变量

使用 unset 命令可以删除变量。语法:

unset variable_name

变量被删除后不能再次使用。unset 命令不能删除只读变量。

示例

#!/bin/sh
myUrl="https://www.runoob.com"
unset myUrl
echo $myUrl

以上例子执行将没有任何输出。

变量类型

运行shell时,会同时存在3种变量:

    1. 局部变量 局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。
    1. 环境变量 所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。
    1. shell变量 shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行

Shell 字符串

字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),【字符串】可以用单引号,也可以用双引号,也可以不用引号

单引号

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量无效的;
  • 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。
str='this is a string'

双引号

双引号的优点:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符
your_name='runoob'
str="Hello, I know you are \"$your_name\"! \n"
echo -e $str

输出结果为:

Hello, I know you are "runoob"! 

拼接字符串

your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting  $greeting_1

# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'
echo $greeting_2  $greeting_3

输出结果为:

hello, runoob ! hello, runoob !
hello, runoob ! hello, ${your_name} !

获取字符串长度

string="abcd"
echo ${#string} #输出 4

提取子字符串

以下实例从字符串第 2 个字符开始截取 4 个字符:

string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

注意:第一个字符的索引值为 0。

查找子字符串

查找字符 i 或 o 的位置(哪个字母先出现就计算哪个):

string="runoob is a great site"
echo `expr index "$string" io`  # 输出 4

注意: 以上脚本中 ` 是反引号,而不是单引号 ',不要看错了哦。

3 shell 流程控制语句

if 语句

if else fi 格式

if 语句语法格式:

if condition1
then
    command1 
    command2
    ...
    commandN 
fi

写成一行(适用于终端命令提示符):

if [ $(ps -ef | grep -c "ssh") -gt 1 ]; then echo "true"; fi

末尾的 fi 就是 if 倒过来拼写,后面还会遇到类似的。

if else 格式

if else 语法格式:

if condition1
then
    command1 
    command2
    ...
    commandN
else
    command
fi

if else-if else 格式

if else-if else 语法格式:

if condition1
then
    command1
elif condition2 
then 
    command2
else
    commandN
fi

case ... esac (多分支)语句

case ... esac 为多选择语句,与其他语言中的 switch ... case 语句类似,是一种多分枝选择结构,
每个 case 分支用右圆括号开始,用两个分号 ;; 表示 break,即执行结束,跳出整个 case ... esac 语句,esac(就是 case 反过来)作为结束标记。
可以用 case 语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。
case ... esac 语法格式如下:

case 值 in
模式1)
    command1
    command2
    ...
    commandN
    ;;
模式2)
    command1
    command2
    ...
    commandN
    ;;
esac

case 工作方式如上所示,取值后面必须为单词 in,每一模式必须以右括号结束。取值可以为变量或常数,匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;。
取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。

echo '输入 1 到 4 之间的数字:'
echo '你输入的数字为:'
read aNum
case $aNum in
    1)  echo '你选择了 1'
    ;;
    2)  echo '你选择了 2'
    ;;
    3)  echo '你选择了 3'
    ;;
    4)  echo '你选择了 4'
    ;;
    *)  echo '你没有输入 1 到 4 之间的数字'
    ;;
esac
#!/bin/sh

site="runoob"

case "$site" in
   "runoob") echo "菜鸟教程"
   ;;
   "google") echo "Google 搜索"
   ;;
   "taobao") echo "淘宝网"
   ;;
esac

输出结果为:

菜鸟教程

while 语句

while 循环用于不断执行一系列命令,也用于从输入文件中读取数据。其语法格式为:

while condition
do
    command
done

以下是一个基本的 while 循环,测试条件是:如果 int 小于等于 5,那么条件返回真。int 从 1 开始,每次循环处理时,int 加 1。运行上述脚本,返回数字 1 到 5,然后终止。

#!/bin/bash
int=1
while(( $int<=5 ))
do
    echo $int
    let "int++"
done

运行脚本,输出:

1
2
3
4
5

无限循环

  • 方法1
while :
do
    command
done
  • 方法2
while true
do
    command
done
  • 方法3
for (( ; ; ))

until 循环

until 循环执行一系列命令直至条件为 true 时停止。
until 循环与 while 循环在处理方式上刚好相反。
一般 while 循环优于 until 循环,但在某些时候—也只是极少数情况下,until 循环更加有用。
until 语法格式:

until condition
do
    command
done

condition 一般为条件表达式,如果返回值为 false,则继续执行循环体内的语句,否则跳出循环。

  • demo: 使用 until 命令来输出 0 ~ 9 的数字:
#!/bin/bash

a=0
until [ ! $a -lt 10 ]
do
   echo $a
   a=`expr $a + 1`
done

输出结果为:

0
1
2
3
4
5
6
7
8
9

for 循环

与其他编程语言类似,Shell支持for循环。

for循环一般格式为:

for var in item1 item2 ... itemN
do
    command1
    command2
    ...
    commandN
done

写成一行:

for var in item1 item2 ... itemN; do command1; command2… done;

当变量值在列表里,for 循环即执行一次所有命令,使用变量名获取列表中的当前取值。命令可为任何有效的 shell 命令和语句。in 列表可以包含替换、字符串和文件名。
in 列表是可选的,如果不用它,for循环使用命令行的位置参数。

  • for i in xxx的各种用法 :
for i in “file1” “file2” “file3”
for i in /boot/*
for i in /etc/*.conf
for i in $(seq -w 10) --》等宽的01-10
for i in {1…10}
for i in $( ls )
for I in $(< file)
for i in “$@” --》取所有位置参数,可简写为for i
  • bash shell支持C式for循环
#!/bin/bash
j=$1
for ((i=1; i<=j; i++))
do
touch file$i && echo file $i is ok
done

跳出循环 语句

在循环过程中,有时候需要在未达到循环结束条件时强制跳出循环,Shell使用两个命令来实现该功能:break和continue。

break命令

break命令允许跳出所有循环(终止执行后面的所有循环)。
下面的例子中,脚本进入死循环直至用户输入数字大于5。要跳出这个循环,返回到shell提示符下,需要使用break命令。

#!/bin/bash
while :
do
    echo -n "输入 1 到 5 之间的数字:"
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
            break
        ;;
    esac
done

执行以上代码,输出结果为:

输入 1 到 5 之间的数字:3
你输入的数字为 3!
输入 1 到 5 之间的数字:7
你输入的数字不是 1 到 5 之间的! 游戏结束

continue语句

continue命令与break命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。

对上面的例子进行修改:

#!/bin/bash
while :
do
    echo -n "输入 1 到 5 之间的数字: "
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的!"
            continue
            echo "游戏结束"
        ;;
    esac
done

运行代码发现,当输入大于5的数字时,该例中的循环不会结束,语句 echo "游戏结束" 永远不会被执行。

4 shell 数组

定义数组

数组中可以存放多个值。Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小(与 PHP 类似)。
与大部分编程语言类似,数组元素的下标0 开始。
Shell 数组括号()来表示,元素用"空格"符号分割开。
语法格式如下:

array_name=(value1 value2 ... valuen)
  • demo
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

my_array1=(A B "C" D)

## 或者:↓

my_array2[0]=value0
my_array2[1]=value1
my_array2[2]=value2

读取数组

读取数组元素值的一般格式是:

${array_name[index]}
  • demo
!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

my_array=(A B "C" D)

echo "第一个元素为: ${my_array[0]}"
echo "第二个元素为: ${my_array[1]}"
echo "第三个元素为: ${my_array[2]}"
echo "第四个元素为: ${my_array[3]}"

获取数组中的所有元素

使用 @* 可以获取数组中的所有元素

  • demo
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

my_array[0]=A
my_array[1]=B
my_array[2]=C
my_array[3]=D

echo "数组的元素为: ${my_array[*]}"
echo "数组的元素为: ${my_array[@]}"

output:

$ chmod +x test.sh 
$ ./test.sh
数组的元素为: A B C D
数组的元素为: A B C D

获取数组的长度

获取数组长度的方法与获取字符串长度的方法相同

  • demo
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

my_array[0]=A
my_array[1]=B
my_array[2]=C
my_array[3]=D

echo "数组元素个数为: ${#my_array[*]}"
echo "数组元素个数为: ${#my_array[@]}"

output

$ chmod +x test.sh 
$ ./test.sh
数组元素个数为: 4
数组元素个数为: 4

5 Shell 基本运算符 【待完善】

  • 参考文献

数值运算

  • shell 包含3个可用于数值计算的命令:bcexprlet
  • 支持情况
  • letexpr : 只支持整数
  • bcawk : 支持浮点数
  • 输出
  • bcexpr可直接显示计算结果;
  • let 则丢弃计算结果,可通过传递结果到变量,取变量值获得计算结果。
  • 空格与语法

经测试,bc允许运算量和运算符之间无空格,let 不允许有空格,expr运算量和运算符之间必须有空格。

[root@localhost ~]# let i = 5 + 3
-bash: let: =: syntax error: operand expected (error token is "=")

[root@localhost ~]# echo 300+5 | bc
305

[root@localhost ~]# expr 300+5
300+5

[root@localhost ~]# expr 300 + 5
305

bc

使用bc进行运算,支持小数运算,但在脚本中不可直接使用,否则会进入交互界面,可以用echo结合管道使用

  • 交换模式:
[root@localhost ~]# bc
 
#默认整数
10/3
3
 
#可以先输入scale= 指定保留小数点后几位
scale=3 //指定小数点后几位
10/3
3.333
  • 非交换模式:
[root@localhost ~]# echo “scale=3;10/3” | bc
3.33310
 
[root@client ~]# echo “3^2” | bc //做幂的运算,计算3的平方
9
 
bc做变量的运算:
[root@localhost ~]# a=10
[root@localhost ~]# b=3
[root@localhost ~]# echo "$a/$b" | bc
3
[root@localhost ~]# echo "scale=2;$a/$b" | bc
3.33
 
bc还可以做逻辑运算,真为1,假为0
[root@client ~]# echo "2>2" | bc
0
[root@client ~]# echo "2==2" | bc
1
[root@client ~]# echo "2<2"| bc
0

expr

a=10
b=3
expr $a + $b
expr $a - $b
expr $a \* $b  #* 号需要转译
expr $a / $b
expr $a % $b

let

  • 有bc 工具为什么还要用let?
  • let 是 Bash shell 的内置命令,因此可以在 Shell 脚本中直接使用,无需外部命令(需要注意路径问题)
  • let 可以直接使用变量进行计算,无需额外的语法和引用符号。
  • let 专注于整数计算,可以比 bc 更高效地执行简单的整数运算
  • let的运算可以改变变量本身的值,但不显示结果,需要echo,其他的运算方式可以做运算但不改变变量本身的值
[root@localhost ~]# let i=300+5
[root@localhost ~]# echo $i
305
[root@localhost ~]# let i=300-5
[root@localhost ~]# echo $i
295
[root@localhost ~]# let i=300/*5
[root@localhost ~]# echo $i
1500
[root@localhost ~]# let i=300/5
[root@localhost ~]# echo $i
60
 
[root@server myscripts]# n=1;let n=n+1;echo $n
2
[root@server myscripts]# let n+=2 //n=n+2
[root@server myscripts]#
[root@server myscripts]# echo $n
4
[root@server myscripts]# let n=n**2 //求幂,4的2次方
[root@server myscripts]# echo $n
16
[root@server myscripts]# let n++ //n自加1
[root@server myscripts]# let n-- //n自减1
 
[root@client opt]# echo $a
13

数值/数值字符串的左侧补0与去0问题

  • 场景1:加减、乘除运算 : 需去除数值字符串的左侧0

  • 场景2:两段字符串拼接为1个字符串时: 数值字符串的左侧需补0

shell对字符串进行处理,如果字符串长度不足5位,需要左边自动补0。
比如:拼接时间戳(10位的秒级时间戳字符串 + 3位的毫秒级时间戳字符串) "1715411840" + "25",需"25" 自动补齐为 "025"
比如: 123,自动补齐为"00123"

左补0

  • 方法1:shell的printf
$ printf "%05d\n" 123
00123

方法2: 使用awk

$ echo 123 | awk '{printf("%05d\n",$0)}'
00123

$ echo "123" | awk '{printf("%05d\n",$0)}'
00123

左去0

  • 样例场景:除法运算中的被除数需左去0

例如: 060069360 / 1000000 需转为 : 60069360 / 1000000 方可正常运算

  • 解决方法1:
$ number=01344
$ echo $(echo  $number | awk '{print $0+0}' )
1344
  • 实际应用: 获取毫秒级时间戳的最后3位
echo $(( $(echo  $(date +%N) | awk '{print $0+0}' ) / 1000000 ))
# 或 : echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' ) / 1000000" ))
# 等效于: 0601693600 / 1000000
# 输出: 60

6 Shell echo命令

Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于字符串的输出。

命令格式:

echo string

显示普通字符串

echo "It is a test"

这里的双引号完全可以省略,以下命令与上面实例效果一致:

echo It is a test

显示转义字符

$ echo "\"It is a test\""
"It is a test"

同样,外层的双引号也可以省略

显示变量

read 命令从标准输入中读取一行,并把输入行的每个字段的值指定给 shell 变量

test.sh

#!/bin/sh
read name 
echo "$name It is a test"

execute sh test.sh's output:

OK                     #标准输入
OK It is a test        #输出

显示换行

echo -e "OK! \n" # -e 开启转义
echo "It is a test"

output

OK!

It is a test

显示不换行

#!/bin/sh
echo -e "OK! \c" # -e 开启转义 \c 不换行
echo "It is a test"

OK! It is a test

显示结果定向至文件

echo "It is a test" > myfile

原样输出字符串,不进行转义或取变量(用单引号)

echo '$name\"'

$name"

显示命令执行结果(用反引号)

echo `date`

Thu Jul 24 10:08:46 CST 2014

7 Shell printf 命令

简介/格式

printf 命令模仿 C 程序库(library)里的 printf() 程序。
printf 由 POSIX 标准所定义,因此使用 printf 的脚本比使用 echo 移植性好。
printf 使用引用文本或空格分隔的参数,外面可以在 printf 中使用格式化字符串,还可以制定字符串的宽度、左右对齐方式等。默认的 printf 不会像 echo 自动添加换行符,我们可以手动添加 \n。

printf 命令的语法:

printf  format-string  [arguments...]

参数说明:

  • format-string: 为格式控制字符串
  • arguments: 为参数列表。

demo: echo vs printf

$ echo "Hello, Shell"
Hello, Shell

$ printf "Hello, Shell\n"
Hello, Shell
$

demo: 显示人员信息列表

#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com
 
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg  
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876

参数说明

  • %s %c %d %f 都是格式替代符,%s 输出一个字符串,%d 整型输出,%c 输出一个字符,%f 输出实数,以小数形式输出。
  • %-10s 指一个宽度为 10 个字符(- 表示左对齐,没有则表示默认右对齐),任何字符都会被显示在 10 个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。
  • %-4.2f 指格式化为小数,其中 .2 指保留2位小数。

output:

姓名     性别   体重kg
郭靖     男      66.12
杨过     男      48.65
郭芙     女      47.99

printf 的转义序列

序列 说明
\a 警告字符,通常为ASCII的BEL字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换行字符(只在%b格式指示符控制下的参数字符串中有效),而且,任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略
\f 换页(formfeed)
\n 换行
\r 回车(Carriage return)
\t 水平制表符
\v 垂直制表符
\ 一个字面上的反斜杠字符
\ddd 表示1到3位数八进制值的字符。仅在格式字符串中有效
\0ddd 表示1到3位的八进制值字符
  • demo: 转义序列的示例
$ printf "a string, no processing:<%s>\n" "A\nB"
a string, no processing:<A\nB>

$ printf "a string, no processing:<%b>\n" "A\nB"
a string, no processing:<A
B>

$ printf "www.runoob.com \a"
www.runoob.com $                  #不换行

8 Shell test 命令

Shell中的 test 命令用于检查某个条件是否成立。它可以进行数值字符文件3个方面的测试。

数值测试

参数 说明
-eq 等于则为真
-ne 不等于则为真
-gt 大于则为真
-ge 大于等于则为真
-lt 小于则为真
-le 小于等于则为真
  • demo
num1=100
num2=100
if test $[num1] -eq $[num2]
then
    echo '两个数相等!'
else
    echo '两个数不相等!'
fi

output: 两个数相等!

代码中的 [] 执行基本的算数运算

  • demo
#!/bin/bash

a=5
b=6

result=$[a+b] # 注意等号两边不能有空格
echo "result 为: $result"

result 为: 11

字符串测试

参数 说明
= 等于则为真
!= 不相等则为真
-z 字符串 字符串的长度为零则为真
-n 字符串 字符串的长度不为零则为真
  • demo
num1="ru1noob"
num2="runoob"
if test $num1 = $num2
then
    echo '两个字符串相等!'
else
    echo '两个字符串不相等!'
fi

两个字符串不相等!

文件测试

参数 说明
-e 文件名 如果文件存在则为真
-r 文件名 如果文件存在且可读则为真
-w 文件名 如果文件存在且可写则为真
-x 文件名 如果文件存在且可执行则为真
-s 文件名 如果文件存在且至少有一个字符则为真
-d 文件名 如果文件存在且为目录则为真
-f 文件名 如果文件存在且为普通文件则为真
-c 文件名 如果文件存在且为字符型特殊文件则为真
-b 文件名 如果文件存在且为块特殊文件则为真
  • demo
cd /bin
if test -e ./bash
then
    echo '文件已存在!'
else
    echo '文件不存在!'
fi

文件已存在!

逻辑运算

另外,Shell 还提供了( -a )、( -o )、( ! )三个逻辑操作符用于将测试条件连接起来,其优先级为:

! 最高, -a 次之, -o 最低。

  • demo
cd /bin
if test -e ./notFile -o -e ./bash
then
    echo '至少有一个文件存在!'
else
    echo '两个文件都不存在'
fi

至少有一个文件存在!

9 Shell 输入/输出/重定向

shell 输入/输出/重定向

大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回​​到您的终端。
一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。
同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。

【重定向命令】列表如下:
| 命令 | 说明 |
| --------------- | ------------------------------ |
| command > file | 将输出重定向(覆盖式)到 file |
| command < file | 将输入重定向(覆盖式)到 file |
| command >> file | 将输出追加的方式重定向到 file |
| n > file | 将文件描述符为 n 的文件重定向(覆盖式)到 file |
| n >> file | 将文件描述符为 n 的文件以追加的方式重定向到 file |
| n >& m | 将输出文件 m 和 n 合并 |
| n <& m | 将输入文件 m 和 n 合并 |
| << tag | 将开始标记 tag 和结束标记 tag 之间的内容作为输入 |

【重定向深入讲解】、文件描述符
一般情况下,每个 Unix/Linux 命令运行时都会打开3个文件:

  • 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
  • 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
  • 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。 即 2 表示标准错误文件(stderr)
  1. 默认情况下,command > filestdout 重定向到 file; command < filestdin 重定向到 file。
  2. 如果希望 stderr 重定向到 file,可以这样写:command 2>file
  3. 如果希望 stderr 追加到 file 文件末尾,可以这样写:command 2>>file
[demo]
command1 < infile > outfile

同时替换输入和输出,先执行command1,从文件infile读取内容;然后,将输出写入到outfile中

Here Document (一种特殊的重定向到交互式shell的方式)

Here Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。

它的基本的形式如下:

其作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。

command << delimiter
    document
delimiter

注意:

  • 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。

  • 开始的delimiter前后的空格会被忽略掉。

  • demo1: 在命令行中通过 wc -l 命令计算 Here Document 的行数

$ wc -l << EOF
    欢迎来到
    菜鸟教程
    www.runoob.com
EOF
3          # 输出结果为 3 行
  • demo2: 让用户选择菜单选项
#!/bin/bash
# author: johnnyzen
# url: https://www.cnblogs.com/johnnyzen

menu(){
cat <<END
=================================================
        1.Install at Debian(Ubuntu) 10(buster)
        2.Install at Redhat(CentOS) 7
        3.Exit
=================================================
END
}

menu # 执行 shell 函数
read -p "Please input number(1|2|3):" num
echo "you pick the menu is $num";

/dev/null 文件

如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:

command > /dev/null

/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃
如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。
如果希望屏蔽 stdout 和 stderr,可以这样写:

command > /dev/null 2>&1

注意:这里的 2 和 > 之间不可以有空格,2> 是一体的时候才表示错误输出

  • 0 是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。

2>&1是将标准错误重定向到标准输出。故:当程序产生错误的时候,相当于错误流向左边,而左边依旧是输入到/dev/null

ls 2>1
  测试一下,不会报没有2文件的错误,但会输出一个空的文件1
ls xxx 2>&1
  测试,不会生成1这个文件了,不过错误跑到标准输出了
ls xxx >out.txt 2>&1
  实际上可换成 ls xxx 1>out.txt 2>&1;
  重定向符号>默认是1,错误和输出都传到out.txt了。

10 Shell 文件包含(外部脚本文件引入)

和其他语言一样,Shell 也可以包含外部脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件。
Shell 文件包含的语法格式如下:

. filename   # 注意点号(.)和文件名中间有一空格
  或
source filename
  • demo: 创建两个 shell 脚本文件, 脚本2 引用并执行 脚本1

test1.sh

#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

url="http://www.runoob.com"

test2.sh

#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

#使用 . 号来引用test1.sh 文件
. ./test1.sh

# 或者使用以下包含文件代码
# source ./test1.sh

echo "菜鸟教程官网地址:$url"

接下来,我们为 test2.sh 添加可执行权限并执行:

$ chmod +x test2.sh 
$ ./test2.sh 
菜鸟教程官网地址:http://www.runoob.com

11 注释

单行注释

# 注释文本

多行注释

:<<EOF 示范
sh /home/sdc/public/lw/bootstrap.sh \
	--web-dir-path="/lw-test-folder" --user-name="admin" --file-types="xlsx,doc" --file-names="faker" \
	--download-folder="/home/sdc/public/lw/in_tmp/in_tmpA" \
	--zip-file-name="test.zip" \
	--exec-path="/home/sdc/public/lw/test.out" \
	--output-folder="/home/sdc/public/lw/out_tmp/in_tmpA" \
	> /home/sdc/public/lw/download-files-from-network-disk.log
EOF

M 应用场景

case0 第一个shell function

#!/bin/bash
# author: johnnyzen
# url: https://www.cnblogs.com/johnnyzen


function helloWorld(){ # 声明 shell 函数
	echo "this is my first bash shell function!";
	echo "please input number 'A':"; 
	read numA
	read -p "please input number 'B':" numB; # 输入前通过read命令提示(可不换行地输入)
	echo "number 'A' is $numA, 'B' is $numB";
	return $(($numA+$numB))
	
}

# 调用方式1
helloWorld # 调用 shell 函数
echo "'A'+'B'="$? # 通过 $?获取到 函数返回值

case1 for循环: 遍历指定路径下的文件

for file in `ls ./`
do
	echo "file:"$file
done

批量添加制订文件夹下的jar包到环境变量中

for f in /usr/local/apache-hive-0.13.1-bin/lib/*.jar; do
  HADOOP_CLASSPATH=${HADOOP_CLASSPATH}:$f;
done
export HADOOP_CLASSPATH

case2 for循环: 产生10个随机数

  • 方法1
for i in {0..9};do echo $RANDOM;done
  • 方法2
for i in $(seq 10);do echo $RANDOM;done

case3 获取当前脚本所处目录路径

#!/bin/bash

work_dir=$(cd $(dirname $0);pwd)

echo $work_dir

case4 if语句: 判断两个变量是否相等

a=10
b=20
if [ $a == $b ]
then
   echo "a 等于 b"
elif [ $a -gt $b ]
then
   echo "a 大于 b"
elif [ $a -lt $b ]
then
   echo "a 小于 b"
else
   echo "没有符合的条件"
fi

输出结果:

a 小于 b
  • if else 语句经常与 test 命令结合使用,如下所示:
num1=$[2*3]
num2=$[1+5]
if test $[num1] -eq $[num2]
then
    echo '两个数字相等!'
else
    echo '两个数字不相等!'
fi

输出结果:

两个数字相等!

case5 shell脚本中exit 0引发的血案

2021.10.13 10:00 - gyjc项目

case6 shell脚本中添加--help帮助选项

test-getopt.sh

if [[ $1 = "--help" ]] || [[ $1 = "-h" ]]; then
    # echo "脚本传入参数$1"
    # echo "这是一个测试demo"
    # echo "这个脚本怎么使用,干嘛的,注意点巴拉巴拉"
    print_help;
    exit 0;
fi

print_help() {
  cat <<EOF
  use $program --ssid=<ssid>

    --ssid=<ssid> - where <ssid> is the name of the AP advertised with the beacon frames. (default:$arg_ssid)
    --passphrase=<passphrase> - where <passphrase> is the password of AP (default:$arg_passphrase)

    --stop  - stop accesspoint
    --start - also start accesspoint
    --help  - prints help screen

EOF

case7 shell获取外部变量/参数 by GETOPT

getopt命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。格式如下:

getopt optstring parameters
  • optstring是关键所在,它定义命令行有效的选项字母,还定义了那些选项字母需要参数值
  • getopt命令有一个更高级的版本叫做getopts

demo:

#!/bin/sh

#1说明
show_usage="args: [-i , -p , -u , -w , -a , -s , -d , -v ]\
                                  [--ip=, --port=, --user=, --pwd=, --path=, --script=, --debug=, --version=]"
#2参数
opt_ip=""
opt_port=""
opt_user=""
opt_pwd=""
opt_path=""
opt_script=""
opt_debug=""
opt_version=""

GETOPT_ARGS=`getopt -o i:p:u:w:a:s:d:v: -al ip:,port:,user:,pwd:,path:,script:,debug:,version: -- "$@"`
# 注1: $@:表示所有脚本参数的内容
# 注2: $#:表示返回所有脚本参数的个数
eval set -- "$GETOPT_ARGS"

#3获取参数
while [ -n "$1" ]
do
        case "$1" in
                -i|--ip) opt_ip=$2; shift 2;;
                -p|--port) opt_port=$2; shift 2;;
                -u|--user) opt_user=$2; shift 2;;
                -w|--pwd) opt_pwd=$2; shift 2;;
                -a|--path) opt_path=$2; shift 2;;
                -s|--script) opt_script=$2; shift 2;;
                -d|--debug) opt_debug=$2; shift 2;;
                -v|--version) opt_version=$2; shift 2;;
                --) break ;;
                *) echo $1,$2,$show_usage; break ;;
        esac
done

if [[ -z $opt_ip || -z $opt_port || -z $opt_user || -z $opt_pwd || -z $opt_path || -z $opt_script || -z $opt_debug || -z $opt_version ]]; then
        echo $show_usage
        echo "opt_ip:"$opt_ip",opt_port:"$opt_port",opt_user:"$opt_user",opt_pwd:"$opt_pwd",opt_path:"$opt_path",opt_script:"$opt_script",opt_debug:"$opt_debug",opt_version:"$opt_version
        exit 0
fi

#4开始处理
#ip port user pwd 连接服务器
#script path debug version 作为参数执行

case8 文件名含空格,无法直接foreach遍历的解决方法

参考文献

SALVEIFS=$IFS # 把系统变量IFS改成"\n\b",修改前先保存其原值到SALVEIFS
IFS=$(echo -en "\n\b")

# ... shell logic code
# ...

IFS=$SAVEIFS # 还原 IFS 的原配置值

case9 shell判断文件、目录是否存在的方法

推荐文献

if [ -f "/home/sdc/test.txt" ]; then rm ${result_path}trials; fi

-e filename 如果 filename存在,则为真
-d filename 如果 filename为目录,则为真
-f filename 如果 filename为常规文件,则为真
-L filename 如果 filename为符号链接,则为真
-r filename 如果 filename可读,则为真
-w filename 如果 filename可写,则为真
-x filename 如果 filename可执行,则为真
-s filename 如果文件长度不为0,则为真
-h filename 如果文件是软链接,则为真
  • 方法2
if test -f "目标文件路径"; then
    echo "目标文件存在"
else
    echo "目标文件不存在"
fi

case10 获取时间

参考文献

date命令的关键参数

  • %T 时间(24小时制)(hh:mm:ss)
  • %y 年的最后两个数字( 1999则是99)
  • %Y (例如:1970,1996等)
  • %m (01..12)
  • %d 、一个月的第几天(01..31)
  • %D 日期(mm/dd/yy)
  • %j 一年的第几天(001..366)
  • %H 小时(00..23)
  • %M (00..59)
  • %S (00..59)
  • %s 从1970年1月1日00:00:00到目前经历的秒数
  • %N 从1970年1月1日00:00:00到目前经历的纳秒
# date +%s.%N
1715409477.512093100

时间字符串

# DATE_STR=$(date +'%Y-%m-%d %H:%M:%S')
# DATE_STR=$(date +'%Y%m%d%H%M%S')

# echo $DATE_STR
2023-04-15 21:08:35

# mv /home/user_xx/app-services/xx-service-parent-1.0.0-SNAPSHOT-20230415111546 /home/user_xx/app-services/xx-service-parent.at.$DATE_STR.bak

时间戳(秒级)

时间戳(毫秒级)

  • 格式1
# 字符串拼接方式(秒级时间戳 + 毫秒级的3位)
# echo $(date +%s)$(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" ))
# echo $(date +%s)$( echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" )) | awk '{printf("%03d\n",$0)}' )
1715409575298

# 字符串拼接方式(秒级时间戳 + 毫秒级的3位) + 字符串转为数值
echo $( expr $((  $(date +%s)$( echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" )) | awk '{printf("%03d\n",$0)}' ) )) \* 1 )
1715409575298
  • 关键点:
  • 在除法运算中,获取【毫秒级时间戳的最后3位】,需先进行左去0
echo $number | awk '{print $0+0}' )/1000000"
  • 在秒级时间戳 + 【毫秒级时间戳的最后3位】,拼接运算时,需进行左补0
echo $number | awk '{printf("%05d\n",$0)}'

应用案例: 统计耗时(毫秒级时间戳)

startTime=$( expr $((  $(date +%s)$( echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" )) | awk '{printf("%03d\n",$0)}' ) )) \* 1 ) && \
echo "startTime:${startTime}" && \
tar -czvf test.tar.gz ./* && \
endTime=$( expr $((  $(date +%s)$( echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" )) | awk '{printf("%03d\n",$0)}' ) )) \* 1 ) && \
echo "endTime:${endTime}" && \
echo "timeConsumging(unit:ms):$((endTime - startTime))"

  • 格式2
# date +%s.%N
1715409477.512093100

CASE11 循环读取文件中的每一行

方法1:使用while循环

【基本语法】

while read line
do
    # 处理每一行的代码
done < $file

其中,read line命令用于读取文件中的每一行,并将其存储在line变量中。< filename表示从文件中读取输入。

【示例】
使用while循环读取example.txt文件中的每一行,并使用echo命令将其输出到终端。

#!/bin/bash
 
file="hello.txt"

while read line
do
    echo $line
done < $file

【示例】
读取文件内容,并将其存储到一个数组中。

array=()
while read line; do
  array+=("$line")
done < file.txt

方法2:使用for循环

【基本语法】

for line in $(cat filename)
do
    # 处理每一行的代码
done

其中,$(cat filename)命令用于将文件的内容读取到一个字符串中,并使用空格或换行符分隔每一行。for循环将字符串中的每一行赋值给line变量。

【示例】

#!/bin/bash
 
filename="example.txt"
 
for line in $(cat $filename)
do
    echo $line
done

参考文献

CASE12 统计指定任务的平均耗时

  • 假定: 任务为tar -czvf test.tar.gz ./y/*

  • 统计模板SHELL

#!/bin/bash

# 总循环次数
totalCount=10
totalTimeConsuming=0

count=0
until [ ! $count -lt $totalCount ]
do
    # step1 打包or压缩文件 + 统计耗时
    startTime=$( expr $((  $(date +%s)$( echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" )) | awk '{printf("%03d\n",$0)}' ) )) \* 1 ) && \
    echo "startTime:${startTime}" && \
    tar -czvf test.tar.gz ./y/* && \
    endTime=$( expr $((  $(date +%s)$( echo $(( "$(echo  $(date +%N) | awk '{print $0+0}' )/1000000" )) | awk '{printf("%03d\n",$0)}' ) )) \* 1 ) && \
    echo "endTime:${endTime}" && \
    echo "timeConsumging(unit:ms):$((endTime - startTime))"

    # step2 耗时求和
    totalTimeConsuming=$((totalTimeConsuming + endTime - startTime))
	echo "totalTimeConsuming:"$totalTimeConsuming

    # step3 每次循环后都删除本轮迭代新增的文件
    rm test.tar.gz
	
    count=`expr $count + 1`
done

echo "average time consuming(unit:ms) : "$((totalTimeConsuming / totalCount))

Y Shell 编程规范

  • 开头指定使用什么shell,例如:bash,ksh,csh等
  • 脚本功能描述,使用方法,作者,版本,日期等
  • 变量名,函数名要有实际意义,函数名以动名词形式,第二个单词首字母要大写。例如:updateConfig()
  • 缩进统一用4个空格,不用TAB
  • 取变量值使用大括号,如$
  • 删除文件时,如果路径有变量的,要判断变量有值,如rm -f ${abc}/* 如果变量abc没有值,则会把根目录下的文件删除
  • 脚本中尽量不要使用cd变换目录
  • 函数中也要有功能描述,使用依法,版本,日期等
  • 函数的功能要单一,不要太复杂
  • $()比` `更好;
  • 推荐给所有变量加上花括号{},这是个好的编程习惯
  • 尽量不要使用多层if语句,而应该以case语句替代
  • 如果需要执行确定次数的循环,应该用for语句替代while语句
  • 输入的参数要有正确性判断
  • 多加注释,方便自己或他人阅读

X 参考文献

posted @ 2021-07-27 19:39  千千寰宇  阅读(948)  评论(0编辑  收藏  举报