创建和使用shell函数

来源: Linux命令行与shell脚本编程大全

内容

  • 基本的脚本函数
  • 返回值
  • 在函数中使用变量
  • 数组变量和函数
  • 函数递归
  • 创建库
  • 在命令行上使用函数

我们可以将shell脚本代码放进函数中封装起来,这样就能在脚本中的任何地方多次使用它了。

下面我们来逐步了解如何创建自己的shell脚本函数并在应用中使用它们。

R-面向对象编程

一些概念

这里首先要提及一些概念,然后我们再看具体的实例加以理解。

每一个单独的对象都可以被称为对应的一个实例(instance)。操作指定类的函数称为方法(method)。

把程序接口从具体的实现细节中分离开来的过程称为封装

在OOP(面向对象编程)中,我们可以通过一个类创建出另外一个类,只需要指定新类的不同信息即可,这种方法称为继承。由此衍生出,被继承的类称为父类或超类(superclass),新创建的类称为子类(subclass)。

在OOP中,允许同一个方法名操纵不同对象并得到不同的结果,称为多态(polymorphism)。

通过一系列的其他类来创建新类的过程称为组合(composition)。在一些语言中,一个类可以从多个类中继承方法,称为多重继承(multiple inheritance)。

R中的属性与类

属性

属性描述了对象所代表的内容以及R解释该对象的方式。很多时候两个对象之间的唯一差别就在于它们的属性不同。下表展示了一些重要的属性。很多常见的属性都是针对常见的数值型数据对象而言的:像数组、矩阵和数据框。

Shell脚本之处理用户输入

Shell脚本笔记系列:

构建基本shell脚本

Linux结构化命令

内容:

  • 传递参数
  • 跟踪参数
  • 移动变量
  • 处理选项
  • 将选项标准化
  • 获得用户输入

经过前面的介绍,我们已经可以掌握一些流程化的脚本编程了。但有时候,我们需要编写的脚本能够跟使用者进行交互。它可以是静态的,输入相应的参数让它运行到底;也可以是动态的,脚本根据输入参数反馈不同的信息,使用者又能根据信息调整下一步的处理,实时与程序互动。

bash shell提供了一些不同的方法来从用户处获得数据,包括命令行参数、命令行选项以及直接从键盘读取输入的能力。下面将一一介绍实现。

Linux结构化命令

条件控制

内容

  • 使用if-then语句
  • 嵌套if语句
  • test命令
  • 复合条件测试
  • 使用双方括号和双括号
  • case命令

许多程序要求对shell脚本中的命令施加一些逻辑流程控制。而某些命令会根据条件判断执行相应的命令,这样的命令通常叫做结构化命令。从概念上理解,结构化命令是shell脚本的逻辑结构,不像顺序执行shell脚本,而是有组织地执行命令以应对复杂任务需求。

if-then语句

最基本的结构化命令是if-then语句,它的格式如下:

1
2
3
4
if command
then
truecommands
fi

注意,在其他编程语言中,if语句之后的对象是一个等式,等式的结果为TRUE或者FALSE,但是bash shell中的if语句是运行if后面的命令,如果该命令的退出状态码是0(命令成功执行),则运行then语句后面的命令。fi表示if语句到此结束。

下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
wsx@wsx-ubuntu:~/script_learn$ cat test1.sh
#! /bin/bash
# testing the if statement
if pwd
then
echo "It worked"
fi

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test1.sh
wsx@wsx-ubuntu:~/script_learn$ ./test1.sh
/home/wsx/script_learn
It worked

这个例子中在判断成功执行pwd命令后,执行输出文本字符串。

大家可以尝试把pwd命令改成随便乱打的字符试试结果。它会显示报错信息,then后面的语句也不会执行。

if-then语句的另一种形式:

1
2
3
if command; then
commands
fi

在then部分,我们可以使用多个命令(从格式中command结尾有没有s也可以看出)。

我们再来一个例子:在if语句中用grep命令在/etc/passwd文件中查找某个用户名当前是否在系统上使用。如果有用户使用了哪个登录名,脚本会显示一些文本信息并列出该用户HOME目录的bash文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
wsx@wsx-ubuntu:~/script_learn$ cat test3.sh
#!/bin/bash
# testing multiple commands in the then section
#
testuser=wsx
#
if grep $testuser /etc/passwd
then
echo "This is my first command"
echo "This is my second command"
echo "I can even put in other commands besides echo:"
ls -a /home/$testuser/.b*
fi

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test3.sh
wsx@wsx-ubuntu:~/script_learn$ ./test3.sh
wsx:x:1000:1000:wsx,,,:/home/wsx:/bin/bash
This is my first command
This is my second command
I can even put in other commands besides echo:
/home/wsx/.bash_history /home/wsx/.bashrc
/home/wsx/.bash_logout /home/wsx/.bashrc-anaconda3.bak

如果设置的用户名不存在,那么就没有输出。那么如果在这里显示的一些消息可以说明用户名在系统中未找到,这样可能就会显得更友好。所以接下来看看if-then-else语句。

if-then-else语句

我相信意思非常容易理解,这里较之前我们添加了一个else块来处理if中命令没有成功执行的步骤。格式为:

1
2
3
4
5
if command
then
commands
else commands
fi

嵌套if

有时我们需要检查脚本代码中的多种条件,可以是用嵌套的if-then语句。

处理一个例子:检查/etc/passwd文件中是否存在某个用户名以及该用户名的目录是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
wsx@wsx-ubuntu:~/script_learn$ cat test5.sh
#!/bin/bash

# Testing nested ifs
#
testuser=NoSuchUser
#
if grep $testuser /etc/passwd
then
echo "The user $testuser exits on this system."
else
echo "The user $testuser does not exit on this system."
if ls -d /home/$testuser/
then
echo "However, $testuser has a directory."
fi
fi

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test5.sh
wsx@wsx-ubuntu:~/script_learn$ ./test5.sh
The user NoSuchUser does not exit on this system.
ls: 无法访问'/home/NoSuchUser/': 没有那个文件或目录

可以使用else部分的另一种形式:elif。这样我们就不再用书写多个if-then语句了。在其他语言中,有的是用elif的形式,有的使用else if等形式。面对相同内含在不同语言中不同的表示方式,我们需要有意识地区别,以免接触的东西多了可能各种语言代码串写喔。

1
2
3
4
5
6
7
if command1
then
truecommands
elif command2
then
truemore commands
fi

这种表示方式逻辑更为清晰,但是也有点容易让写的人搞混。其实可以看到一个if对应一个fi。这是一个大的嵌套if结构。

记住,在elif语句中,紧跟其后的else语句属于elif代码块,而不是属于if-then代码块。

test命令

到此为止,我们很清楚if后面跟着的是普通的shell命令,那么我们需要测试其他条件怎么办呢?

test命令提供了在if-then语句中测试不同条件的途径。如果test命令中列出的条件成立,test命令就会退出并返回状态码0。这样if-then语句就与其他编程语言中的if-then语句以类似的方式工作了。

test命令格式:

1
test condition

conditiontest命令要测试的一系列参数和值。如果不写这个conditiontest返回非0,if语句跳转到else进行执行。

bash shell提供了一种条件测试方法,无需在if-then语句中声明test命令。

1
2
3
if [ condition ]
then commands
fi

这跟我们其他的编程习惯非常接近。建议使用这种方式。

如果使用test命令,需要记住的是各种条件参数。

数值比较

比较 描述
n1 -eq n2 (n1)等于(n2)
n1 -ge n2 大于或等于
n1 -gt n2 大于
n1 -le n2 小于或等于
n1 -lt n2 小于
n1 -ne n2 不等于

字符串比较

比较 描述
str1 = str2 (str1与str2比较)相同
str1 != str2 不同
str1 < str2
str1 > str2
-n str1 检查string1的长度非0
-z str1 检查string1的长度是否为0

注意,大于和小于号必须转义;大于和小于顺序和sort命令所采用的不同。

文件比较

比较 描述
-d file 检查file是否存在并是一个目录
-e file ~是否存在
-f file ~是否存在并是一个文件
-r file ~是否存在并可读
-s file ~是否存在并非空
-w file ~是否存在并可写
-x file ~是否存在并可执行
-O file ~是否存在并属当前用户所有
-G file ~是否存在并且默认组与当前用户相同
file1 -nt file2 检查file1是否比file2新
file1 -ot file2 检查file1是否比file2旧

复合条件测试

if-then语句允许我们使用布尔逻辑来组合测试。可用

  • [ condition1] && [ condition2]
  • [ condition1] || [ condition2]

if-then的高级特性

  • 用于数学表达式的双括号
  • 用于高级字符串处理功能的双方括号

双括号

命令格式:

1
(( expresiion ))

expression可以是任意的数学赋值或比较表达式。除了test命令使用的标准数学运算符,下面列出了一些其他的:

符号 描述
val ++ 后增
val – 后减
++ val 先增
– val 先减
! 逻辑取反
~ 位求反
** 幂运算
<< 左位移
>> 右位移
& 位布尔和
\ 位布尔或
&& 逻辑和
\ \ 逻辑或

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wsx@wsx-ubuntu:~/script_learn$ cat test23.sh
#!/bin/bash
# using doble parenthesis
#
val1=10
#
if (( $val1 ** 2 > 90 ))
then
(( val2 = $val1 ** 2 ))
echo "The square of $val1 is $val2"
fi

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test23.sh
wsx@wsx-ubuntu:~/script_learn$ ./test23.sh
The square of 10 is 100

双方括号

双方括号命令提供了针对字符串比较的高级特性。命令格式如下:

1
[[ expression ]]

双方括号里的expression使用了test命令中采用的标准字符串比较。但它提供了test没有提供的一个特性——模式匹配。

在模式匹配中,可以定义一个正则表达式来匹配字符串值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wsx@wsx-ubuntu:~/script_learn$ cat test24.sh
#! /bin/bash
# using pattern matching
#
if [[ $USER == r* ]]
then
echo "Hello $USER"
else
echo "Sorry, I do not know you"
fi

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test24.sh
wsx@wsx-ubuntu:~/script_learn$ ./test24.sh
Sorry, I do not know you

上面一个脚本中,我们使用了双等号。双等号将右边的字符串视为一个模式,并将其应用模式匹配规则。

case命令

有了case命令,就不需要写出所有的elif语句来不停地检查同一个变量的值了。case命令会采用列表格式来检查单个变量的多值。

下面是两个脚本实现相同功能进行对比:

if语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
wsx@wsx-ubuntu:~/script_learn$ cat test25.sh
#!/bin/bash
# looking for a possible value
#
if [ $USER = "rich" ]
then
echo "Welcome $USER"
echo "Please enjoy you visit"
elif [ $USER = "barbara" ]
then
echo "Welcome $USER"
echo "Please enjoy you visit"
elif [ $USER = "testing" ]
then
echo "Special testing account"
elif [ $USER = "jessica" ]
then
echo "Do not forget to logout when you're done"

case语句:

1
2
3
4
5
case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac

上面的实例可以用case语句表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
wsx@wsx-ubuntu:~/script_learn$ cat test26.sh
#!/bin/bash
# using the case command
#
case $USER in
rich | barbara)
echo "Welcome, $USER"
echo "Please enjoy your visits";;
testing)
echo "Special testing account";;
jessica)
echo "Do not forget to log off whe you're done";;
*)
echo "Sorry, you are not allowed here";;
esac

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test26.sh
wsx@wsx-ubuntu:~/script_learn$ ./test26.sh
Sorry, you are not allowed here

case命令会将指定的变量与不同模式进行比较。如果变量和模式是匹配的,那么shell会执行为该模式指定的命令。可以通过竖线操作符在一行中分隔出多个模式。星号会捕获所有与已知模式不匹配的值。注意双分号的使用。

小结

最基本的命令是if-then语句;

可以拓展if-then语句为if-then-else语句;

可以将if-then-else语句通过elif语句连接起来;

在脚本中,我们需要测试一种条件而不是命令时,比如数值、字符串内容、文件或目录的状态,test命令提供了简单方法;

方括号是test命令统一的特殊bash命令;

双括号使用另一种操作符进行高级数学运算双方括号允许高级字符串模式匹配运算;

case命令是执行多个if-then-else命令的简便方式,它会参照一个值列表来检查单个变量的值。

关于结构化命令中循环,将在下次整理的笔记中阐述。

循环控制

内容

  • for循环语句
  • until迭代语句使用while语句
  • 循环
  • 重定向循环的输出

这一节我们来了解如何重复一些过程和命令,也就是循环执行一组命令直到达到了某个特定条件。

for命令

基本格式:

1
2
3
4
for var in list
do
truecommands
done

也可以

1
for var in list; do

分号只用来分隔命令的,让代码更简约。

来个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wsx@wsx-ubuntu:~/script_learn$ cat test1
#!/bin/bash
# basic for command

for test in Alabama Alaska Arizona Arkansas California Colorado
do
echo The next state is $test
done

wsx@wsx-ubuntu:~/script_learn$ ./test1
The next state is Alabama
The next state is Alaska
The next state is Arizona
The next state is Arkansas
The next state is California
The next state is Colorado

这里操作基本和其他语言一致(格式不同),不多讲啦。

在读取列表中的复杂值时,我们可能会遇到问题。比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
wsx@wsx-ubuntu:~/script_learn$ cat badtest1
#!/bin/bash
# another example of how not to use the for command

for test in I don't know if this'll work
do
echo "word:$test"
done

wsx@wsx-ubuntu:~/script_learn$ ./badtest1
word:I
word:dont know if thisll
word:work

我们可以看到shell看到了列表值中的单引号尝试使用它们来定义一个单独的数据值。

这里有两种解决办法:

  • 使用转义字符将单引号转义
  • 使用双引号来定义用到单引号的值

我们将这两种解决办法同时用到上个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wsx@wsx-ubuntu:~/script_learn$ cat test2
#! /bin/bash
# another example of how not to use the for command

for test in I don\'t know if "this'll" work; do
echo "word:$test"
done
wsx@wsx-ubuntu:~/script_learn$ ./test2
word:I
word:don't
word:know
word:if
word:this'll
word:work

我们可能明白了for循环是假定每个值是用空格分隔的,所以当有包含空格的数据时,我们需要用双引号括起来。

通常我们会将列表值存储在一个变量中,然后通过遍历变量的方式遍历了其内容的的列表。

看看怎么完成这个任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
wsx@wsx-ubuntu:~/script_learn$ cat test3
#!/bin/bash

# using a variable to hold the list
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut" # 在尾部拼接文本

for state in $list; do
echo "Have you ever visited $state?"
done

wsx@wsx-ubuntu:~/script_learn$ ./test3
Have you ever visited Alabama?
Have you ever visited Alaska?
Have you ever visited Arizona?
Have you ever visited Arkansas?
Have you ever visited Colorado?
Have you ever visited Connecticut?

注意,代码中还用了另一个赋值语句向$list变量包含的已有列表中添加了一个值。这是在已有文本字符串尾部添加文本的一种常用方法。

我们还可以用命令来输出我们需要的列表内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
wsx@wsx-ubuntu:~/script_learn$ cat test4
#!/bin/bash
# reading values from a file

file="states"

for state in $(cat $file)
do
echo "Visit beautiful $state"
done

wsx@wsx-ubuntu:~/script_learn$ cat states
Alabama
Alaska
Arizona
Arkansas
Colorado
Connecticut
Delaware
Florida
Georgia
wsx@wsx-ubuntu:~/script_learn$ ./test4
Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
Visit beautiful Arkansas
Visit beautiful Colorado
Visit beautiful Connecticut
Visit beautiful Delaware
Visit beautiful Florida
Visit beautiful Georgia

更改字段分隔符

环境变量IFS,也叫作字段分隔符。它定义了bash shell用作字段分隔符的一系列字符。默认情况下,bash shell会将空格、制表符和换行符当作字段分隔符。

如果想修改IFS的值,比如使其只能识别换行符,我们可以将下面这行代码加入脚本:

1
IFS=$'\n'

在处理大量脚本时,我们可能只在某一部分使用其他的分隔符,这时候可以先保存原有的IFS值,然后修改,最后恢复:

1
2
3
4
IFS.OLD=$IFS
IFS=$'\n'
<在代码中使用新的IFS值>
IFS=$IFS.OLD

假如我们要遍历一个文件中用冒号分隔的值:

1
IFS=:

假如要指定多个IFS字符,只要将它们的赋值行串起来:

1
IFS=$'\n':;"

这个赋值会将换行符、冒号、分号以及双引号作为字段分隔符。

用通配符读取目录

我们可以用for命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制shell使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。

我拿我的一个目录来尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
wsx@wsx-ubuntu:~/script_learn$ cat test5
#!/bin/bash

# iterate through all the files in a directory

for file in /home/wsx/python_learn/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done
wsx@wsx-ubuntu:~/script_learn$ ./test5
/home/wsx/python_learn/athletelist.py is a file
/home/wsx/python_learn/athletemodel.py is a file
/home/wsx/python_learn/ch2_data_input.py is a file
/home/wsx/python_learn/chapter5_first.py is a file
/home/wsx/python_learn/chapter6_first.py is a file
/home/wsx/python_learn/chapter6_second.py is a file
/home/wsx/python_learn/chapter6_third.py is a file
/home/wsx/python_learn/coinFlips.py is a file
/home/wsx/python_learn/Dive_into_python is a directory

注意:第一个方括号之后和第二个方括号之前必须加上一个空格,否则会报错。

在Linux中,目录名和文件名中包含空格是合法的,所以将$file变量用双引号圈起来。当然,大家尽量不要让文件或目录包含空格,不然很容易出问题(命令会把空格当做文件的分隔符)。

C语言风格的for命令

C语言风格的for命令看起来如下:

1
for (( a = 1; a < 10; a++ ))

值得注意的是,这里有些部分没有遵循bash shell标准的for命令:

  • 变量赋值可以有空格;
  • 条件中的变量不以美元符开头;
  • 迭代过程的算式未用expr命令格式。

在使用这种格式时要小心,不同的格式不注意就会出错。

下面举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
wsx@wsx-ubuntu:~/script_learn$ cat test6
#!/bin/bash

# testing the C-style for loop

for (( i=1; i <= 10; i++ ))
do
echo "The next number is $i"
done

wsx@wsx-ubuntu:~/script_learn$ ./test6
The next number is 1
The next number is 2
The next number is 3
The next number is 4
The next number is 5
The next number is 6
The next number is 7
The next number is 8
The next number is 9
The next number is 10

while命令

while命令的格式为:

1
2
3
4
while test command
do
other commands
done

while命令某种意义上是if-then语句和for循环的混杂体。注意,这里while后面接的也是命令。while命令允许定义一个要测试的命令,然后循环执行一组命令,只要定义的测试命令返回的是退出状态码是0(类似一般语言中 的TRUE)。直到非0时退出循环。

while命令中定义的test commandif-then语句中的格式一模一样。可以使用任何普通的bash shell命令,或者用test命令进行条件测试,比如测试变量值。

最常见的用法是用方括号来检查循环命令中用到的shell变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
wangsx@SC-201708020022:~/tmp$ cat test
#/bin/bash
# while command test

var1=10
while [ $var1 -gt 0 ]
do
echo $var1
var1=$[ $var1 - 1 ]
done

wangsx@SC-201708020022:~/tmp$ ./test
10
9
8
7
6
5
4
3
2
1

使用多个测试命令
while命令允许我们在while语句行中定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。
比如while echo $var1 [ $var1 -ge 0 ]检测的就是后面方括号命令的退出状态码。

until命令

until命令和while命令工作的方式完全相反。只有测试命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结束了。

1
2
3
4
until test command
do
other commands
done

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wangsx@SC-201708020022:~/tmp$ cat test12
#!/bin/bash
# using the until command

var1=100

until [ $var1 -eq 0 ]
do
echo $var1
var1=$[ $var1 - 25 ]
done

wangsx@SC-201708020022:~/tmp$ ./test12
100
75
50
25

同样地,在until命令中放入多个测试命令时也要注意(类似while)。

嵌套循环

在循环语句内使用任意类型的命令,包括其他循环命令,叫做嵌套循环。因为是在迭代中迭代,需要注意变量的使用以及程序的效率问题。

下面举一个for循环嵌套for循环的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
wangsx@SC-201708020022:~/tmp$ cat test14
#!/bin/bash
# nesting for loops

for (( a = 1; a <= 3; a++ ))
do
echo "Starting loop $a:"
for (( b = 1; b <= 3; b++ ))
do
echo " Inside loop: $b"
done
done

wangsx@SC-201708020022:~/tmp$ . test14
Starting loop 1:
Inside loop: 1
Inside loop: 2
Inside loop: 3
Starting loop 2:
Inside loop: 1
Inside loop: 2
Inside loop: 3
Starting loop 3:
Inside loop: 1
Inside loop: 2
Inside loop: 3

shell能够自动识别匹配的dodone字符。这种模式很常见,比如通常的小括号(())、中括号、花括号匹配等等。它们的本质都是字符匹配。

在混用循环命令时也一样,比如在while循环中内嵌一个for循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
wangsx@SC-201708020022:~/tmp$ cat test15
#!/bin/bash
# placing a for loop inside a while loop

var1=5

while [ $var1 -ge 0 ]
do
echo "Outer loop: $var1"
for (( var2 = 1; $var2 < 3; var2++))
do
var3=$[ $var1 * $var2 ]
echo " Inner loop: $var1 * $var2 = $var3"
done
var1=$[ $var1 - 1 ]
done

wangsx@SC-201708020022:~/tmp$ . test15
Outer loop: 5
Inner loop: 5 * 1 = 5
Inner loop: 5 * 2 = 10
Outer loop: 4
Inner loop: 4 * 1 = 4
Inner loop: 4 * 2 = 8
Outer loop: 3
Inner loop: 3 * 1 = 3
Inner loop: 3 * 2 = 6
Outer loop: 2
Inner loop: 2 * 1 = 2
Inner loop: 2 * 2 = 4
Outer loop: 1
Inner loop: 1 * 1 = 1
Inner loop: 1 * 2 = 2
Outer loop: 0
Inner loop: 0 * 1 = 0
Inner loop: 0 * 2 = 0

如果想要挑战脑力,可以混用untilwhile循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
wangsx@SC-201708020022:~/tmp$ cat test16
#!/bin/bash
# using until and while loop

var1=3

until [ $var1 -eq 0 ]
do
echo "Outer loop: $var1"
var2=1
while [ $var2 -lt 5 ]
do
var3=$(echo "scale=4; $var1 / $var2" | bc)
echo " Inner loop: $var1 / $var2 = $var3"
var2=$[ $var2 + 1 ]
done
var1=$[ $var1 - 1 ]
done

wangsx@SC-201708020022:~/tmp$ . test16
Outer loop: 3
Inner loop: 3 / 1 = 3.0000
Inner loop: 3 / 2 = 1.5000
Inner loop: 3 / 3 = 1.0000
Inner loop: 3 / 4 = .7500
Outer loop: 2
Inner loop: 2 / 1 = 2.0000
Inner loop: 2 / 2 = 1.0000
Inner loop: 2 / 3 = .6666
Inner loop: 2 / 4 = .5000
Outer loop: 1
Inner loop: 1 / 1 = 1.0000
Inner loop: 1 / 2 = .5000
Inner loop: 1 / 3 = .3333
Inner loop: 1 / 4 = .2500

外部的until循环以值3开始,并继续执行到值等于0。内部while循环以值1开始一直执行,只要值小于5。需要注意循环条件的设置,我跑的几次都没写完整,然后无限循环只好重开终端。

控制循环

之前的学的命令已经可以让我们写循环程序了,设定好以后等待命令开始执行和等待循环结束。但是很多情况下,在循环中我们设定的某个(多个)变量达到某种条件时,我们就想要停止循环,然后运行循环下面的命令。这时候我们需要用到breakcontinue命令来帮我们控制住循环。

这两个命令在其他语言中基本都时关键字,特别是C,用法差不多。我也就不具体介绍了,只点出它们的功能。

break

在shell执行break命令时,它会尝试跳出当前正在执行的循环。

在处理多个循环时,break命令会自动终止你所在的最内层循环。

break命令接受单个命令行参数值:

​ break n

​ 其中n制订了要跳出的循环层级(层数)

continue

continue命令可以提前终止某次循环的命令,但并不会完全终止整个循环。可以在循环内部设置shell不执行命令的条件。

也就是说使用continue命令时,它会自动跳过本次循环中接下来的运行步骤,跳转到下一次循环。但注意不是跳出,跳出时break的功能。

同样的可以使用continue n n制定要继续执行哪一级循环

处理循环的输出

在shell脚本中,我们可以对循环的输出使用管道或进行重定向。这是通过在done命令之后添加一个处理命令来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
wangsx@SC-201708020022:~/tmp$ cat test
#!/bin/bash
for file in /home/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
else
echo "$file is a file"
fi
done > output.txt
wangsx@SC-201708020022:~/tmp$ cat output.txt
/home/wangsx is a directory

shell将for命令的结果重定向到文件output.txt中,而不是显示在屏幕上。

实例

下面两个例子演示如何用简单循环来处理数据。

查找可执行文件

Linux运行程序时通过环境变量$PATH提供的目录搜索可执行文件。如果徒手找的话,比较费时间,我们可以写个脚本来搞定它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
wangsx@SC-201708020022:~$ cat test25
#!/bin/bash
# finding files in the PATH

IFS=:
for folder in $PATH
do
echo "$folder:"
for file in $folder/*
do
if [ -x $file ]
then
echo " $file"
fi
done
done

# 输出结果太多,我就不拷贝结果了

先设定IFS分隔符以便于能正确分隔目录,然后将目录存放在$folder中,用for循环来迭代特定的目录中所有文件,然后用if-then命令检查文件的可执行权限。

Linux有一个tree工具,非常方便输出目录结构,推荐使用下。

创建多个用户账号

如果你是管理员,需要创建大量账号时。不必每次都有useradd命令添加用户。将用户信息存放在指定文件,然后用脚本进行处理就可以了。

用户信息的格式如下:

1
userid, user name

第一个是你为用户选择的id,第二个是用户的全名。这是csv文件格式。

为了能够读取它,我们使用以下命令:

1
while IFS=',' read -r userid name

read命令会自动获取.csv文本文件的下一行内容,所以不用再写一个循环来处理。当read命令返回FALSE时(也就是读完了),while命令就会退出。

为了把数据从文件导向while命令,只要再while命令尾部加一个重定向符号。

处理过程写成脚本如下:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# process new user accounts

input="users.csv"
while IFS=',', read -r userid name
do
trueecho "adding $userid"
trueuseradd -c "$name" -m $userid
done < "$input"

构建基本shell脚本

内容

  • 使用多个命令
  • 创建脚本文件
  • 显示消息
  • 使用变量
  • 输入输出重定向
  • 管道
  • 数学运算
  • 退出脚本

使用多个命令

如果多个命令一起使用,可以放在一行并用分号分隔。

1
2
3
wsx@wsx-ubuntu:~$ date; who
2017年 07月 26日 星期三 09:53:43 CST
wsx tty7 2017-07-26 09:48 (:0)

创建脚本文件

在创建脚本文件时,必须在文件的第一行指定要使用的shell,格式为:

1
#!/bin/bash

脚本文件的第一行中#后的惊叹号会告诉shell使用哪个shell来运行脚本(如果是其他编码语言脚本,像python,第一行类似)。

其他地方的#用作注释行。

添加名为test1的脚本文件,内容为:

1
2
3
4
#!/bin/bash
# This script displays the date and who's logged on
date
who

现在运行脚本,结果会是:

1
2
3
4
5
6
wsx@wsx-ubuntu:~/script_learn$ test1
未找到 'test1' 命令,您要输入的是否是:
命令 'testr' 来自于包 'python3-testrepository' (main)
命令 'testr' 来自于包 'python-testrepository' (universe)
命令 'test' 来自于包 'coreutils' (main)
test1:未找到命令

我们现在需要做的是让bash shell能够找到我们的脚本文件。shell会通过PATH环境变量来查找命令,我们可以看看:

1
2
wsx@wsx-ubuntu:~/script_learn$ echo $PATH
/home/wsx/Anaconda/bin:/home/wsx/Anoconda/ENTER/bin:/usr/lib/jvm/default-java/bin:/home/wsx/bin:/home/wsx/Anaconda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

很显然,我们的文件没有在这些目录范围内。要让shell找到test1脚本,我们可以采取以下两种做法之一:

  • 将shell脚本文件所处的目录添加到PATH环境变量中;
  • 在提示符中用绝对路径或相对路径来引用shell脚本文件。

第二种方法比较简单,我们在这里试试:

1
2
3
4
5
6
7
8
wsx@wsx-ubuntu:~/script_learn$ ./test1
bash: ./test1: 权限不够
wsx@wsx-ubuntu:~/script_learn$ ll test1 # 发现权限不够,查看文件的权限
-rw-rw-r-- 1 wsx wsx 73 7月 26 10:03 test1
wsx@wsx-ubuntu:~/script_learn$ chmod u+x test1 # 修改权限,添加可执行属性
wsx@wsx-ubuntu:~/script_learn$ ./test1 # 成功运行脚本
2017年 07月 26日 星期三 10:09:23 CST
wsx tty7 2017-07-26 09:48 (:0)

显示消息

echo命令后面加上一个字符串,就能显示出这个文本字符串。这种方式可以添加自己的文本消息来告诉脚本用户脚本正在做什么。

1
2
wsx@wsx-ubuntu:~/script_learn$ echo This is a test
This is a test

如果文本本身带有字符串,我们需要用单引号或双引号来划定文本字符串

1
2
wsx@wsx-ubuntu:~/script_learn$ echo "Let's see if this'll work"
Let's see if this'll work

我们修改下之前的test1文件,增加消息显示:

1
2
3
4
5
6
#!/bin/bash
# This script displays the date and who's logged on
echo The time and date are:
date
echo "Let's see who's logged into the system"
who

运行:

1
2
3
4
5
wsx@wsx-ubuntu:~/script_learn$ ./test1
The time and date are:
2017年 07月 26日 星期三 10:17:59 CST
Let's see who's logged into the system
wsx tty7 2017-07-26 09:48 (:0)

如果想把文本字符串和命令输出显示在同一行中,可以用echo语句的-n参数。需要在字符串的两侧加上引号,并且保证字符串尾部有一个空格(不然字符串和命令输出就粘连到一起了)。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# This script displays the date and who's logged on
echo -n "The time and date are: "
date
echo "Let's see who's logged into the system: "
who

# 运行结果输出
wsx@wsx-ubuntu:~/script_learn$ ./test1
The time and date are: 2017年 07月 26日 星期三 10:24:04 CST
Let's see who's logged into the system:
wsx tty7 2017-07-26 09:48 (:0)

使用变量

变量允许我们临时性地将信息存储在shell脚本中,以便和脚本中的其他命令一起使用。

环境变量

shell维护着一组环境变量,用来记录特定的系统信息。比如系统的名称、登录到系统上的用户名、用户的系统ID(也称为UID)、用户默认主目录以及shell查找程序的搜索路径。

使用set命令显示一份完整的当前环境变量列表。envprintenv命令都可以显示全局变量。(这些命令输出结果比较多,不展示了。之前关于环境变量的笔记有比较详细的描述。)

在环境变量名称之前加上美元符可以使用这些环境变量。

1
2
3
4
5
6
7
8
9
10
11
wsx@wsx-ubuntu:~/script_learn$ cat test2
#! /bin/bash
# display user information from the system
echo "User info for userid: $USER"
echo UID: $UID
echo HOME: $HOME
wsx@wsx-ubuntu:~/script_learn$ chmod u+x test2
wsx@wsx-ubuntu:~/script_learn$ ./test2
User info for userid: wsx
UID: 1000
HOME: /home/wsx

可以想象的到,如果我们想要使用实际的美元符而不是引用变量,肯定会出问题。这时候我们需要在美元符前面加上\进行转义,以显示美元符本身。

用户变量

使用等号将值赋给用户变量。注意,在变量、等号和值之间不能出现空格!这个是初学者常见的一个问题,本人也非常不太适应这个。因为在其他语言中不区分等号两边的空格,相信接触过其他脚本的朋友们肯定有习惯打空格使代码美观的,这在bash shell中是万万行不通滴。

一个使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
wsx@wsx-ubuntu:~/script_learn$ cat test3
#!/bin/bash
# testing variables
days=10
guest="Katie"
echo "$guest checked in $days days ago"
days=5
guest="Jessica"
echo "$guest checked in $days days ago"
wsx@wsx-ubuntu:~/script_learn$ chmod u+x test3
wsx@wsx-ubuntu:~/script_learn$ ./test3
Katie checked in 10 days ago
Jessica checked in 5 days ago

变量每次被引用时,都会输出当前赋给它的值。重要的是要记住,引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不需要使用美元符。

命令替换

shell脚本最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。

有两种方法可以将命令输出赋给变量:

  • 反引号字符(`)
  • $()格式
1
2
3
4
要么用一对反引号把整个命令行命令围起来:
testing=`date`
要么使用$()格式
testing=$(date)

下面是一个例子,在脚本中通过命令替换获得当前日期并用它来生成唯一文件名:

1
2
3
4
5
6
7
8
wsx@wsx-ubuntu:~/script_learn$ cat test4
#!/bin/bash
# copy the /usr/bin directory listing to a log file
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test4
wsx@wsx-ubuntu:~/script_learn$ ./test4

重定向输入和输出

通过几个操作符进行重定向,我们可以将命令的结果输出到另外的位置(文件)。当然,重定向可以用于输入。

输出重定向

最基本的操作符是>。比如我们想要输出命令结果到一个指定文件:

1
2
3
4
5
wsx@wsx-ubuntu:~/script_learn$ date > test6
wsx@wsx-ubuntu:~/script_learn$ ls -l test6
-rw-rw-r-- 1 wsx wsx 43 7月 26 16:42 test6
wsx@wsx-ubuntu:~/script_learn$ cat test6
2017年 07月 26日 星期三 16:42:34 CST

如果想要将命令的输出追加到已有文件中,需要用双大于号(>>)来追加数据。

输入重定向

输入重定向和输出重定向正好相反。输入重定向将文件的内容重定向到命令,而非将命令的输出重定向到文件。

使用的符号是小于号(<)。

一种简单的记忆方法是:在命令行上,命令总是在左侧,而重定向符号“指向”数据流动的方向。小于号说明数据正在从输入文件流向命令。

比如用wc命令检查文本的行数、词数和字节数。

1
2
wsx@wsx-ubuntu:~/script_learn$ wc < test6
1 6 43

另一种输入重定向的方法是内联输入重定向。它无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据即可。它使用的符号是远小于号(<<),除了这个符号,我们还需要指定一个文本标记用来划分输入数据的开始和结尾。任何字符串都可以作为文本标记,但在数据的开始和结尾文本标记必须一致。

1
2
3
4
5
6
wsx@wsx-ubuntu:~/script_learn$ wc << EOF
> test string1
> test string2
> test string3
> EOF
3 6 39

它的形式为:

1
2
3
command << marker
data
marker

管道

有时候需要将一个命令的输出作为另一个命令的输入。通过|符号分隔命令即可实现管道。

比如我想查看某个文件(test1)的前两行并进行排序,操作如下:

1
2
3
4
5
6
7
8
9
10
wsx@wsx-ubuntu:~/script_learn$ cat test1
#!/bin/bash
# This script displays the date and who's logged on
echo -n "The time and date are: "
date
echo "Let's see who's logged into the system: "
who
wsx@wsx-ubuntu:~/script_learn$ cat test1 | head -2 | sort
#!/bin/bash
# This script displays the date and who's logged on

管道的强大之处在于可以根据自己的需求灵活地组合和使用各种linux命令工具。这里只是一个简单的例子,要熟练掌握少不了平时多多研究和练习。

执行数学运算

对shell脚本来说,执行数学运算非常麻烦。有两种实现方式。

expr命令

expr命令允许在命令行上处理数学表达式,但是特别笨拙。(Bourne shell中)

1
2
3
4
wsx@wsx-ubuntu:~/script_learn$ exrpr 1 + 5
未找到 'exrpr' 命令,您要输入的是否是:
命令 'expr' 来自于包 'coreutils' (main)
exrpr:未找到命令

看到没有,那算了。它基本涉及的操作跟我们使用的其他语言是一致的。但是有些问题需要处理,像*是通配符,在运算是是做乘号处理的,需要进行转义。

使用方括号

bash shell提供了一种更简单的方法来执行数学表达式。在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号($[operator])将数学表达式围起来。

1
2
3
4
5
6
wsx@wsx-ubuntu:~/script_learn$ var1=$[1+5]
wsx@wsx-ubuntu:~/script_learn$ echo $var1
6
wsx@wsx-ubuntu:~/script_learn$ var2=$[$var1+2]
wsx@wsx-ubuntu:~/script_learn$ echo $var2
8

这种方式不仅方便,而且因为在方括号内,不会让shell误解乘号或其他符号。

但bash shell计算有一个主要限制:它只支持整数运算!

浮点解决方案

最常见的方案是用内建的bash计算器。它实际上是一门编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。bash计算器能够识别:

  • 数字(整数和浮点数)
  • 变量(简单变量和数组)
  • 注释(/ /开始的行)
  • 表达式
  • 编程语句
  • 函数
1
2
3
4
5
6
7
8
9
10
wsx@wsx-ubuntu:~/script_learn$ bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
12 * 5.4
64.8
3.156 * (3 + 5)
25.248
quit

在脚本中使用bc

可以用命令替换运行bc命令,并将输出赋给一个变量。基本格式如下:

1
variable=$(echo "options; expression" | bc)

options设置变量,expression参数定义了通过bc执行的数学表达式。

看一个简单实例:

1
2
3
4
5
6
7
wsx@wsx-ubuntu:~/script_learn$ cat test9
#!/bin/bash
var1=$(echo "scale=4; 3.44/5" | bc)
echo The answer is $var1
wsx@wsx-ubuntu:~/script_learn$ chmod u+x test9
wsx@wsx-ubuntu:~/script_learn$ ./test9
The answer is .6880

这个例子将scale变量设置为四位小数,并在expression部分指定了特定的运算。

这个方法适用于较短的运算,但有时我们会涉及更多的数字。如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。

这里有一个解决方法:使用内联输入重定向,将一个文件重定向到bc命令来处理。格式为:

1
2
3
4
5
variable=$(bc << EOF
options
statements
expressions
EOF)

EOF文本字符串标识了内联重定向数据的起始。注意,仍然需要命令替换符号将bc命令的输出赋给变量。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wsx@wsx-ubuntu:~/script_learn$ cat test10
#!/bin/bash

var1=10.46
var2=43.67
var3=33.2
var4=71

var5=$(bc <<EOF
scale=4
a1 = ( $var1 * $var2)
b1 = ( $var3 * $var4)
a1 + b1
EOF
)

echo The final answer for this mess is $var5
wsx@wsx-ubuntu:~/script_learn$ chmod u+x test10
wsx@wsx-ubuntu:~/script_learn$ ./test10
The final answer for this mess is 2813.9882

在普通的shell脚本中,数字默认当做字符串处理。这也是为什么我们脚本处理计算麻烦和我们需要特定的工具和方法来进行处理。一定要注意区分。

退出脚本

前面运行的脚本都是命令执行完成,脚本自动结束。其实我们可以用更为优雅的方式告诉shell命令运行完成,因为每个命令都使用退出状态码(exit status),它是一个0-255的整数值,我们可以捕获这个值并在脚本中使用。

Linux提供了一个专门的变量$?来保存上个已执行命令的退出状态码。

1
2
3
4
wsx@wsx-ubuntu:~/script_learn$ date
2017年 07月 27日 星期四 10:44:18 CST
wsx@wsx-ubuntu:~/script_learn$ echo $?
0

按照惯例,一个成功结束的命令的退出状态码是0。如果有错误,则显示一个正数值。

Linux错误退出状态码没有什么标准,但有一些参考:

状态码 描述
0 命令成功结束
1 一般性未知错误
2 不适合的shell命令
126 命令不可执行
127 没找到命令
128 无效的退出参数
128+x 与Linux信号x相关的严重错误
130 通过Ctrl+C终止的命令
255 正常范围之外的退出状态码
1
2
3
4
5
6
wsx@wsx-ubuntu:~/script_learn$ asfg
未找到 'asfg' 命令,您要输入的是否是:
命令 'asdfg' 来自于包 'aoeui' (universe)
asfg:未找到命令
wsx@wsx-ubuntu:~/script_learn$ echo $?
127

exit命令

默认,shell脚本会以脚本最后的一个命令的退出状态码退出。

但是我们可以改变这种默认行为,返回自己的退出状态码。exit命令允许在脚本结束时指定一个状态退出码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wsx@wsx-ubuntu:~/script_learn$ cat test13
#!/bin/bash
# testing the exit status
var1=10
var2=30
var3=$[$var1 + $var2]
echo The answer is $var3
exit 5

wsx@wsx-ubuntu:~/script_learn$ chmod u+x test13
wsx@wsx-ubuntu:~/script_learn$ ./test13
The answer is 40
wsx@wsx-ubuntu:~/script_learn$ echo $?
5

注意最大255,如果大于它,得到的是求模的结果(余数)。

|