Shell

文章目录

  • Shell
    • 基础知识
      • 脚本执行
      • 变量
        • 变量使用
        • 变量作用域
      • 参数
        • 位置参数
      • 特殊变量及含义
        • **$?** 获取上一个命令的退出状态或获取函数返回值
      • 字符串
        • 获取字符串长度
        • 字符串拼接
        • 字符串截取
      • 数组
      • 内建命令
        • **alias**
        • **echo**命令:
        • **read**
        • **exit**
        • **declare**
      • 数学计算
        • **数学计算命令**
        • **shell(())**
        • **Shell let**
        • **Shell $[]**
        • Shell expr
        • Shell bc
        • **declare -i**
      • **Shell if else**
      • 退出状态
        • **test**
        • **[[]]**
        • case in
          • while
        • until
        • for
        • select in循环
        • break continue
      • Shell函数
        • 函数定义
        • 函数参数
        • 函数返回值
        • Shell重定向(输入输出重定向)
      • 模块化(source命令)
        • 子Shell和子进程
      • shell组命令
      • Shell进程替换
      • 文件描述符
        • Shell exec命令操作文件描述符
      • Linux Shell管道

Shell

主要参考

基础知识

Shell 可以通过调用其他程序,进行无限扩展
主要用于开发自动化的小工具
常见的 Shell 有 sh、bash、csh、tcsh、ash
Shell 就是这样的一款软件,不同的组织机构开发了不同的 Shell,它们各有所长,有的占用资源少,有的支持高级编程功能,有的兼容性好,有的重视用户体验。
Shell 既是一种脚本编程语言,也是一个连接内核和用户的软件。
现代的 Linux 上,sh 已经被 bash 代替,/bin/sh往往是指向/bin/bash的符号链接

cat /etc/shells
echo $SHELL

对于普通用户,Base shell 默认的提示符是美元符号$;对于超级用户(root 用户),Bash Shell 默认的提示符是井号#

  • Shell 通过PS1和PS2两个环境变量来控制提示符格式:
  • PS1 控制最外层命令行的提示符格式。
  • PS2 控制第二层命令行的提示符格式。
[mozhiyan@localhost ~]$ echo $PS1
[\u@\h \W]\$
[mozhiyan@localhost ~]$ echo $PS2
>
[mozhiyan@localhost ~]$ 

脚本执行

#!/bin/bash       
#!是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,或在执行时,
/bin/bash test.sh  #使用Bash的绝对路径
echo "Hello World !"  #这是一条语句
  • 执行shell脚本,在终端需要使用chmod命令给脚本加上执行权限。 chmod 755 filename.sh ./filename.sh执行shell脚本
  • Linux 中的每一个进程都有一个唯一的 ID,称为 PID,使用$$变量就可以获取当前进程的 PID。
  • source是 Shell 内置命令的一种,它会读取脚本文件中的代码,并依次执行所有语句。你也可以理解为,source 命令会强制执行脚本文件中的全部命令,而忽略脚本文件的权限. source filename 或者 . filename 两种写法的效果相同。对于第二种写法,注意点号.和文件名中间有一个空格。

变量

变量使用

在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储

**定义变量**  
variable=value
variable='value'
variable="value"
赋值号=的周围不能有空格  
  • Shell 变量的命名规范和大部分编程语言都一样:

变量名由数字、字母、下划线组成;
必须以字母或者下划线开头;
不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。

url=http://c.biancheng.net/shell/
echo $url
name='C语言中文网'
echo $name
author="严长生"
echo ${author}
变量名外面的花括号{ }是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界  

**修改变量的值 **   
url="http://c.biancheng.net"
echo ${url}
url="http://c.biancheng.net/shell/"
echo ${url} 

单双引号区别

#!/bin/bash
url="http://c.biancheng.net"
website1='C语言中文网:${url}'
website2="C语言中文网:${url}"
echo $website1  >> C语言中文网:${url}
echo $website2  >> C语言中文网:http://c.biancheng.net  
以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出  
以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出  

将命令的结果赋值给变量

variable=`command`
variable=$(command)  

只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变,类似于其他语言的常量

#!/bin/bash
myUrl="http://c.biancheng.net/shell/"
readonly myUrl
myUrl="http://c.biancheng.net/shell/"  

删除变量

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

变量作用域

  • Shell 变量的作用域可以分为三种:
  • 有的变量可以在***当前 Shell 会话***中使用,这叫做全局变量(global variable);
  • 有的变量只能在函数内部使用,这叫做局部变量(local variable);
  • 而有的变量还可以在其它 Shell 中使用,这叫做环境变量(environment variable)

全局变量:一般定义的变量均为全局变量
局部变量:要想变量的作用域仅限于函数内部,那么可以在定义时加上local命令,此时该变量就成了局部变量

#!/bin/bash
#定义函数
function func(){
    local a=99
}
#调用函数
func
#输出函数内部的变量
echo $a

环境变量:如果使用export命令将它导出,那么它就在所有的子 Shell 中也有效了,这称为“环境变量”
环境变量被创建时所处的 Shell 被称为父 Shell,如果在父 Shell 中再创建一个 Shell,则该 Shell 被称作子 Shell。当子 Shell 产生时,它会继承父 Shell 的环境变量为自己所用,所以说环境变量可从父 Shell 传给子 Shell。不难理解,环境变量还可以传递给孙 Shell

注意,环境变量只能向下传递而不能向上传递,即“传子不传父”。

export a这种形式是在定义变量 a 以后再将它导出为环境变量,如果想在定义的同时导出为环境变量,可以写作export a=22

参数

位置参数

  • 通过$n的形式来接收的参数,在 Shell 中称为位置参数。
  • Shell 函数参数的传递没有所谓的形参和实参,在定义函数时也不用指明参数的名字和数目。换句话说,定义 Shell 函数时不能带参数,但是在调用函数时却可以传递参数,

给脚本文件传递位置参数

下面的代码命名为 test.sh:  
#!/bin/bash  
echo "Language: $1"
echo "URL: $2"

运行 test.sh,并附带参数:

. ./test.sh Shell http://c.biancheng.net/shell/
>> Language: Shell
>>URL: http://c.biancheng.net/shell/  

给函数传递位置参数

编写下面的代码,并命名为 test.sh:
#!/bin/bash
#定义函数
function func(){
    echo "Language: $1"
    echo "URL: $2"
}
#调用函数
func C++ http://c.biancheng.net/cplus/  

运行 test.sh:
. ./test.sh
>>Language: C++
>>URL: http://c.biancheng.net/cplus/  

特殊变量及含义

变量 含义
$0 当前脚本的文件名。
$n(n≥1) 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。当被双引号" "包含时,$@ 与 $* 稍有不同
$? 上个命令的退出状态,或函数的返回值,我们将在《Shell $?》一节中详细讲解。
$$ 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID。

$ $@ 区别*

  • $*$@ 不被双引号" "包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔。
  • 它们被双引号" "包含时,就会有区别:
  • "$*"会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据
  • "$@"仍然将每个参数都看作一份数据,彼此之间是独立的

编写下面的代码,并保存为 test.sh:

#!/bin/bash
echo "print each param from \"\$*\""
for var in "$*"
do
    echo "$var"
done
echo "print each param from \"\$@\""
for var in "$@"
do
    echo "$var"
done  

运行 test.sh,并附带参数:
 . ./test.sh a b c d
print each param from "$*"
a b c d
print each param from "$@"
a
b
c
d
对于"$*",只循环了 1 次,因为它只有 1 分数据;对于"$@",循环了 5 次,因为它有 5 份数据。

$? 获取上一个命令的退出状态或获取函数返回值

退出状态:
所谓退出状态,就是上一个命令执行后的返回结果。退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1

编写下面的代码,并保存为 test.sh:
#!/bin/bash
if [ "$1" == 100 ]
then
   exit 0  #参数正确,退出状态为0
else
   exit 1  #参数错误,退出状态1
fi

运行 test.sh 时传递参数 100:

./test.sh 100  #作为一个新进程运行
echo $?
>>0

再如,运行 test.sh 时传递参数 89:

./test.sh 89  #作为一个新进程运行 
echo $?
>>1

获取函数的返回值

编写下面的代码,并保存为 test.sh:
#!/bin/bash
#得到两个数相加的和
function add(){
    return `expr $1 + $2`
}
add 23 50  #调用函数
echo $?  #获取函数返回值

运行结果:
73

严格来说,Shell 函数中的 return 关键字用来表示函数的退出状态,而不是函数的返回值

字符串

三种形式字符串:

  • 由单引号' '包围的字符串:
  • 任何字符都会原样输出,在其中使用变量是无效的。
  • 字符串中不能出现单引号,即使对单引号进行转义也不行。
  • 由双引号" "包围的字符串:
  • 如果其中包含了某个变量,那么该变量会被解析(得到该变量的值),而不是原样输出。
  • 字符串中可以出现双引号,只要它被转义了就行。
  • 不被引号包围的字符串
  • 不被引号包裹的字符串中出现变量时也会被解析,与双引号" "包裹的字串一样
  • 字符串中不能出现空格,否则空格后面的字符串会作为其他变量或命令解析
#!/bin/bash
n=74
str1=c.biancheng.net$n 
str2="shell \"script\" $n"
str3='C语言中文网 $n'
echo $str1
echo $str2
echo $str3

运行结果:
c.biancheng.net74
shell "script" 74
C语言中文网 $n

获取字符串长度

${#string_name}string_name 表示字符串名字。

#!/bin/bash
str="http://c.biancheng.net/shell/"
echo ${#str}

运行结果:
29

字符串拼接

在 Shell 中你不需要使用任何运算符,将两个字符串并排放在一起就能实现拼接

#!/bin/bash
name="Shell"
url="http://c.biancheng.net/shell/"
str1=$name$url  #中间不能有空格
str2="$name $url"  #如果被双引号包围,那么中间可以有空格
str3=$name": "$url  #中间可以出现别的字符串
str4="$name: $url"  #这样写也可以
str5="${name}Script: ${url}index.html"  #这个时候需要给变量名加上大括号
echo $str1
echo $str2
echo $str3
echo $str4
echo $str5

运行结果:
Shellhttp://c.biancheng.net/shell/
Shell http://c.biancheng.net/shell/
Shell: http://c.biancheng.net/shell/
Shell: http://c.biancheng.net/shell/
ShellScript: http://c.biancheng.net/shell/index.html

字符串截取

  1. 从字符串左边开始计数
    ${string: start :length}string 是要截取的字符串,start 是起始位置(从左边开始,从 0 开始计数),length 是要截取的长度(省略的话表示直到字符串的末尾)。

    url=“c.biancheng.net”
    echo ${url: 2: 9}
    结果为biancheng

  2. 从右边开始计数
    ${string: 0-start :length}第 2) 种格式仅仅多了0-,这是固定的写法,专门用来表示从字符串右边开始计数。

不管从哪边开始计数,截取方向都是从左到右

url="c.biancheng.net"
echo ${url: 0-13: 9}
结果为biancheng。从右边数,b是第 13 个字符。

url="c.biancheng.net"
echo ${url: 0-13}  #省略 length,直接截取到字符串末尾
结果为biancheng.net  
  1. 从指定字符(子字符串)开始截取
    这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾
  • 使用 # 号截取右边字符
    ${string#*chars}string 表示要截取的字符,chars 是指定的字符(或者子字符串),*是通配符的一种,表示任意长度的字符串。 *chars连起来使用的意思是:忽略左边的所有字符,直到遇见 chars(chars 不会被截取)。
	url="http://c.biancheng.net/index.html"
	echo ${url#*:}
	结果为//c.biancheng.net/index.html。

以上写法遇到第一个匹配的字符(子字符串)就结束了。例如:

	url="http://c.biancheng.net/index.html"
	echo ${url#*/}
	结果为/c.biancheng.net/index.html

如果希望直到最后一个指定字符(子字符串)再匹配结束,那么可以使用##: ${string##*chars}

#!/bin/bash
url="http://c.biancheng.net/index.html"
echo ${url#*/}    #结果为 /c.biancheng.net/index.html
echo ${url##*/}   #结果为 index.html
str="---aa+++aa@@@"
echo ${str#*aa}   #结果为 +++aa@@@
echo ${str##*aa}  #结果为 @@@
  • 使用 % 截取左边字符
    使用%号可以截取指定字符(或者子字符串)左边的所有字符${string%chars*}

      #!/bin/bash
      url="http://c.biancheng.net/index.html"
      echo ${url%/*}  #结果为 http://c.biancheng.net
      echo ${url%%/*}  #结果为 http:
      str="---aa+++aa@@@"
      echo ${str%aa*}  #结果为 ---aa+++
      echo ${str%%aa*}  #结果为 ---	
    

汇总

格式 说明
${string: start :length} 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。
${string: start} 从 string 字符串的左边第 start 个字符开始截取,直到最后。
${string: 0-start :length} 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。
${string: 0-start} 从 string 字符串的右边第 start 个字符开始截取,直到最后。
${string#*chars} 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
${string##*chars} 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
${string%*chars} 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。
${string%%*chars} 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。

数组

常用的 Bash Shell 只支持一维数组,不支持多维数组。Shell 数组元素的下标也是从 0 开始计数。
数组定义array_name=(ele1 ele2 ele3 ... elen),元素空格隔开 注意,赋值号=两边不能有空格,必须紧挨着数组名和数组元素

nums=(29 100 13 8 91 44)
3
#Shell 是弱类型的,它并不要求所有数组元素的类型必须相同:
arr=(20 56 "http://c.biancheng.net/shell/")

#Shell 数组的长度不是固定的,定义之后还可以增加元素。
nums[6]=88

#无需逐个元素地给数组赋值,下面的代码就是只给特定元素赋值:
ages=([3]=24 [5]=19 [10]=12)
#以上代码就只给第 3、5、10 个元素赋值,所以数组长度是 3。

获取数组元素:

	#!/bin/bash
	nums=(29 100 13 8 91 44)
	echo ${nums[@]}  #输出所有数组元素
	nums[10]=66  #给第10个元素赋值(此时会增加数组长度)
	echo ${nums[*]}  #输出所有数组元素
	echo ${nums[4]}  #输出第4个元素
	运行结果:
	29 100 13 8 91 44
	29 100 13 8 91 44 66
	91

获取数组长度
利用@或*,可以将数组扩展成列表,然后使用#来获取数组元素的个数

${#array_name[@]}
${#array_name[*]}

	#!/bin/bash
	nums=(29 100 13)
	echo ${#nums[*]}
	#向数组中添加元素
	nums[10]="http://c.biancheng.net/shell/"
	echo ${#nums[@]}
	echo ${#nums[10]}
	#删除数组元素
	unset nums[1]
	echo ${#nums[*]}
	运行结果:
	3
	4
	29
	3

数组拼接,数组合并
拼接数组的思路是:先利用@或*,将数组扩展成列表,然后再合并到一起。

array_new=(${array1[@]}  ${array2[@]})
array_new=(${array1[*]}  ${array2[*]})

	#演示代码:
	#!/bin/bash
	array1=(23 56)
	array2=(99 "http://c.biancheng.net/shell/")
	array_new=(${array1[@]} ${array2[*]})
	echo ${array_new[@]}  #也可以写作 ${array_new[*]}
	运行结果:
	23 56 99 http://c.biancheng.net/shell/

删除数组元素或数组

删除数组元素:unset array_name[index]其中,array_name 表示数组名,index 表示数组下标。
删除整个数组:unset array_name

#!/bin/bash
arr=(23 56 99 "http://c.biancheng.net/shell/")
unset arr[1]
echo ${arr[@]}
unset arr
echo ${arr[*]}
运行结果:
>> 23 99 http://c.biancheng.net/shell/
>>

关联数组
关联数组也称为“键值对(key-value)”数组,键(key)也即字符串形式的数组下标,值(value)也即元素值。

declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"

#也可以在定义的同时赋值:
declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff") 

关联数组必须使用带有-A选项的 declare 命令创建

访问关联数组元素:array_name["index"]

color["white"]="#ffffff"
color["black"]="#000000"

#加上$()即可获取数组元素的值:
$(array_name["index"])

echo $(color["white"])
white=$(color["black"])

示例:

#!/bin/bash
declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"
color["white"]="#ffffff"
color["black"]="#000000"
#获取所有元素值
for value in ${color[*]}
do
    echo $value
done
echo "****************"
#获取所有元素下标(键)
for key in ${!color[*]}
do
    echo $key
done
echo "****************"
#列出所有键值对
for key in ${!color[@]}
do
    echo "${key} -> ${color[$key]}"
done

运行结果:
#ff0000
#0000ff
#ffffff
#000000
#00ff00
****************
red
blue
white
black
green
****************
red -> #ff0000
blue -> #0000ff
white -> #ffffff
black -> #000000
green -> #00ff00

内建命令

alias

.bashrc其实也是一个 Shell 脚本文件,该文件专门用来存放用户自定义的别名和函数。

#将别名写入.bashrc文件后的效果如下所示:
# .bashrc
# User specific aliases and functions
alias myShutdown='shutdown -h now'
# Source global definitions
if [ -f /etc/bashrc ]; then
    . /etc/bashrc
fi

unalias:删除别名

echo命令:

  • 输出字符串
    echo 命令输出结束后默认会换行,如果不希望换行,可以加上-n参数

      #!/bin/bash
      name="Tom"
      age=20
      height=175
      weight=62
      echo -n "${name} is ${age} years old, "
      echo -n "${height}cm in height "
      echo "and ${weight}kg in weight."
      echo "Thank you!"
      
      #运行结果:
      Tom is 20 years old, 175cm in height and 62kg in weight.
      Thank you!
    
  • 输出转义字符
    默认情况下,echo 不会解析以反斜杠\开头的转义字符,添加-e参数来让 echo 命令解析转义字符

      echo -e "hello \nworld"
      hello
      world  
    

    有了-e参数,我们也可以使用转义字符\c来强制 echo 命令不换行了

         #!/bin/bash
      	name="Tom"
      	age=20
      	height=175
      	weight=62
      	echo -e "${name} is ${age} years old, \c"
      	echo -e "${height}cm in height \c"
      	echo "and ${weight}kg in weight."
      	echo "Thank you!"
      	
      	#运行结果:
      	Tom is 20 years old, 175cm in height and 62kg in weight.
      	Thank you!
    

read

用来从标准输入中读取数据并赋值给变量,如果没有进行重定向,默认就是从键盘读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据

read [-options] [variables]options表示选项,如下表所示;variables表示用来存储数据的变量,可以有一个,也可以有多个。

选项 说明
-a array 把读取的数据赋值给数组 array,从下标 0 开始。
-d delimiter 用字符串 delimiter 指定读取结束的位置,而不是一个换行符(读取到的数据不包括 delimiter)。
-e 在获取用户输入的时候,对功能键进行编码转换,不会直接显式功能键对应的字符。
-n num 读取 num 个字符,而不是整行字符。
-p prompt 显示提示信息,提示内容为 prompt。
-r 原样读取(Raw mode),不把反斜杠字符解释为转义字符。
-s 静默模式(Silent mode),不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这是很有必要的。
-t seconds 设置超时时间,单位为秒。如果用户没有在指定时间内输入完成,那么 read
-u fd 使用文件描述符 fd 作为输入源,而不是标准输入,类似于重定向。

使用 read 命令给多个变量赋值:

#!/bin/bash
read -p "Enter some information > " name url age
echo "网站名字:$name"
echo "网址:$url"
echo "年龄:$age"

#运行结果:
Enter some information > C语言中文网 http://c.biancheng.net 7↙
网站名字:C语言中文网
网址:http://c.biancheng.net
年龄:7

只读取一个字符:

#!/bin/bash
read -n 1 -p "Enter a char > " char
printf "\n"  #换行
echo $char

#运行结果:
Enter a char > 1
1

在指定时间内输入密码:

#!/bin/bash
if
    read -t 20 -sp "Enter password in 20 seconds(once) > " pass1 && printf "\n" &&  #第一次输入密码
    read -t 20 -sp "Enter password in 20 seconds(again)> " pass2 && printf "\n" &&  #第二次输入密码
    [ $pass1 == $pass2 ]  #判断两次输入的密码是否相等
then
    echo "Valid password"
else
    echo "Invalid password"
fi
# 使用&&组合了多个命令

exit

exit 命令可以接受一个整数值作为参数,代表退出状态。如果不指定,默认状态值是 0。
一般情况下,退出状态为 0 表示成功,退出状态为非 0 表示执行失败(出错)了。
exit 退出状态只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。

#!/bin/bash
echo "befor exit"
exit 8
echo "after exit"

./test.sh
befor exit
echo $? #使用$?来获取 test.sh 的退出状态:
8

declare

设置变量属性 declare [+/-] [aAfFgilprtux] [变量名=变量值] 其中,-表示设置属性,+表示取消属性,aAfFgilprtux都是具体的选项

选项 含义
-f [name] 列出之前由用户在脚本中定义的函数名称和函数体。
-F [name] 仅列出自定义函数名称。
-g name 在 Shell 函数内部创建全局变量。
-p [name] 显示指定变量的属性和值。
-a name 声明变量为普通数组。
-A name 声明变量为关联数组(支持索引下标为字符串)。
-i name 将变量定义为整数型。
-r name[=value] 将变量定义为只读(不可修改和删除),等价于 readonly name。
-x name[=value] 将变量设置为环境变量,等价于 export name[=value]。

将变量声明为整数并进行计算:

#!/bin/bash
declare -i m n ret  #将多个变量声明为整数
m=10
n=30
ret=$m+$n
echo $ret

#运行结果:
40

将变量定义为只读变量:

declare -r n=10
n=20
>>bash: n: 只读变量
echo $n
>>10

显示变量的属性和值:

declare -r n=10
declare -p n
>>declare -r n="10"

###命令替换
将命令的输出结果赋值给变量

有两种方式可以完成命令替换,一种是反引号,一种是$(),使用方法如下:

var_name=`command`
var_name=$(command)

	#!/bin/bash
	DATE_01=`date`
	DATE_02=$(date)
	echo $DATE_01
	echo $DATE_02
	
	#运行结果:
	2018年 10月 31日 星期三 10:15:16 CST
	2018年 10月 31日 星期三 10:15:16 CST
  • 如果被替换的命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围

      #!/bin/bash
      LSL=`ls -l`
      echo $LSL  #不使用双引号包围
      echo "--------------------------"  #输出分隔符
      echo "$LSL"  #使用引号包围
      
      运行结果:
      total 8 drwxr-xr-x. 2 root root 21 7月 1 2016 abc -rw-rw-r--. 1 mozhiyan mozhiyan 147 10月 31 10:29 demo.sh -rw-rw-r--. 1 mozhiyan mozhiyan 35 10月 31 10:20 demo.sh~
      --------------------------
      total 8
      drwxr-xr-x. 2 root     root      21 7月   1 2016 abc
      -rw-rw-r--. 1 mozhiyan mozhiyan 147 10月 31 10:29 demo.sh
      -rw-rw-r--. 1 mozhiyan mozhiyan  35 10月 31 10:20 demo.sh~
    

数学计算

算术运算符 说明/含义
+、- 加法(或正号)、减法(或负号)
*、/、% 乘法、除法、取余(取模)
** 幂运算
++、– 自增和自减,可以放在变量的前面也可以放在变量的后面
!、&&、
<、<=、>、>= 比较符号(小于、小于等于、大于、大于等于)
==、!=、= 比较符号(相等、不相等;对于字符串,= 也可以表示相当于)
<<、>> 向左移位、向右移位
~、 、 &、^
=、+=、-=、*=、/=、%= 赋值运算符,例如 a+=1 相当于 a=a+1,a-=1 相当于 a=a-1

Shell 不能直接进行算数运算,必须使用数学计算命令,默认情况下,Shell 不会直接进行算术运算

数学计算命令

***学习 (()) 和 bc 即可

运算操作符/运算命令 说明
(( )) 用于整数运算,效率很高,推荐使用。
let 用于整数运算,和 (()) 类似。
$[] 用于整数运算,不如 (()) 灵活。
expr 可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。
bc Linux下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。
declare -i 将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用。

shell(())

表达式可以只有一个,也可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个(( ))命令的执行结果

运算操作符/运算命令 说明
((a=10+66) ((b=a-15)) ((c=a+b)) 这种写法可以在计算完成后给变量赋值。以((b=a-15)) 为例,即将 a-15 的运算结果赋值给变量 c。 注意,使用变量时不用加$前缀,(( )) 会自动解析变量名。
a= ( ( 10 + 66 ) b = ((10+66) b= ((10+66)b=((a-15)) c=$((a+b)) 可以在 (( )) 前面加上 符 号 获 取 ( ( ) ) 命 令 的 执 行 结 果 , 也 即 获 取 整 个 表 达 式 的 值 。 以 c = 符号获取 (( )) 命令的执行结果,也即获取整个表达式的值。以 c= (())c=((a+b)) 为例,即将 a+b 这个表达式的运算结果赋值给变量 c。 注意,类似 c=((a+b)) 这样的写法是错误的,不加$就不能取得表达式的结果。
((a>7 && b==c)) (( )) 也可以进行逻辑运算,在 if 语句中常会使用逻辑运算。
echo $((a+10)) 需要立即输出表达式的运算结果时,可以在 (( )) 前面加$符号。
((a=3+5, b=a+10)) 对多个表达式同时进行计算。

基本复杂运算:

((a=1+2**3-4%3))
echo $a
>>8

b=$((1+2**3-4%3)) #运算后将结果赋值给变量,变量放在了括号的外面。
echo $b
>>8

echo $((1+2**3-4%3)) #也可以直接将表达式的结果输出,注意不要丢掉 $ 符号。
>>8

a=$((100*(100+1)/2)) #利用公式计算1+2+3+...+100的和。
echo $a
>>5050

echo $((100*(100+1)/2)) #也可以直接输出表达式的结果。
5050

利用(()) 进行逻辑运算:

echo $((3<8))  #3<8 的结果是成立的,因此,输出了 1,1 表示真
>>1
echo $((8<3))  #8<3 的结果是不成立的,因此,输出了 0,0 表示假。
>>0
echo $((8==8)) #判断是否相等。
>>1

if ((8>7&&5==5))
> then
> echo yes
> fi
yes

利用 (( )) 同时对多个表达式进行计算:

((a=3+5, b=a+10))  #先计算第一个表达式,再计算第二个表达式
echo $a $b
8 18

c=$((4+8, a+b))  #以最后一个表达式的结果作为整个(())命令的执行结果
echo $c
26

Shell let

只能对整数进行计算
let 表达式let "表达式"let '表达式' => 都等价于((表达式))

  • let 命令以空格来分隔多个表达式;

  • (( )) 以逗号,来分隔多个表达式。

      a=10 b=20
      echo $((a+b))
      >>30
      echo let a+b  #错误,echo会把 let a+b作为一个字符串输出
      >>let a+b
    
      #给变量 i 加 8:
      i=2
      let i+=8
      echo $i
      >>10
      
      let 后面可以跟多个表达式:
      a=10 b=35
      let a+=6 c=a+b  #多个表达式以空格为分隔
      echo $a $c
      >>16 51
    

Shell $[]

$[] 也只能进行整数运算

echo $[3*5]  #直接输出结算结果
>>15

echo $[(3+4)*5]  #使用()
35
	
n=6
m=$[n*2]  #将计算结果赋值给变量
echo $[m+n]
>>18	

echo $[$m*$n]  #在变量前边加$也是可以的
>>72

注意的是,不能单独使用 $[],必须能够接收 $[] 的计算结果:
 $[3+4]
>>bash: 7: 未找到命令...

$[m+3]
bash: 15: 未找到命令...

Shell expr

除了可以实现整数计算,还可以结合一些选项对字符串进行处理,例如计算字符串长度、字符串比较、字符串匹配、字符串提取等

expr对表达式的格式有几点特殊的要求:

  • 出现在表达式中的运算符、数字、变量和小括号的左右两边至少要有一个空格,否则会报错。

  • 有些特殊符号必须用反斜杠\进行转义(屏蔽其特殊含义),比如乘号*和小括号(),如果不用\转义,那么 Shell 会把它们误解为正则表达式中的符号(*对应通配符,()对应分组)。

  • 使用变量时要加$前缀。

      expr 2 +3  #错误:加号和 3 之前没有空格
      >>expr: 语法错误
      
      expr 2 + 3  #这样才是正确的
      >>5
      
      expr 4 * 5  #错误:乘号没有转义
      >>expr: 语法错误
      
      expr 4 \* 5  #使用 \ 转义后才是正确的
      >>20
      
      expr ( 2 + 3 ) \* 4  #小括号也需要转义
      >>bash: 未预期的符号 `2' 附近有语法错误
      
      expr \( 2 + 3 \) \* 4  #使用 \ 转义后才是正确的
      >>20
      
      n=3
      expr n + 2  #使用变量时要加 $
      >>expr: 非整数参数
      
      expr $n + 2  #加上 $ 才是正确的
      >>5
      
      m=7
      expr $m \* \( $n + 5 \)
      >>56
      
      如果希望将计算结果赋值给变量,那么需要将整个表达式用反引号``(位于 Tab 键的上方)包围起来
      将 expr 的计算结果赋值给变量:
      m=5
      n=`expr $m + 10`
      echo $n
      >>15
    

Shell bc

交互式
在终端输入bc命令,然后回车即可进入bc进行交互式的数学计算,bc -q清爽显示
bc 还支持函数、循环结构、分支结构等常见的编程元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PdthOxNy-1597903870361)(./res/bc_function.png)]

  • 内置变量:
变量名 作 用
scale 指定精度,也即小数点后的位数;默认为 0,也即不使用小数部分。
ibase 指定输入的数字的进制,默认为十进制。
obase 指定输出的数字的进制,默认为十进制。
last 或者 . 表示最近打印的数字
bc -q
23*2
46

obase=16
23*2
2E

obase=10
ibase=16
10*10
256

在一行中使用多个表达式:
Shell_第1张图片
Shell 中使用 bc
在 Shell 脚本中,我们可以借助管道或者输入重定向来使用 bc 计算器

  • 管道是 Linux 进程间的一种通信机制,它可以将前一个命令(进程)的输出作为下一个命令(进程)的输入,两个命令之间使用竖线|分隔
  • 通常情况下,一个命令从终端获得用户输入的内容,如果让它从其他地方(比如文件)获得输入,那么就需要重定向。

借助管道使用 bc 计算器:
echo "expression" | bc

将 bc 的计算结果赋值给 Shell 变量
variable=$(echo "expression" | bc)

echo "3*8"|bc
>>24

ret=$(echo "4+9"|bc)
echo $ret
>>13

echo "scale=4;3*8/7"|bc
>>3.4285

echo "scale=4;3*8/7;last*5"|bc
>>3.4285
>>17.1425 

x=4
echo "scale=5;n=$x+2;e(n)"|bc -l
>>403.42879

借助输入重定向使用 bc 计算器

variable=$(bc << EOF
expressions
EOF
)

variable是 Shell 变量名,express是要计算的数学表达式(可以换行,和进入 bc 以后的书写形式一样),EOF是数学表达式的开始和结束标识(你也可以换成其它的名字,比如 aaa、bbb 等)。

m=1E
n=$(bc << EOF
> obase=10;
> ibase=16;
> print $m
> EOF
> )
echo $n
>>30

declare -i

#!/bin/bash
declare -i m n ret
m=10
n=30
ret=$m+$n
echo $ret
ret=$n/$m
echo $ret

除了将参与运算的变量定义为整数,还得将承载结果的变量定义为整数,而且只能用整数类型的变量来承载运算结果,不能直接使用echo输出

Shell if else

if  condition
then
    statement(s)
fi

condition是判断条件,如果condition成立(返回“真”),那么then后边的语句将会被执行;如果condition不成立(返回“假”),那么不会执行任何语句

#!/bin/bash
read a
read b
if (( $a == $b ))
then
    echo "a和b相等"
fi

if else

if  condition
then
   statement1
else
   statement2
fi

example:

#!/bin/bash
read a
read b
if (( $a == $b ))
then
    echo "a和b相等"
else
    echo "a和b不相等,输入错误"
fi

if elif else

if  condition1
then
   statement1
elif condition2
then
    statement2
elif condition3
then
    statement3
……
else
   statementn
fi

if 和 elif 后边都得跟着 then。

#!/bin/bash
read age
if (( $age <= 2 )); then
    echo "婴儿"
elif (( $age >= 3 && $age <= 8 )); then
    echo "幼儿"
elif (( $age >= 9 && $age <= 17 )); then
    echo "少年"
elif (( $age >= 18 && $age <=25 )); then
    echo "成年"
elif (( $age >= 26 && $age <= 40 )); then
    echo "青年"
elif (( $age >= 41 && $age <= 60 )); then
    echo "中年"
else
    echo "老年"
fi

退出状态

退出状态为 0 表示“成功”,为真;也就是说,程序执行完成并且没有遇到任何问题。除 0 以外的其它任何退出状态都为“失败”,为假。
退出状态和逻辑运算符的组合

运算符 使用格式 说明
&& expression1 && expression2 逻辑与运算符,当 expression1expression2 同时成立时,整个表达式才成立。
|| expression1 || expression2 逻辑或运算符,expression1expression2 两个表达式中只要有一个成立,整个表达式就成立。
! !expression 逻辑非运算符,相当于“取反”的效果。如果expression成立,那么整个表达式就不成立;如果expression不成立,那么整个表达式就成立。

test

用来检测某个条件是否成立
test expression 或者 [ expression ],注意[]expression之间的空格,这两个空格是必须的,否则会导致语法错误

#!/bin/bash
read age
if test $age -le 2; then
    echo "婴儿"
elif test $age -ge 3 && test $age -le 8; then
    echo "幼儿"
elif [ $age -ge 9 ] && [ $age -le 17 ]; then
    echo "少年"
elif [ $age -ge 18 ] && [ $age -le 25 ]; then
    echo "成年"
elif test $age -ge 26 && test $age -le 40; then
    echo "青年"
elif test $age -ge 41 && [ $age -le 60 ]; then
    echo "中年"
else
    echo "老年"
fi

-le选项表示小于等于,-ge选项表示大于等于,&&是逻辑与运算符。
重点是学习test的各种选项,具体参考

test命令中使用变量时,我强烈建议将变量用双引号""包围起来,这样能避免变量为空值时导致的很多奇葩问题

[[]]

[[ expression ]]
[[ ]]判断 expression成立时,退出状态为 0,否则为非 0 值。注意[[ ]]expression之间的空格,这两个空格是必须的,否则会导致语法错误

#!/bin/bash
read str1
read str2
if [[ -z $str1 ]] || [[ -z $str2 ]]  #不需要对变量名加双引号
then
    echo "字符串不能为空"
elif [[ $str1 < $str2 ]]  #不需要也不能对 < 进行转义
then
    echo "str1 < str2"
else
    echo "str1 >= str2"
fi  

#可以使用逻辑运算符将多个 test 命令连接起来
[ -z "$str1" ] || [ -z "$str2" ] 
[ -z "$str1" -o -z "$str2" ]
[[ -z $str1 || -z $str2 ]]  
test 或 [] [[ ]]
[ -z "$str1" ] || [ -z "$str2" ] [[ -z $str1 ]] ||[[ -z $str2 ]] √
[ -z "$str1" -o -z "$str2" ] [[ -z $str1 -o -z$str2 ]] ×
[[ -z $str1 || -z $str2 ]] × [[ -z $str1 || -z $str2 ]]

[[ ]] 支持正则表达式
[[ str =~ regex ]]

#!/bin/bash
read str1
read str2
if [[ -z $str1 ]] || [[ -z $str2 ]]  #不需要对变量名加双引号
then
    echo "字符串不能为空"
elif [[ $str1 < $str2 ]]  #不需要也不能对 < 进行转义
then
    echo "str1 < str2"
else
    echo "str1 >= str2"
fi

case in

case expression in
    pattern1)
        statement1
        ;;
    pattern2)
        statement2
        ;;
    pattern3)
        statement3
        ;;
    ……
    *)
        statementn
esac

例子:

#!/bin/bash
printf "Input integer number: "
read num
case $num in
    1)
        echo "Monday"
        ;;
    2)
        echo "Tuesday"
        ;;
    3)
        echo "Wednesday"
        ;;
    4)
        echo "Thursday"
        ;;
    5)
        echo "Friday"
        ;;
    6)
        echo "Saturday"
        ;;
    7)
        echo "Sunday"
        ;;
    *)
        echo "error"
esac	
  • case、int 和 esac 都是 Shell 关键字,expression 表示表达式,pattern 表示匹配模式。
  • expression 既可以是一个变量、一个数字、一个字符串,还可以是一个数学计算表达式,或者是命令的执行结果,只要能够得到 expression 的值就可以。
  • pattern 可以是一个数字、一个字符串,甚至是一个简单的正则表达式。
  • case 会将expression的值与pattern1、pattern2、pattern3逐个进行匹配:
  • 如果expression和某个模式(比如pattern2)匹配成功,就会执行这模式(比如pattern2)后面对应的所有语句(该语句可以有一条,也可以有多条),直到遇见双分号;;才停止;然后整个 case语句就执行完了,程序会跳出整个case语句,执行esac后面的其它语句。
  • 如果expression没有匹配到任何一个模式,那么就执行*)后面的语句(*表示其它所有值),直到遇见双分号;;或者esac才结束。*)相当于多个if分支语句中最后的 else 部分。
  • Shell case in语句中的*)用来“托底”,万一 expression没有匹配到任何一个模式,*)部分可以做一些“善后”工作,或者给用户一些提示。
  • 可以没有*)部分。如果 expression没有匹配到任何一个模式,那么就不执行任何操作。
  • 除最后一个分支外(这个分支可以是普通分支,也可以是*)分支),其它的每个分支都必须以;;结尾,;;代表一个分支的结束,不写的话会有语法错误。最后一个分支可以写;;,也可以不写,因为无论如何,执行到esac都会结束整个case in语句

case in 和正则表达式

格式 说明
* 表示任意字符串。
[abc] 表示 a、b、c 三个字符中的任意一个。比如,[15ZH] 表示 1、5、Z、H 四个字符中的任意一个。
[m-n] 表示从 m 到 n 的任意一个字符。比如,[0-9] 表示任意一个数字,[0-9a-zA-Z] 表示字母或数字。
| 表示多重选择,类似逻辑运算中的或运算。比如,abc

最后一个分支*)并不是什么语法规定,它只是一个正则表达式,*表示任意字符串,所以不管 expression 的值是什么,*)总能匹配成功。

#!/bin/bash
printf "Input a character: "
read -n 1 char
case $char in
    [a-zA-Z])
        printf "\nletter\n"
        ;;
    [0-9])
        printf "\nDigit\n"
        ;;
    [0-9])
        printf "\nDigit\n"
        ;;
    [,.?!])
        printf "\nPunctuation\n"
        ;;
    *)
        printf "\nerror\n"
esac
while

while 循环的用法如下:

while condition
do
    statements
done

在 while 循环体中必须有相应的语句使得 condition 越来越趋近于“不成立”,只有这样才能最终退出循环,否则 while 就成了死循环,会一直执行下去,永无休止。

#!/bin/bash
i=1
sum=0
while ((i <= 100))
do
    ((sum += i))
    ((i++))
done
echo "The sum is: $sum"  

until

until 循环的用法如下:

until condition
do
    statements
done

until 循环的执行流程为:

  1. 先对 condition 进行判断,如果该条件不成立,就进入循环,执行 until 循环体中的语句(do 和 done 之间的语句),这样就完成了一次循环。

  2. 每一次执行到 done 的时候都会重新判断 condition 是否成立,如果不成立,就进入下一次循环,继续执行循环体中的语句,如果成立,就结束整个 until 循环,执行 done 后面的其它 Shell 代码。

  3. 如果一开始 condition 就成立,那么程序就不会进入循环体,do 和 done 之间的语句就没有执行的机会。

     #!/bin/bash
     i=1
     sum=0
     until ((i > 100))
     do
         ((sum += i))
         ((i++))
     done
     echo "The sum is: $sum"
    

for

Shell for循环有两种使用形式

  • C语言风格for 循环

      for((exp1; exp2; exp3))
      do
          statements
      done
    

for 循环的执行过程

	#!/bin/bash
	sum=0
	for ((i=1; i<=100; i++))
	do
	    ((sum += i))
	done
	echo "The sum is: $sum"

for 循环中的 exp1(初始化语句)、exp2(判断条件)和 exp3(自增或自减)都是可选项,都可以省略(但分号;必须保留)

在循环体内部使用break关键字强制结束循环:

  • Python 风格for in循环

      for variable in value_list
      do
          statements
      done
    

variable表示变量,value_list 表示取值列表,inShell 中的关键字。

#!/bin/bash
sum=0
for n in 1 2 3 4 5 6
do
    echo $n
     ((sum+=n))
done
echo "The sum is "$sum
  • value_list 的说明:
    取值列表 value_list 的形式有多种,你可以直接给出具体的值,也可以给出一个范围,还可以使用命令产生的结果,甚至使用通配符
  • 直接给出具体的值

可以在 in 关键字后面直接给出具体的值

#!/bin/bash
for str in "C语言中文网" "http://c.biancheng.net/" "成立7年了" "日IP数万"
do
    echo $str
done
  • 给出一个取值范围

可以在 in 关键字后面给出范围:{start..end},start 表示起始值,end表示终止值;注意中间用两个点号相连,而不是三个点号,这种形式只支持数字和字母

#计算从 1 加到 100 的和:
#!/bin/bash
sum=0
for n in {1..100}
do
    ((sum+=n))
done
echo $sum

#输出从 A 到 z 之间的所有字符:
#!/bin/bash
for c in {A..z}
do
    printf "%c" $c
done
  • 使用命令的执行结果

使用反引号’'或者$()都可以取得命令的执行结果

#!/bin/bash
sum=0
for n in $(seq 2 2 100)
do
    ((sum+=n))
done
echo $sum


#列出当前目录下的所有 Shell 脚本文件:
#!/bin/bash
for filename in $(ls *.sh)
do
    echo $filename
done
  • 使用 Shell 通配符

Shell 通配符可以认为是一种精简化的正则表达式,通常用来匹配目录或者文件,而不是文本

#!/bin/bash
for filename in *.sh
do
    echo $filename
done
  • 使用特殊变量

Shell 中有多个特殊的变量,例如 KaTeX parse error: Expected 'EOF', got '#' at position 1: #̲、*、 @ 、 @、 @?、$$ 等

#!/bin/bash
function func(){
    for str in $@
    do
        echo $str
    done
}

也可以省略 value_list,省略后的效果和使用$@一样

select in循环

select in循环用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。

Shell select in循环的用法如下:

select variable in value_list
do
    statements
done

variable 表示变量,value_list 表示取值列表,in 是 Shell 中的关键字

#!/bin/bash

echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
    echo $name
done
echo "You have selected $name"

运行结果:
What is your favourite OS?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#? 4↙
You have selected UNIX
#? 1↙
You have selected Linux
#? 9↙
You have selected
#? 2↙
You have selected Windows
#?^D

#?用来提示用户输入菜单编号;^D表示按下 Ctrl+D 组合键,它的作用是结束 select in 循环

运行到 select 语句后,取值列表 value_list 中的内容会以菜单的形式显示出来,用户输入菜单编号,就表示选中了某个值,这个值就会赋给变量 variable,然后再执行循环体中的 statementsdodone之间的部分)。

select in通常和 case in 一起使用,在用户输入不同的编号时可以做出不同的反应

#!/bin/bash
echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
    case $name in
        "Linux")
            echo "Linux是一个类UNIX操作系统,它开源免费,运行在各种服务器设备和嵌入式设备。"
            break
            ;;
        "Windows")
            echo "Windows是微软开发的个人电脑操作系统,它是闭源收费的。"
            break
            ;;
        "Mac OS")
            echo "Mac OS是苹果公司基于UNIX开发的一款图形界面操作系统,只能运行与苹果提供的硬件之上。"
            break
            ;;
        "UNIX")
            echo "UNIX是操作系统的开山鼻祖,现在已经逐渐退出历史舞台,只应用在特殊场合。"
            break
            ;;
        "Android")
            echo "Android是由Google开发的手机操作系统,目前已经占据了70%的市场份额。"
            break
            ;;
        *)
            echo "输入错误,请重新输入"
    esac
done

break continue

  • 在C语言、C++、C#、Python、Java 等大部分编程语言中,break 和 continue 只能跳出当前层次的循环,内层循环中的 break 和 continue 对外层循环不起作用;
  • 但是 Shell 中的 breakcontinue 却能够跳出多层循环,也就是说,内层循环中的 breakcontinue 能够跳出外层循环

Shell break 关键字的用法为:
break n n 表示跳出循环的层数,如果省略 n,则表示跳出当前的整个循环

#可以在 break 后面跟一个数字,让它一次性地跳出两层循环
#!/bin/bash
i=0
while ((++i)); do  #外层循环
    j=0;
    while ((++j)); do  #内层循环
        if((i>4)); then
            break 2  #跳出内外两层循环
        fi
        if((j>4)); then
            break  #跳出内层循环
        fi
        printf "%-4d" $((i*j))
    done
    printf "\n"
done

Shell continue 关键字的用法为:
continue n
n 表示循环的层数:

  • 如果省略 n,则表示 continue 只对当前层次的循环语句有效,遇到 continue 会跳过本次循环,忽略本次循环的剩余代码,直接进入下一次循环。
  • 如果带上 n,比如 n 的值为 2,那么 continue 对内层和外层循环语句都有效,不但内层会跳过本次循环,外层也会跳过本次循环,其效果相当于内层循环和外层循环同时执行了不带 n 的 continue。

使用 continue 跳出多层循环,请看下面的代码:

#!/bin/bash
for((i=1; i<=5; i++)); do
    for((j=1; j<=5; j++)); do
        if((i*j==12)); then
            continue 2
        fi
        printf "%d*%d=%-4d" $i $j $((i*j))
    done
    printf "\n"
done

运行结果:

1*1=1   1*2=2   1*3=3   1*4=4   1*5=5  
2*1=2   2*2=4   2*3=6   2*4=8   2*5=10 
3*1=3   3*2=6   3*3=9   4*1=4   4*2=8   5*1=5   	5*2=10  5*3=15  5*4=20  5*5=25
从运行结果可以看出,遇到continue 2时,不但跳过了内层 for 循环,也跳过了外层 for 循环  

Shell函数

函数定义

Shell 函数定义的语法格式

function name() {
    statements
    [return value]
}

对各个部分的说明:

  • function是 Shell 中的关键字,专门用来定义函数;
  • name是函数名;
  • statements是函数要执行的代码,也就是一组语句;
  • return value表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写。

由{ }包围的部分称为函数体,调用一个函数,实际上就是执行函数体中的代码。其中,function() 均可省略任一个

  • 调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:name
  • 如果传递参数,那么多个参数之间以空格分隔:name param1 param2 param3
  • 和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。
#!/bin/bash
function getsum(){
    local sum=0
    for n in $@
    do
         ((sum+=n))
    done
    return $sum
}
getsum 10 20 55 15  #调用函数并传递参数
echo $?

函数参数

  1. Shell 中的函数在定义时不能指明参数,但是在调用时却可以传递参数。

  2. 函数参数是 Shell 位置参数的一种,在函数内部可以使用$n来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。

  3. $#可以获取传递的参数的个数;

  4. $@或者$*可以一次性获取所有的参数

     使用 $n 来接收函数参数。
     #!/bin/bash
     #定义函数
     function show(){
         echo "Tutorial: $1"
         echo "URL: $2"
         echo "Author: "$3
         echo "Total $# parameters"
     }
     #调用函数
     show C# http://c.biancheng.net/csharp/ Tom
    

使用 $@ 来遍历函数参数。

#!/bin/bash
function getsum(){
    local sum=0
    for n in $@
    do
         ((sum+=n))
    done
    echo $sum
    return 0
}
#调用函数并传递参数,最后将结果赋值给一个变量
total=$(getsum 10 20 55 15)
echo $total
#也可以将变量省略
echo $(getsum 10 20 55 15)

函数返回值

Shell 函数的返回值只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。

得到函数的处理结果:

  1. 一种是借助全局变量,将得到的结果赋值给全局变量;

  2. 一种是在函数内部使用 echoprintf 命令将结果输出,在函数外部使用$()或者``捕获结果。

     # 在函数内部使用 echo 输出结果
     #!/bin/bash
     function getsum(){
         local sum=0  #局部变量
         for((i=$1; i<=$2; i++)); do
             ((sum+=i))
         done
        
         echo $sum
         return $?
     }
     read m
     read n
     total=$(getsum $m $n)
     echo "The sum is $total"
     #也可以省略 total 变量,直接写成下面的形式
     #echo "The sum is "$(getsum $m $n)
    

$()捕获了第一个 echo 的输出结果,它并没有真正输出到终端上。除了$(),你也可以使用``来捕获 echo 的输出结果

Shell重定向(输入输出重定向)

Linux Shell 重定向分为两种,一种输入重定向,一种是输出重定向;从字面上理解,输入输出重定向就是「改变输入与输出的方向」

表1:与输入输出有关的文件描述符

文件描述符 文件名 类型 硬件
0 stdin 标准输入文件 键盘
1 stdout 标准输出文件 显示器
2 stderr 标准错误输出文件 显示器

Linux Shell 输出重定向
Shell_第2张图片
Shell 输入重定向
输入重定向

符号 说明
command 将 file 文件中的内容作为 command 的输入。
command < 从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止(分界符可以是任意的字符串,用户自己定义)。
command file2 将 file1 作为 command 的输入,并将 command 的处理结果输出到 file2。

Linuxwc 命令可以用来对文本进行统计,包括单词个数、行数、字节数,它的用法如下:
wc [选项] [文件名]
其中,-c选项统计字节数,-w选项统计单词数,-l选项统计行数。

逐行读取文件内容。
#!/bin/bash

while read str; do
    echo $str
done 

模块化(source命令)

source 是 Shell 内置命令的一种,会读取 filename 文件中的代码,并依次执行所有语句.
source 命令会强制执行脚本文件中的全部命令,而忽略脚本文件的权限。

source命令的用法为:source filename 也可以简写为:. filename

func.sh 文件内容:
#计算所有参数的和
function sum(){
    local total=0
    for n in $@
    do
         ((total+=n))
    done
    echo $total
    return 0
}

main.sh 文件内容:
#!/bin/bash
source func.sh
echo $(sum 10 20 55 15)

source filenamesh filename./filename执行脚本的区别在那里呢?

  1. 当shell脚本具有可执行权限时,用sh filename./filename执行脚本是没有区别得。./filename是因为当前目录没有在PATH中,所有"."是用来表示当前目录的。
  2. sh filename 重新建立一个子shell,在子shell中执行脚本里面的语句,该子shell继承父shell的环境变量,但子shell新建的、改变的变量不会被带回父shell,除非使用export。
  3. source filename:这个命令其实只是简单地读取脚本里面的语句依次在当前shell里面执行,没有建立新的子shell。那么脚本里面所有新建、改变变量的语句都会保存在当前shell里面。

避免重复引入

子Shell和子进程

Shell 脚本是从上至下、从左至右依次执行每一行的命令及语句的,即执行完一个命令之后再执行下一个。如果在 Shell 脚本中遇到子脚本(即脚本嵌套),就会先执行子脚本的内容,完成后再返回父脚本继续执行父脚本内后续的命令及语句。
Shell_第3张图片
如果以新进程的形式运行 Shell 脚本,会向系统内核请求启动一个新的进程,以便在该进程中执行脚本的命令及子 Shell 脚本,基本流程如下图所示:

Shell_第4张图片

shell组命令

Shell 组命令的写法有两种:
{ command1; command2; command3; . . . }
(command1; command2; command3;. . . )

两种写法的区别在于:

  1. 由花括号{}包围起来的组命名在当前 Shell 进程中执行,而由小括号()包围起来的组命令会创建一个子 Shell,所有命令都在子 Shell 中执行。

  2. 对于第一种写法,花括号和命令之间必须有一个空格,并且最后一个命令必须用一个分号或者一个换行符结束。

  3. 组命令可以将多条命令的输出结果合并在一起,在使用重定向和管道时会特别方便。

    下面的代码将多个命令的输出重定向到 out.txt:
    ls -l > out.txt #>表示覆盖
    echo “http://c.biancheng.net/shell/” >> out.txt #>>表示追加
    cat readme.txt >> out.txt

    借助组命令,三条命令合并在一起,简化成一次重定向:
    { ls -l; echo “http://c.biancheng.net/shell/”; cat readme.txt; } > out.txt

    (ls -l; echo “http://c.biancheng.net/shell/”; cat readme.txt) > out.txt

    也可以将组命令和管道结合起来:
    { ls -l; echo “http://c.biancheng.net/shell/”; cat readme.txt; } | lpr

Shell进程替换

  1. 命令替换是把一个命令的输出结果赋值给另一个变量,例如dir_files=ls -l或date_time=$(date);
  2. 进程替换则是把一个命令的输出结果传递给另一个(组)命令。

Shell 进程替换有两种写法:

  1. 一种用来产生标准输出,借助输入重定向,它的输出结果可以作为另一个命令的输入:
    <(commands)

  2. 另一种用来接受标准输入,借助输出重定向,它可以接收另一个命令的输出结果:
    >(commands)
    commands是一组命令列表,多个命令之间以分号;分隔。注意,<>与圆括号之间是没有空格的。

     使用进程替换:
     read < <(echo "http://c.biancheng.net/shell/")
     echo $REPLY   
    

整体上来看,Shell 把echo "http://c.biancheng.net/shell/"的输出结果作为 read 的输入。<()用来捕获 echo命令的输出结果,<用来将该结果重定向到 read
注意,两个<之间是有空格的,第一个<表示输入重定向,第二个<()连在一起表示进程替换。

一个进程替换用作「接受标准输入」的例子:
echo "C语言中文网" > >(read; echo "你好,$REPLY")
运行结果:
你好,C语言中文网

进程替换本质:

#!/bin/bash
read newProcess< <(echo $$)
echo "newPID=$newProcess"
echo "thisPID=$$"
运行结果:
newPID=2681
thisPID=2681

两个进程的 PID 一样,进程替换并没有创建新的进程

实际上,进程替换会跟系统中的文件关联起来,这个文件的名字为/dev/fd/n(n 是一个整数)。该文件会作为参数传递给()中的命令,()中的命令对该文件是读取还是写入取决于进程替换格式是<还是>

  1. 如果是>(),那么该文件会给()中的命令提供输入;借助输出重定向,要输入的内容可以从其它命令而来。
  2. 如果是<(),那么该文件会接收()中命令的输出结果;借助输入重定向,可以将该文件的内容作为其它命令的输入。

使用 echo 命令可以查看进程替换对应的文件名:

[c.biancheng.net]$ echo >(true)
/dev/fd/63
[c.biancheng.net]$ echo <(true)
/dev/fd/63
[c.biancheng.net]$ echo >(true) <(true)
/dev/fd/63 /dev/fd/62

/dev/fd/目录下有很多序号文件,进程替换一般用的是 63 号文件,该文件是系统内部文件,我们一般查看不到。

我们通过下面的语句进行实例分析:
echo "shellscript" > >(read; echo "hello, $REPLY")
第一个>表示输出重定向,它把第一个 echo 命令的输出结果重定向到/dev/fd/63文件中。

/dev/fd/63文件起到了数据中转或者数据桥梁的作用,借助重定向,它将>()内部的命令和外部的命令联系起来,使得数据能够在这些命令之间流通。

文件描述符

  1. 一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。
  2. 内核空间是虚拟地址空间的一部分,进程启动后要占用内存,其中一部分内存分配给了文件描述符表。
  3. 除了文件描述符表,系统还需要维护另外两张表:
  • 打开文件表(Open file table)
  • i-node 表(i-node table):i节点不仅包含了文件数据存储区的地址,还包含了很多信息,比如数据大小,等等文件信息。但是i节点是不保存文件名的。文件名是保存在一个目录项中。每一个目录项中都包含了文件名和i节点。

Shell_第5张图片

  • 通过文件描述符,可以找到文件指针,从而进入打开文件表。该表存储了以下信息:
  1. 文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
  2. 状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
  3. i-node 表指针。
  • 还得通过打开文件表的 i-node 指针进入 i-node表,该表包含了诸如以下的信息:
  1. 文件类型,例如常规文件、套接字或 FIFO。
  2. 文件大小。
  3. 时间戳,比如创建时间、更新时间。
  4. 文件锁。

结合Linux文件描述符理解重定向:
Linux 系统每次读写文件的时候,都从文件描述符下手,通过文件描述符找到文件指针,然后进入打开文件表和 i-node 表,这两个表里面才真正保存了与打开文件相关的各种信息。

文件指针只不过是一个内存地址,修改它是轻而易举的事情。文件指针是文件描述符和真实文件之间最关键的“纽带”,然而这条纽带却非常脆弱,很容易被修改。

输入输出重定向就是通过修改文件指针实现的!
发生重定向时,Linux 会用文件描述符表(一个结构体数组)中的一个元素给另一个元素赋值,或者用一个结构体变量给数组元素赋值,整体上的资源开销相当低。

发生重定向的时候,文件描述符并没有改变,改变的是文件描述符对应的文件指针。对于标准输出,Linux 系统始终向文件描述符 1 中输出内容,而不管它的文件指针指向哪里;只要我们修改了文件指针,就能向任意文件中输出内容。

>是输出重定向符号,<是输入重定向符号;更准确地说,它们应该叫做文件描述符操作符。>< 通过修改文件描述符改变了文件指针的指向,所以能够实现重定向的功能。

Shell_第6张图片

Shell exec命令操作文件描述符

使用 exec 命令可以永久性地重定向,后续命令的输入输出方向也被确定了,直到再次遇到 exec 命令才会改变重定向的方向;换句话说,一次重定向,永久有效

exec可以让重定向对当前 Shell 进程中的所有命令有效:exec 文件描述符操作

echo "重定向未发生"
重定向未发生

exec >log.txt
echo "c.biancheng.net"
echo "C语言中文网"
exec >&2
echo "重定向已恢复"
重定向已恢复

cat log.txt
c.biancheng.net
C语言中文网 
  • exec >log.txt将当前 Shell 进程的所有标准输出重定向到 log.txt文件,它等价于exec 1>log.txt
  • 后面的两个 echo 命令都没有在显示器上输出,而是输出到了 log.txt文件。
  • exec >&2用来恢复重定向,让标准输出重新回到显示器,它等价于exec 1>&22是标准错误输出的文件描述符,它也是输出到显示器,并且没有遭到破坏,我们用 2 来覆盖 1,就能修复 1,让 1 重新指向显示器。
  • 接下来的 echo 命令将结果输出到显示器上,证明exec >&2奏效了。
  • 最后我们用 cat命令来查看 log.txt 文件的内容,发现就是中间两个 echo 命令的输出。

重定向的恢复

以输出重定向为例,手动恢复的方法有两种:

  • /dev/tty文件代表的就是显示器,将标准输出重定向到 /dev/tty即可,也就是 exec >/dev/tty
  • 如果还有别的文件描述符指向了显示器,那么也可以别的文件描述符来恢复标号为1的文件描述符,例如 exec >&2。注意,如果文件描述符 2 也被重定向了,那么这种方式就无效了

Shell代码块重定向
代码块,就是由多条语句组成的一个整体;for、while、until 循环,或者 if…else、case…in 选择结构,或者由{ }包围的命令都可以称为代码块

将重定向命令放在代码块的结尾处,就可以对代码块中的所有命令实施重定向。

使用 while 循环不断读取 nums.txt 中的数字,计算它们的总和。
#!/bin/bash
sum=0
while read n; do
    ((sum += n))
done log.txt  #同时使用输入输出重定向
echo "sum=$sum"  

	对{}包围的代码使用重定向。
	#!/bin/bash
	{
	    echo "C语言中文网";
	    echo "http://c.biancheng.net";
	    echo "7"
	} >log.txt  #输出重定向
	{
	    read name;
	    read url;
	    read age
	} 

Shell Here Document(内嵌文档/嵌入文档

  • Here Document 的基本用法为:

    command < document
    END

command是 Shell 命令,<是开始标志,END是结束标志,document是输入的文档(也就是一行一行的字符串)。

在脚本文件中使用 Here Document,并将 document 中的内容转换为大写。
#!/bin/bash
#在脚本文件中使用立即文档
tr a-z A-Z <

注意,终止符END必须独占一行,并且要定顶格写。

分界符(终止符)<< 对应的END可以是任意的字符串,由用户自己定义,比如 END、MARKER 等。分界符可以出现在正常的数据流中,只要它不是顶格写的独立的一行,就不会被作为结束标志

cat < Shell教程
> http://c.biancheng.net/shell/
> 已经进行了三次改版
> END
Shell教程
http://c.biancheng.net/shell/
已经进行了三次改版 
  • 忽略命令替换
    默认情况下,正文中出现的变量和命令也会被求值或运行,Shell 会先将它们替换以后再交给 command

      name=C语言中文网
      url=http://c.biancheng.net
      age=7
      cat < ${name}已经${age}岁了,它的网址是 ${url}
      > END
      
      C语言中文网已经7岁了,它的网址是 http://c.biancheng.net
    

可以将分界符用单引号或者双引号包围起来使 Shell 替换失效:

	name=C语言中文网
	url=http://c.biancheng.net
	age=7
	cat <<'END'  #使用单引号包围
	> ${name}已经${age}岁了,它的网址是 ${url}
	> END
	
	${name}已经${age}岁了,它的网址是 ${url}
  • 忽略制表符

默认情况下,行首的制表符也被当做正文的一部分。

#!/bin/bash
cat <

这里的制表符仅仅是为了格式对齐,并不希望它作为正文的一部分,为了达到这个目的,可以在<<和END之间增加-

	#!/bin/bash
	#增加了减号-
	cat <<-END
	    Shell教程
	    http://c.biancheng.net/shell/
	    已经进行了三次改版
	END

	这次的运行结果为:
	Shell教程
	http://c.biancheng.net/shell/
	已经进行了三次改版

Shell Here String(内嵌字符串)
Here String 是 Here Document 的一个变种,它的用法如下:
command <<< string

单双引号区别:
var=two
tr a-z A-Z <<<"one $var there"
ONE TWO THERE

tr a-z A-Z <<<'one $var there'
ONE $VAR THERE

tr a-z A-Z <<

有了引号的包围,Here String 还可以接收多行字符串作为命令的输入:

	tr a-z A-Z <<<"one two there
	> four five six
	> seven eight"
	
	ONE TWO THERE
	FOUR FIVE SIX
	SEVEN EIGHT

Linux Shell管道

管道(pipe):将两个或者多个命令(程序或者进程)连接到一起,把一个命令的输出作为下一个命令的输入

Linux 管道使用竖线|连接多个命令,这被称为管道符。Linux 管道的具体语法格式如下:

command1 | command2
command1 | command2 [ | commandN... ]

当在两个命令之间设置管道时,管道符|左边命令的输出就变成了右边命令的输入。只要第一个命令向标准输出写入,而第二个命令是从标准输入读取,那么这两个命令就可以形成一个管道。大部分的 Linux 命令都可以用来形成管道。

command1 必须有正确输出,而 command2 必须可以处理 command2 的输出结果;而且 command2只能处理 command1的正确输出结果,不能处理 command1的错误信息。

mysqldump -u root -p '123456' wiki | gzip -9 | ssh username@remote_ip "cat > /backup/wikidb.gz"
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PDdRy6Is-1597903870364)(./res/shell-pipe.png)]
这些使用了管道的命令有如下特点:

  1. 命令的语法紧凑并且使用简单。
  2. 通过使用管道,将三个命令串联到一起就完成了远程 mysql 备份的复杂任务。
  3. 从管道输出的标准错误会混合到一起。

管道与输入重定向
输入重定向操作符<可以在管道中使用,以用来从文件中获取输入:

command1 < input.txt | command2
command1 < input.txt | command2 -option | command3

tr a-z A-Z 

管道与输出重定向
也可以使用重定向操作符>>>将管道中的最后一个命令的标准输出进行重定向:
command1 | command2 | ... | commandN > output.txt
command1 < input.txt | command2 | ... | commandN > output.txt

tr a-z A-Z os.txt.new

你可能感兴趣的:(#,Linux)