shell 编程
author : 老姚
一、shell 简介
Shell是一个应用程序,它连接了用户和Linux内核,让用户能够更加高效、安全、低成本地使用Linux内核,这就是Shell的本质。Shell本身并不是内核的一部分,它只是站在内核的基础上编写的一个应用程序,它和QQ、迅雷、Firefox等其它软件没有什么区别。- 然而
Shell也有着它的特殊性,就是开机立马启动,并呈现在用户面前;用户通过Shell来使用Linux,不启动Shell的话,用户就没办法使用Linux。 Shell是如何连接用户和内核的?Shell程序本身的功能是很弱的,比如文件操作、输入输出、进程管理等都得依赖内核。我们运行一个命令,大部分情况下Shell都会去调用内核暴露出来的接口,这就是在使用内核,只是这个过程被Shell隐藏了起来,它自己在背后默默进行,我们看不到而已。- 接口其实就是一个一个的函数,使用内核就是调用这些函数。这就是使用内核的全部内容了吗?嗯,是的!除了函数,你没有别的途径使用内核。
- 比如,我们都知道在
Shell中输入cat log.txt命令就可以查看log.txt文件中的内容,然而,log.txt 放在磁盘的哪个位置?分成了几个数据块?在哪里开始?在哪里终止?如何操作探头读取它?这些底层细节Shell统统不知道的,它只能去调用内核提供的open()和read()函数,告诉内核我要读取log.txt文件,请帮助我,然后内核就乖乖地按照Shell的吩咐去读取文件了,并将读取到的文件内容交给Shell,最后再由Shell呈现给用户(其实呈现到显示器上还得依赖内核)。整个过程中Shell就是一个“中间商”,它在用户和内核之间“倒卖”数据,只是用户不知道罢了。
二、shell 基础语法
shell 变量
-
Shell支持以下三种定义变量的方式:# variable 是变量名,value 是赋给变量的值。 variable=value #如果不含任何空白符,可以不用引号 variable='value' #单引号里面是什么就输出什么 variable="value" #输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出- 注意,赋值号
=的周围不能有空格,这可能和你熟悉的大部分编程语言都不一样。 - Shell 变量的命名规范和大部分编程语言都一样:
- 变量名由数字、字母、下划线组成;
- 必须以字母或者下划线开头;
- 不能使用
Shell里的关键字(通过help命令可以查看保留关键字)。
- 注意,赋值号
-
eg
url=http://hzoi.com echo $url name='衡中oj' echo $name author="老姚" echo $author
使用变量
-
使用一个定义过的变量,只要在变量名前面加美元符号
$即可。 -
变量名外面的花括号
{}是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界。 -
推荐给所有变量加上花括号
{},这是个良好的编程习惯。 -
eg
author="老姚" skill="shell" echo "I am ${author},good at ${skill}Script!" -
只读变量
- 使用
readonly命令可以将变量定义为只读变量,只读变量的值不能被改变。
- 使用
-
删除变量
- 使用 unset 命令可以删除变量。
- 变量被删除后不能再次使用;unset 命令不能删除只读变量。
-
eg:
url="hzoi.com" readonly url #设置变量url为只读变量 unset url #错误,不能删除只读变量
变量的作用域
Shell脚本中定义的变量是global的,其作用域从被定义的地方开始,到shell结束或被显示删除的地方为止。Shell函数定义的变量默认是global的,其作用域从“函数被调用时执行变量定义的地方”开始,到shell结束或被显示删除处为止。- 函数定义的变量可以被显示定义成
local的,其作用域局限于函数内。但请注意,函数的参数是local的。 - 函数参数是
local变量
- 函数定义的变量可以被显示定义成
- 如果同名,Shell函数定义的
local变量会屏蔽脚本定义的global变量。
#!/bin/bash
zyy(){
x=200 #global变量,调用开始生效
local y=100 #局部变量,函数调用结束失效
echo ${y} #输出100
}
zyy #调用函数
echo ${x} ${y} #只会输出x的值
Shell 命令替换
-
Shell命令替换是指将命令的输出结果赋值给某个变量。 -
Shell中有两种方式可以完成命令替换,一种是反引号,一种是$(),使用方法如下:variable=`command` #第一种方式把命令用反引号(位于 Esc 键的下方)包围起来 variable=$(command)#第二种方式把命令用$()包围起来,区分更加明显,所以推荐使用这种方式。 # eg: dir=$(ls -a) -
eg
#!/bin/bash begin_time=`date +%s` #开始时间,使用``替换 sleep 20s #休眠20秒 finish_time=$(date +%s) #结束时间,使用$()替换 run_time=$((finish_time - begin_time)) #时间差 echo "begin time: $begin_time" echo "finish time: $finish_time" echo "run time: ${run_time}s" LS=`ls -l` echo ${LS} #不用双引号包围 echo "--------------------------" #输出分隔符 echo "$LS" #使用引号包围- 第 5 行代码中的
(( ))是Shell数学计算命令。 - 如果被替换的命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个。
- 原则上讲,上面提到的两种变量替换的形式是等价的,可以随意使用;
- 反引号毕竟看起来像单引号,有时候会对查看代码造成困扰,而使用
$()就相对清晰,能有效避免这种混乱。 $()支持嵌套,反引号不行。$()仅在Bash Shell中有效,而反引号可在多种Shell中使用。
- 第 5 行代码中的
字符串
基本概念
- 字符串就是一系列字符的组合。字符串是
Shell编程中最常用的数据类型之一(除了数字和字符串,也没有其他类型了)。 - 字符串可以由单引号
' '包围,也可以由双引号" "包围,也可以不用引号。- 由单引号
' '包围的字符串:- 任何字符都会原样输出,在其中使用变量是无效的。
- 字符串中不能出现单引号,即使对单引号进行转义也不行。
- 由双引号
" "包围的字符串:- 如果其中包含了某个变量,那么该变量会被解析(得到该变量的值),而不是原样输出。
- 字符串中可以出现双引号,只要它被转义了就行。
- 不被引号包围的字符串
- 不被引号包围的字符串中出现变量时也会被解析,这一点和双引号
" "包围的字符串一样。 - 字符串中不能出现空格,否则空格后边的字符串会作为其他变量或者命令解析。
- 不被引号包围的字符串中出现变量时也会被解析,这一点和双引号
- 由单引号
获取字符串长度
-
在 Shell 中获取字符串长度很简单,具体方法如下:
${#string_name} #eg: str="hzoi.com" echo ${#str} #输出8
shell 字符串拼接
-
在
Shell中你不需要使用任何运算符,将两个字符串并排放在一起就能实现拼接。#!/bin/bash name="Shell" url="hzoi.com" 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
shell 字符串截取
从指定位置开始截取
-
这种方式需要两个参数:除了指定起始位置,还需要截取长度,才能最终确定要截取的字符串。
-
既然需要指定起始位置,到底是从字符串左边开始计数,还是从字符串右边开始计数。答案是
Shell同时支持两种计数方式。-
从字符串左边开始计数
-
如果想从字符串的左边开始计数,那么截取字符串的具体格式如下:
${string: start :length} #string 是要截取的字符串 #start 是起始位置(从左边开始,从 0 开始计数) #length 是要截取的长度(省略的话表示直到字符串的末尾)。 url="hzoi.com" url1="${url:2:5}" #从下标2开始连续截取5个字符 url2="${url:2}" #从下标2
-
-
从字符串右边开始计数
-
如果想从字符串的右边开始计数,那么截取字符串的具体格式如下:
${string: 0-start :length} #eg url="hzoi.com" url1="${url:0-6:5}" #从1开始往左数6个位置开始连续截取5个字符 url2="${url:0-6}" #从1开始往左数6个位置开始到结束- 同第
1)种格式相比,第2)种格式仅仅多了0-,这是固定的写法,专门用来表示从字符串右边开始计数。 - 从左边开始计数时,起始数字是
0(这符合程序员思维);从右边开始计数时,起始数字是1(这符合常人思维)。计数方向不同,起始数字也不同。 - 不管从哪边开始计数,截取方向都是从左到右。
- 同第
-
-
2.3.4.2 从指定字符串开始截取
-
这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾。
-
Shell可以截取指定字符(子字符串)右边的所有字符,也可以截取左边的所有字符。-
使用
#号截取右边字符-
使用
#号可以截取指定字符(或者子字符串)右边的所有字符,具体格式如下:${string#*chars} # string 表示要截取的字符 # chars 是指定的字符(或者子字符串),*是通配符的一种,表示任意长度的字符串。 # *chars连起来使用的意思是:忽略左边的所有字符,直到遇见 chars(chars 不会被截取)。 # eg: url="hzoi.com" url1=${url#*.} #结果为:com url2=${url#*oi.} #结果同上 url3=${url#*o} #遇到第一个字符o就截止,结果:i.com url4=${url##*o} #遇到最后一个字符o就截止,结果:m
-
-
使用
%截取左边字符
-
使用
%号可以截取指定字符(或者子字符串)左边的所有字符,具体格式如下:${string%chars*} # 请注意*的位置,因为要截取 chars 左边的字符,而忽略 chars 右边的字符 # 同时%用法同上面# # eg: url="http://www.hbhz.net" url1=${url%.*} #结果为:http://www.hbhz url2=${url%%.*} #结果为:http://www
-
汇总
| 格式 | 说明 |
|---|---|
| $ | 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。 |
| $ | 从 string 字符串的左边第 start 个字符开始截取,直到最后。 |
| $ | 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。 |
| $ | 从 string 字符串的右边第 start 个字符开始截取,直到最后。 |
| $ | 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。 |
| $ | 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。 |
| $ | 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。 |
| $ | 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。 |
shell 数组
Shell并且没有限制数组的大小,理论上可以存放无限量的数据。Shell数组元素的下标也是从0开始计数。- 获取数组中的元素要使用下标
[ ],下标可以是一个整数,也可以是一个结果为整数的表达式; - 常用的
Bash Shell只支持一维数组,不支持多维数组。
shell 数组的定义
-
在
Shell中,用括号( )来表示数组,数组元素之间用空格来分隔。由此,定义数组的一般形式为:array_name=(ele1 ele2 ele3 ... elen) #注意,赋值号=两边不能有空格,必须紧挨着数组名和数组元素。 # eg: nums=(29 100 13 8 91 44) #初始化长度为6的数组 nums[6]=88 #Shell 数组的长度不是固定的,定义之后还可以增加元素。 arr=(20 56 "http://c.biancheng.net/shell/") # Shell是弱类型的,它并不要求所有数组元素的类型必须相同 ages=([3]=24 [5]=19 [10]=12) #无需逐个元素地给数组赋值,可以只给特定元素赋值
获取数组元素
-
获取数组元素的值,一般使用下面的格式:
${array_name[index]} # array_name 是数组名,index 是下标。 # eg: n=${nums[2]} # 表示获取 nums 数组的第二个元素,然后赋值给变量 n nums=(29 100 13 8 91 44) echo ${nums[@]} #输出所有数组元素 nums[10]=66 #给第10个元素赋值(此时会增加数组长度) echo ${nums[*]} #输出所有数组元素 echo ${nums[4]} #输出第4个元素
获取数组长度
-
所谓数组长度,就是数组元素的个数。
-
利用
@或*,可以将数组扩展成列表,然后使用#来获取数组元素的个数,格式如下:${#array_name[@]} ${#array_name[*]} # array_name 表示数组名。两种形式是等价的,选择其一即可。 # 如果某个元素是字符串,还可以通过指定下标的方式获得该元素的长度 ${#arr[2]} # 获取 arr 数组的第 2 个元素(假设它是字符串)的长度。 # eg: nums=(29 100 13) echo ${#nums[*]} # 输出nums数组的长度:3 #向数组中添加元素 nums[10]="http://c.biancheng.net/shell/" echo ${#nums[@]} # 输出nums数组的长度:4 echo ${#nums[10]} # 输出字符串nums[10]的长度:29 unset nums[1] #删除数组元素 echo ${#nums[*]} # 输出nums数组的长度:3
数组拼接
-
所谓
Shell数组拼接(数组合并),就是将两个数组连接成一个数组。 -
拼接数组的思路是:先利用
@或*,将数组扩展成列表,然后再合并到一起。具体格式如下:array_new=(${array1[@]} ${array2[@]}) array_new=(${array1[*]} ${array2[*]}) # 两种方式是等价的,选择其一即可。 # 其中,array1 和 array2 是需要拼接的数组,array_new 是拼接后形成的新数组。 # eg: array1=(23 56) array2=(99 "http://c.biancheng.net/shell/") array_new=(${array1[@]} ${array2[*]}) #拼接后重新从下标0编号 echo ${array_new[@]} #也可以写作 ${array_new[*]}
删除数组元素
-
在
Shell中,使用unset关键字来删除数组元素,具体格式如下:unset array_name[index] # 删除元素 unset array_name # 删除数组 # array_name 表示数组名,index 表示数组下标。 # eg: arr=(23 56 99 "http://c.biancheng.net/shell/") unset arr[1] echo ${arr[@]} unset arr echo ${arr[*]}
关联数组
Bash支持关联数组,它可以使用字符串作为数组索引,有时候采用字符串索引更容易理解。
定义关联数组
declare -A assArray # declare -A 定义格式
assArray=([lucy]=beijing [yoona]=shanghai) # 利用内嵌索引-值列表的方法赋值
echo ${assArray[lucy]} # 输出:beijing
assArray[lily]=shandong # 使用独立的索引-值进行赋值
echo ${assArray[lily]} # 输出:shandong
列出数组索引
- 每一个数组都有一个索引用于查找。使用 ${!数组名[@或者*]} 获取数组的索引列表
echo ${!assArray[*]} # 输出:lily yoona sunny lucy
echo ${!assArray[@]} # 输出:lily yoona sunny lucy
获取所有键值对
declare -A city # 声明city为关联数组
city=([capital]=beijing [economy]=shanghai [home]=hubei)
echo "${key} come from ${city[${key}]}"
done
shell 内置命令
- 所谓
Shell内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。 - 可以使用
type来确定一个命令是否是内建命令 $PATH变量包含的目录中几乎聚集了系统中绝大多数的可执行命令,它们都是外部命令。- 内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘
I/O,还需要fork出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前Shell进程的一个函数。
冒号(😃
-
空命令:空命令就是什么也不做,所以返回码永远都是0。虽说是空命令,但仍是命令,就具备一般命令应有的特征,可以象一般命令一样使用。
-
参数扩展:冒号引起的参数扩展,意思是将参数的值替换为新的值。一般有以下几种参数扩展用法:
${parameter:-word} 如果parameter没有设置或者为空,替换为word;否则替换为parameter的值 ${parameter:+word} 如果parameter没有设置或者为空,不进行任何替换;否则替换为word。 ${parameter:=word} 如果parameter没有设置或者为空,把word赋值给parameter。实际parameterd的值真的被替换了,这就是=号的意思。不能用这种方式指派位置参数或特殊参数的值。 ${parameter:?word} 如果parameter没有设置或者为空,把word输出到stderr,否则替换为parameter的值。-、+、? 实际parameter的值并不被修改,扩展只是临时显示成word的值。准确的讲,扩展实际替换的是参数的显示,而不是参数的定义。只有=,才是替换参数的定义。 ${parameter:offset} 扩展为parameter中从offset开始的子字符串。 ${parameter:offset:length} 扩展为parameter中从offset开始的长度不超过length的字符。word前的“-”可以理解为“没定义,则替换成word;- “+”可以理解为“有定义,则替换成
word。 word前的“?”可以理解为“参数到底定义了没,没定义,把word当错误消息打印出来。”
-
三元运算符
var=100 (($var>100 ? var++:var--))
alias:别名
alias用来给命令创建一个别名。若直接输入该命令且不带任何参数,则列出当前 Shell 进程中使用了哪些别名。
# 使用 alias 命令自定义别名的语法格式为:
alias new_name='command'
alias Myshutdown='shutdown -h now' #关机
# eg:
alias timestamp='date +%s' # 别名
begin=`timestamp` # 命令替换
sleep 20s
finish=$(timestamp) # 命令替换
difference=$((finish - begin)) # 计算
echo "run time: ${difference}s"
unalias命令删除别名- 使用
unalias内建命令可以删除当前Shell进程中的别名。unalias有两种使用方法:- 第一种用法是在命令后跟上某个命令的别名,用于删除指定的别名。
- 第二种用法是在命令后接
-a参数,删除当前Shell进程中所有的别名。
- 使用
- 在代码中使用
alias命令定义的别名只能在当前Shell进程中使用,在子进程和其它进程中都不能使用。当前Shell进程结束后,别名也随之消失。 - 要想让别名对所有的
Shell进程都有效,就得把别名写入Shell配置文件。
echo 命令
-
echo是一个Shell内建命令,用来在终端输出字符串,并在最后默认加上换行符。 -
如果不希望换行,可以加上
-n参数 -
默认情况下,
echo不会解析以反斜杠\开头的转义字符。 -
我们可以添加
-e参数来让echo命令解析转义字符。echo "读者,你好!" #直接输出字符串 echo $url #输出变量 echo "${name}的网址是:${url}" #双引号包围的字符串中可以解析变量 echo '${name}的网址是:${url}' #单引号包围的字符串中不能解析变量 echo -n "Thank you!" #输出后不换行 echo "hello \nworld" #不会转义\n echo -e "hello \nworld" # 转义\n echo -e "hello world\c" # 转义\c 不会换行

浙公网安备 33010602011771号