本文主要基于《实用Linux Shell编程》总结,并加入一些网上查询资料和博主自己的推断。 其中命令相关的,已抽取出来在另一篇系统性学习】Linux Shell常用命令中,可以一起使用。
文章目录
一、基础知识 二、命令与环境 三、变量和数组 四、条件流程控制 五、循环 六、函数 七、通配符、正则表达和文本处理 八、进程与作业 九、其他话题 十、Bash调试
一、基础知识
Linux 系统主要目录及简单描述
目录 描述 /bin bin 是 Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令 /boot 内核及其他系统启动时需要的文件,包括一些连接文件以及镜像文件 /dev dev 是 Device(设备) 的缩写, 存放的Linux 的外部设备,Linux把所有外设都看做是一个文件,对文件的操作就是对外部设备的操作 /etc etc 是 Etcetera(等等) 的缩写,用来存放所有的系统管理所需要的配置文件,该目录及子目录下有很多.conf文件 /home 用户的主目录,系统默认的普通用户主目录为/home/<acount_name>
,如上图中的 alice、bob 和 eve,保存用户自己的配置文件、文档、数据等 /lib lib 是 Library(库) 的缩写,存放着系统最基本的动态连接共享库文件(由/bin和/sbin使用),其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。/user/lib中包含更多用于用户程序的库文件 /lost+found 这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件,用于系统修复和恢复 /mnt 系文件系统挂载点,一般用于安装移动介质,其他文件系统的分区、网络共享文件系统或者任何可安装的文件系统。比如可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。 /opt opt 是 optional(可选) 的缩写,主要由第三方开发者用于安装和卸载他们的软件包。比如你安装一个ORACLE数据库或者spark就可以放到这个目录下。默认是空的。 /proc proc 是 Processes(进程) 的缩写,是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息(如 /proc/version)。这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器:echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
/root 该目录为系统管理员,也称作超级权限者的默认主目录。 /sbin s就是 Super User 的意思,是 Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是超级用户管理系统时使用的系统管理程序。普通用户几乎没有权限执行这里面的命令。/user/sbin是超级用户使用的比较高级的管理程序和系统守护程序 /tmp tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。当系统重启时,改目录的文件会被自动清空 /usr usr 是 unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的 program files 目录。 /var var 是 variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。
ls -l 详情解释:
-rw-------. 1 root root 1 .3K Dec 14 01:38 anaconda-ks.cfg
dr-xr-xr-x. 17 root root 224 Dec 14 01:38 bin
第一列共10位,第1位表示文档类型,d表示目录,-表示文件,l表示链接文件,d表示可随机存取的设备,如U盘等,c表示一次性读取设备,如鼠标、键盘等。后9位,依次对应三种身份所拥有的权限,身份顺序为:owner、group、others,权限顺序为:readable、writable、excutable 第二列,对于文件表示硬链接数,表示有多少个文件链接到inode号码。对于目录表示子目录数(包括隐藏文件),所有目录都包含".“和”…"两个隐藏目录,所以目录的第二列>=2
。 第三列表示拥有者,user 第四列表示所属群组,group 第五列表示文档容量大小 第六列表示文档最后修改时间,注意不是文档的创建时间 第七列表示文档名称。以点(.)开头的是隐藏文档
账户权限说明:对于文件,读:可以查看 写:可以修改 执行:可以运行。对于目录,读:可以浏览,用ls 写:可以创建和删除文件 执行:可以进入该目录。 SUID:有的可执行二进制文件 的账户权限为rws
,s是SUID(即set uid),表示其他账户在执行时,有文件所有者的权限。如passwd命令会改/etc/shadow文件,这个文件只有root用户可以修改。但其他账户同样可以执行passwd修改该文件。原因就是passwd文件是s
权限。chmod u+s file
或chmod 4755 file
添加该权限。rwS
显示S是不正常的,给文件添加x权限才会正常。SUID仅对可执行二进制文件 起作用。 SGID:有的可执行二进制文件 的所属组权限为rws
,,s是SGID(即set gid)表示其他账户在执行时,具有文件所属组的权限。对目录设置SGID,则作用为:任何账户如果可以在该目录内建立新文件或新的子目录,那么新文件和子目录的所属组与该目录的所属组保持一致。SGID属性要保证目录或文件对所属组由x权限,否则为大写S。chmod g+s file
或chmod 2755 file
添加该权限 SBIT:粘滞位,t是SBIT只有目录可以设置,作用是:在一个大家都有权限的目录下,账户不能删除别人的文件或目录。chmod o+t file
或chmod 1755 file
添加该权限。SBIT要求其他账户有执行权限,否则为大写T。
二、命令与环境
linux命令分为内置命令和外部命令:help查看所有内置命令和关键字列表, help command
查看内置命令详情。外置命令:which command
查看外置命令位置,which无法对内置命令生效。command --help
显示外置命令详情。 内置变量PS1,主命令提示参数。重新登陆shell则恢复。永久保持写入~/.bashrc
中。
\ h 计算机名
\ t 当前时间
\ u 账户名
\ w 当前路径
> PS1 = "\h@\u@\t $"
host-10-31-17-80@user_client@16:17:03$
搜索路径PATH,路径用:
隔开,按顺序先后查找,找到就停止。在原路径后面加路径PATH='$PATH:/tmp/bin'
。 要永久改变环境变量,将修改写进~/.bashrc文件中。 权限掩码umask source和点命令:souce file
和. file
等价,如果file不在当前目录下,会在$PATH
下找。 直接执行脚本文件和source都能执行脚本中的命令。差异:a. 脚本在子shell中运行,而source是在父shell b. 直接运行脚本需要脚本有x权限,source不需要 命令解释顺序及改变。命令类型查看用type command
alias-> keywords-> function-> built-in-> $PATH
别名-> 关键字-> 函数-> 内置命令-> 外部命令
command <命令> 禁用别名和函数,先处理内置命令和外部命令 builtin <命令> 只查找内置命令 enable 禁用(-n)和使能内置命令(尽量少用,要避免自己的编写的命令和内置命令重名)
命令或程序结束后会返回一个退出状态,取值0-255,0表示成功执行。内置变量$?
存了上一条命令的退出状态 。 内置命令true用于返回成功的状态,false则是失败。 管道左侧|右侧
,理解成左侧命令的输出,会给到右侧命令的输入,所以右侧命令不需要写代表输入的参数。 执行一个shell命令会自动打开标准输入文件(stdin)和标准输出文件(stdout),他们默认对应终端键盘和终端屏幕,分别对应文件描述符0和1。文件描述符(file descriptor,fd)是进程对其打开文件的索引,形式上是个非负整数。
> ls -l /dev/std*
lrwxrwxrwx 1 root root 15 Aug 19 2015 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Aug 19 2015 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Aug 19 2015 /dev/stdout -> /proc/self/fd/1
> ls -l /proc/self/fd/*
lrwx------ 1 user_client users 64 Oct 22 17 :00 /proc/self/fd/0 -> /dev/pts/0
lrwx------ 1 user_client users 64 Oct 22 17 :00 /proc/self/fd/1 -> /dev/pts/0
lrwx------ 1 user_client users 64 Oct 22 17 :00 /proc/self/fd/2 -> /dev/pts/0
Linux下的tty和pts详解 概述Linux TTY/PTS的区别 简单的说,/dev/pts/0 可以当做一个终端,这个终端连接了输入和输出,所以fd/0(标准输入)、fd/1(标准输出)、fd/2(标准错误)都可以连到这个终端上来。
Input +--------------------------+ R/W +------+
----------->| | < ---------->| bash |
| pts/1 | +------+
< -----------| | < ---------->| lsof |
Output | Foreground process group | R/W +------+
+--------------------------+
输入重定向: 命令 输入
->命令 < 文件名(不能是带输出的命令)
输出重定向: 命令(能产生输出)
->命令(能产生输出) >文件名
,命令(能产生输出) >>文件名
追加模式。> 文件名
可以创建空文件或者清空文件。输出重定向是不安全的。如果想不覆盖源文件,可开启noclobber选项,set -o noclobber
。 标准错误输出(stderr),对应fd/2,普通重定向>
不起作用。需要 2>
实现重定向。 同时处理,1>file1 2>file2
标准到file1,错误到file2,>file 2>&1
同时输出到file(等价 &> file
和>& file
)。 “黑洞”,/dev/null一般用作垃圾箱,把不要的输出重定向到这里,如2> /dev/null
一行多命令,用;
隔开。 后台执行:command &
,执行后会输出 [工作号] 进程号
。 续行:命令太长可用\
来分行,PS2控制续行提示符。 特殊文件名处理:空格前面加\
,-a.txt
这样-开头的,用vi -- -a.txt
或vi ./-a.txt
。因为-后面会被认为是选项。Bash规定--(空格)
后面的东西不是选项,而是文件名或参数。 小括号,大括号,中括号
三、变量和数组
Bash只有字符串类型。整数型字符串赋值被变量时,变量相当于多了个整数属性,可以进行整数运算。 变量定义时,等号左右两边不能有空格。引用变量时,前面加$
。 双引号作用,保留空格。a=
和b=" "
,a是为空的,b才能保留空格。
单引号:相当于raw字符串:被单引号括起的内容不管是常量还是变量者不会发生替换。 双引号:把双引号内的内容输出出来;如果内容中有命令、变量等,会先把变量、命令解析出结果,发生替换,然后在输出最终内容来。 不加引号:不会将含有空格的字符串视为一个整体输出, 如果内容中有命令、变量等,会先把变量、命令解析出结果,然后在输出最终内容来,如果字符串中带有空格等特殊字符,则不能完整的输出,需要改加双引号,一般连续的字符串,数字,路径等可以用。
取命令执行结果:`命令`,$(命令)
都可以取命令输出的结果。要保留换行符、首尾空格等符号,则用"$(命令)"
。可以把()
当做子shell,里面的操作不会对父shell产生影响。少用反引号,因为$()
便于嵌套。注意对比取变量${变量}
和取命令输出结果$(命令)
。
> x = 10
> b = $( echo $x)
> echo b = $b x = $10
b = 10 x = 0
> b = $( x = 5 ; echo $x)
> echo b = $b x = $x
b = 5 x = 10 --父shell中的x没被改变
> time = $( echo Today is $( date) )
> echo $time
Today is Sun Oct 23 14 :06:37 CST 2022
> aa = ${date}
> echo aa
--空,因为变量date没有定义
> date = 3
> t1 = $( date )
> t2 = $date
> t3 = ${date}
> echo -e "t1=$t1 \n t2=$t2 \n t3=$t3 "
t1 = Sun Oct 23 14 :11:21 CST 2022
t2 = 3
t3 = 3
> echo "` date ` "
Sun Oct 23 14 :19:05 CST 2022
> echo "\` date\ ` "
` date `
> echo "$( date ) "
Sun Oct 23 14 :19:22 CST 2022
> echo "\$( date ) "
$( date )
整型计算:放在双括号间或let后面。$((表达式))
可取出表达式计算结果。单独的((表达式))
仅仅是计算,而没有输出。i++
(先用后加),++i
先加后用,支持+=,-=,*=,/=,%=,^=
等运算。支持算木运算((条件?结果1:结果2))
。$(())
可替换成$[]
,注意,有$时才能替换。另外,declare -i var
定义了整型变量后,var=表达式
不需要括号也可进行整型计算。
w = date
> echo $w
date
> $w
Sun Oct 23 16 :03:32 CST 2022
> echo $(( 50 / 20 ))
2
> echo $(( w= 50 / 20 ))
2
> echo $w
2
> echo $[ a= 10 +5]
15
> echo $a
15
> declare -i v
> v= $a +$w
> echo $v
30
> v= a+10
> echo $v
25
浮点运算:用bc或者awk。echo "scale=5;表达式|bc"
或者直接bc
开客户端运算,quit退出。 其他进制:bash支持其他进制运算,只要在算数表达式能生效的地方都可以使用。两种书写方法,a. 基数#数值
,基数可取2到64。b. 0开头表示8进制,0x开头表示十六进制:
(( w= 8 #16 ))
(( w= 016 ))
(( w= 16 #25 ))
(( w= 0x25 ))
数组:bash只支持一维数组,下标默认从0开始。单独引用数组名时,返回数组第一个元素。打印数组全部详细信息,用 declare -p 数组名
。
y = ( aa bb cc)
y = ( [ 1 ] = aa [ 3 ] = bb [ 5 ] = cc)
> echo $y
aa
> declare -p y
declare -a y = '([0]="aa" [1]="bb" [2]="cc")'
echo $y
echo ${y[ 1] }
echo ${y[ *] }
echo ${y[ @] }
echo ${# y[ *] }
echo ${! y[ *] }
y[ 3 ] = dd
y[ 2 ] = cc2
unset y[ 1 ]
unset y
关联数组(其他语言里的map):Bash4以上版本才支持。使用和数组类似,定义如下:
declare -A age = ( [ Mike] = 12 [ Jack] = 24 [ Tom] = 30 )
declare -p age
echo ${! age[ @] }
echo ${age[ @] }
Bash中的特殊变量。P118
变量 含义 $0
脚本自身的名字,或者shell的名字 $N
脚本或函数的位置参数,$1,$2,...${10}
,注意大于9要用{}
包起来 $#
位置参数个数,不包括$0
$*
所有位置参数(整体作为一个字符串),不包括$0
$@
所有位置参数(每个作为独立字符串),不包括$0
$?
上一条命令退出状态 $$
当前shell进程ID $!
最后一个命令的进程ID $-
当前shell的选项,若echo $-
的输出中包含i,则表示是交互式shell,包含C则表示noclobber开启 $_
上条命令的最后一个参数
set --
清除所有位置参数,不包括$0
。父shell与子shell及其进程ID。 一些常用内置变量(或者说环境变量)P121
变量 含义 BASH bash的完整路径,默认为/bin/bash BASH_ENV 非交互式分登陆模式中(比如执行shell脚本时),先执行一遍该变量下指定的脚本 CDPATH 命令cd的搜索路径,多个路径用:
隔开,用于减少输入全路径的情况 DISTACK 当前目录栈存放的数组,配合poshd,popd,dirs使用 FUNCNAME 当某函数被调用时,该变量为函数名;实际上它是数组,记录调用链上所有的函数名 GLOBIGNORE 要忽略的通配模式列表,冒号分割,定义了文件名扩展时(通配符模式下)要忽略的文件名集合,本身也支持通配符,例如GLOBIGNORE=a*;b*
会使得ls *
忽略所有a和b开头的文件 HISTFILE 存放命令历史的文件,通常为~/.bash_history HISTFILESIZE 命令历史文件保存的最大行数 HISTIGNORE 不需保存的历史命令序列,多个通配符模式列表组成,由冒号分隔。用冒号隔开。如HISTIGNORE=ls:t*:\&
,将忽略ls命令,和t开头的命令,并且&
表示连续输入的相同命令只被记录一次,赋值时需要用\
屏蔽其将命令后台挂起含义 HOME 用户主目录,通常为/home/用户名
,不要改,会影响cd
及cd ~
的结果 LINENO 脚本中当前行号 OLDPWD 前一个工作目录,cd -
等价于cd $OLDPWD
OPTARG 存放getopts参数的值 OPTIND 待处理的下一个getopts参数索引,初始值为1 PATH 外部命令搜索路径,多个以冒号隔开 PPID 父进程(父shell)的进程ID PWD 当前工作目录 RANDOM 0-32767之间一个随机数 REPLY read不加变量时,变量缓存。select 用户选择项缓存 SECONDS 当前shell的启动时间 SHELL 登陆linux后的默认shell,/bin/bash比较常用 SHELLOPTS shell的选项,冒号隔开,例:braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor
SHLVL 第一次打开一个shell终端,值为1,没进入一层子shell,值增加1 TMOUT 如果该值大于0,当前shell在等待TMOUT秒后没有任何输入就会自动退出 BASH_SOURCE 更稳健地获取自身脚本路径。BASH_SOURCE是数组,不过它的第一个元素是当前脚本的名称。这在source的时候非常有用,因为在被source的脚本中,$0
是父脚本的名称,而不是被source的脚本名称。而BASH_SOURCE就可以派上用场了。还可和FUNAME配合打印哪个脚本调用了哪个函数。 BASH_SUBSHELL 检查当前环境是不是在subshell中,这个值在非subshell中是0;每进入一层subshell就加1
常用变量赋值表达式。下面的Default可以是变量,取$变量
。var也可以是0(不能赋值)或者位置变量1,2,3…10(不能赋值)。
表达式 含义 ${var:-Default}
若var未定义或为空值,则表达式的值为Default,var不变 ${var:=Default}
若var未定义或为空值,则表达式的值为Default,var赋值Default ${var:?Message}
若var未定义或为空值,则打印Message且后续代码不再执行,var不变。 ${!pre*}
匹配之前所有声明的以pre开头的变量 ${!pre@}
匹配之前所有声明的以pre开头的变量
常用内置字符串操作(支持通配符)。sed和awk也有字符串处理函数,但内置操作最快。下表中的str可替换成数组名[*]
,则表达式对于数组每个元素做处理。expr命令也可处理字符串,支持正则表达式。
表达式 含义 ${#str}
字符串str的长度 ${str:pos}
从str的pos位置提取子串,到结尾 ${str:pos:len}
从str的pos位置提取长度为len的子串 ${str#glob}
从str的开头,删除最短匹配的通配符模式glob ${str##glob}
从str的开头,删除最长匹配的通配符模式glob ${str%glob}
从str的结尾,删除最短匹配的通配符模式glob ${str%%glob}
从str的结尾,删除最长匹配的通配符模式glob ${str/glob/replacement}
用replacement代替第一个glob,最长匹配 ${str//glob/replacement}
用replacement代替所有glob,最长匹配。 ${str/#glob/replacement}
用replacement代替第一个包含开头的glob,最长匹配 ${str/%glob/replacement}
用replacement代替第一个包含开头的glob,最长匹配
> a = ( one on1 tow)
> echo ${a[ *] # o*}
ne n1 tow
> echo ${a[ *] ## o*}
tow
> b = ${a[ *] ## o.*}
> declare -p b
declare -- b = "one on1 tow"
> echo ${a[ @] ## o*}
tow
> c = ${a[ @] ## o*}
> declare -p c
declare -- c = "tow"
四、条件流程控制
条件判断命令:test 条件表达式
或[ 条件表达式 ]
,中括号前后一定要有空格。$?
取命令退出结果来看条件是否满足。满足则退出结果为0,否则为1。 整型关系运算:
条件表达式 含义 [ int1 -lt int2 ]
int1小于int2 [ int1 -le int2 ]
int1小于等于int2 [ int1 -gt int2 ]
int1大于int2 [ int1 -ge int2 ]
int1大于等于int2 [ int1 -eq int2 ]
int1等于int2 [ int1 -ne int2]
int1不等于int2
字符串关系运算,注意需要加\
转义,且str最好用""
保护首尾的空格。
条件表达式 含义 [ str1 == str2 ]
str1等于str2,也可以用str1=str2
[ str1 != str2 ]
str1不等于str2 [ str1 > str2 ]
字典序,str1大于str2,str1在str2后面 [ str1 < str2 ]
字典序,str1小于str2,str1在str2前面 [ -z str ]
str为空串zero,长度为0 [ -n str ]
str非空not-zero,长度大于0 [ str ]
同上 [[ str =~ regex ]]
str匹配正则表达式regex,注意=~
只能在中括号中使用,相当于启用正则匹配
文件属性判断,
条件表达式 含义 [ -a file ]
file存在 [ -e file ]
file存在,同a [ -s file ]
file存在且非空(字节数大于0) [ -d file ]
file存在且是一个目录 [ -f file]
file存在且是一个普通文件 [ -h file]
file存在且是一个符号链接 [ -L file]
file存在且是一个符号链接,同h [ -p file]
file存在且为已命名管道 [ -u file ]
file存在且设置了SUID [ -g file ]
file存在且设置了SGID [ -k file ]
file存在且设置了粘滞位 [ -r file ]
file存在且当前用户有读权限 [ -w file ]
file存在且当前用户有写权限 [ -x file ]
file存在且当前用户有执行权限,如果是目录,则有进入该目录权限 [ -N file ]
file存在且最后一次读取后有更改 [ -O file ]
file存在且当前用户是该文件所有者 [ -N file ]
file存在且当前用户所属组是该文件所属组 [ -t FD]
文件描述符打开,并指向一个终端 [ f1 -nt f2 ]
f1修改时间比f2新newer than,或f1存在且f2不存在 [ f1 -ot f2 ]
f1修改时间比f2旧older than,或f2存在且f1不存在 [ f1 -ef f2 ]
f1和f2有相同的设备和节点号(互为硬链接)
逻辑与:[ 条件表达式1 -a 条件表达式2 ]
,逻辑或:[ 条件表达式1 -o 条件表达式2 ]
,逻辑非:[ ! 条件表达式 ]
。test同样。优先级:非、与、或,多条件可用()
,括号最优先。 双中括号代替test或[]
.[[]]
使得P146:
a. (,<
等不用加反斜杠; b. 保留字符串首尾有空格时,也不需要加双引号 c. 判断字符串相等或者不等时,右侧支持通配模式。*
代表0或多个字符,?
代表一个字符 d. 支持&&
和 ||
表达 代替 -a
,-o
双小括号助力整数条件运算。(())
使用场景: I. 计算表达式,((表达式))
等价于 let 表达式
。表达式结果不是逻辑0或1的时候,该表达式不报错的退出状态永远是0。 II. 上述表达式可以是整数相关的条件表达式,使得 a. (,<
等不用加反斜杠; b. >,<,!=
可应用于整数,并且>=,<=,==
也可使用 c. 支持&&
和 ||
表达 代替 -a
,-o
命令的与或非: I. 与:命令1 && 命令2
,命令结果都为0,才为0。若命令1结果为非0,则命令2不再执行。可用来实现命令依赖。 II. 或:命令1 || 命令2
,命令结果有一个为0,则为0。若命令1结果为0,则命令2不再执行。可用来按优先级至少执行一个命令。 III. 非: ! 命令1
,命令结果有一个为0,则为0。若命令1结果为0,则命令2不再执行。可用来按优先级至少执行一个命令。
> [ -r a.txt -a -w b.txt ]
> echo $?
0
> x = 5 ; y = 10 ; z = 20
> [ \ ( $x -eq 5 -o $y -gt 5 \ ) -a $z -lt 5 ]
> echo $?
0
> [ [ ( $x -eq 5 || $y -gt 5 ) && $z -lt 5 ] ]
> echo $?
0
> (( ( $x == 5 || $y > 5 ) && $z < 5 ))
> echo $?
0
> [ [ $s1 == a* && $s2 != b? ] ]
> [ 1 -eq 2 ] && [ 1 -lt 3 ]
> echo $?
1
判断变量是否有定义:[-v 变量名]
或 test -v 变量名
。结果为0则表示有定义。或者set,从结果中找,存在则表示有定义,但这种方式太繁琐。 10.if控制结构,可嵌套使用。
if 命令
then
命令1
命令2
.. .
if
if 命令; then 命令1; 命令2.. .; fi
if 命令
then
命令(命令组)
else
命令(命令组)
fi
if 命令; then 命令(命令组); else 命令(命令组); fi
if 命令1
then
命令(命令组)
elif 命令2
then
命令(命令组)
elif 命令3
then
命令(命令组)
else
命令(命令组)
fi
if cp a.txt b.txt 2 > /dev/null
then
echo “copy done”
fi
if cmd1; then cmd2; else cmd3; fi
cmd1 && cmd2 || cmd3
case 控制结构,如下模式可以是1个,或者多个,多个用|
分割,或关系。模式支持正则表达式。如[6-7][0-9]
表示60~79。
case $变量 in
模式1)
命令( 命令组) ; ;
模式2)
命令( 命令组) ; ;
.. .
*)
命令( 命令组) ; ;
esac
case $score in
100 )
echo "Full Mark" ; ;
9 [ 0 -9] )
echo "Excellent" ; ;
8 [ 0 -5] | 8 [ 6 -9] )
echo "Very Good" ; ;
[ 6 -7] [ 0 -9] )
echo "Passing" ; ; \
*)
echo "Failed,or Error" ; ;
esac
exit的退出状态默认为前一条命令的退出状态。也可以exit N
带整型参数,则退出状态为N,这个特点可以配合if(产生exti不同结果)和case(对不同结果做处理)命令使用。 here文档和case可模拟select命令的功能。详见常用命令here。
cat << INPUT
choose your role
A)student
B) teacher
INPUT
read var
case $var in
A)
.. .; ;
B)
.. .; ;
*)
echo "error" ; ;
esac
五、循环
for控制流程,如下“”列表“”可以是字符串字面量,字符串变量,或位置参数枚举$*
,$@
,或数组枚举${数组名[*]
,${数组名[@]
I. 为字符串时,元素分割符由IFS指定(Internal Filed Seperator,字段分割符),默认为“空白字符”,linux中的"空白字符"包括:空格、\t
、换行\n
、回车\r
(\n
和\r
是不同的:\r
是指 在同一行中, 使光标回到该行的行首。\n
是指 光标转到下一行)。IFS使用后要记得恢复。 II. $*
,$@
的表现受双引号限制,有引号时"$*"
仍是一个整体,"$@"
则各元素分别作为一个整体;没有引号时,$*
,$@
表现一样,每个元素都分别作为一个整体。 III. ${数组名[*]
,${数组名[@]
同上。 IV. [in 列表]部分不是必须的,没有的时候相当于 for 变量 in "#@"
for 变量 [ in 列表]
do
命令(命令组)
done
for 变量 [ in 列表] ; do 命令(命令组); done
> OLD_IFS = "$IFS "
> IFS = "$OLD_IFS "
> for a in 1 2 3 ; do echo "${a} T" ; done
1T
2T
3T --不带引号的字符串,触发天然分割,但该分割不是由IFS 指定。
> for a in "1 2 3" ; do echo "${a} T" ; done
1 2 3T --不触发天然分割和IFS 分割
> str2 = "1 2 3"
> for a in $str2 ; do echo "${a} T" ; done
1T
2T
3T --变量引用触发IFS 分割,当前分割符为默认的空白字符
> IFS = ":"
> for a in "1:2:3" ; do echo "${a} T" ; done
1 :2:3T
> str = "1:2:3"
> for a in $str ; do echo "${a} T" ; done
1T
2T
3T --变量引用触发IFS 分割,当前分割符为:
> for a in 1 2 3 ; do echo "${a} T" ; done
1T
2T
3T --触发天然分割未受影响
> echo $str
1 2 3 --此时,echo得到的str结果可以看出,IFS 分割已触发,分割后的元素用空格连接打印
> for a in $str2 ; do echo "${a} T" ; done
1 2 3T --分割符已变,所以即使触发IFS 分割,也分割不了
> a = $str
> declare -p str
declare -- str = "1:2:3"
> declare -p a
declare -- a = "1:2:3"
> echo "1:2"
1 :2
所以,可以猜测,IFS 只作用于引用变量的时候,即变量引用触发IFS 分割,并将变量以IFS 字符分割。
in后面只看有多少被分割元素,不触发分割。
> cat test.sh
for a in "$@ " ; do echo $a ; done
> test.sh A B C
A
B
C
> cat test.sh
for a in "$* " ; do echo $a ; done
> test.sh A B C
A B C
> 若不加双引号, 即 for a in $* 或 $@ ,结果都如下
> test.sh A B C
A
B
C --可理解成,$* 也触发天然分割,被切开了
for i in { 1 .. 10 }
do
.. .
done
算数for循环,三个表达式不是必须的,但;
需要保留。循环条件为空时,表示无限循环。
for (( 变量初始化; 循环条件; 变量值更新))
do
命令(命令组)
done
for (( i= 0 ; i< 10 ; i++ )) ; do echo $i ; done
i = 0
for (( ; i< 10 ; )) ; do echo $i ; (( i++ )) ; done
while循环。先判断while后面命令的退出状态,为0时,do后面的命令再执行。
while 命令
do
命令(命令组)
done
until循环。先判断while后面命令的退出状态,为0时,do后面的命令再停止。
until 命令
do
命令(命令组)
done
break和continue可以跳出循环,包括for、while、until。break N
和continue N
跳出N层循环。技巧if 命令;then break/continue;fi
可写成命令 && break/continue
。 循环的重定向以及和管道的配合。循环结构for、while、until都是以done结尾的,done后面可以接<
,>
做输入输出重定向,或者接| 命令
做管道。因为while和until后面是命令,所以命令就有可能需要有输入,那么也是可以在前面加管道的,即 命令 | while 需要输入的命令
。
for i in c b a m p
do
echo $i
done | sort
while read line
do
echo $line
done < test.txt
cat test.txt| while read line
do
echo $line
done
cat test.txt| while read line
do
echo $line
done > out.txt
六、函数
函数定义有三种形式,可接受位置参数,调用时用法和脚本一样
function 函数名
{
命令(命令组)
}
函数名( )
{
命令(命令组)
}
function 函数名( )
{
命令(命令组)
}
function 函数名 { 命令(命令组); }
函数名 arg1 arg2 arg3
source 函数定义脚本
$( 函数名)
$( 函数名 arg1 agr2)
函数内变量,如果和全局变量同名,则为全局变量,否则需要加上 local
或者declare
声明成局部变量;如果是非同名变量,则为局部变量,加上declare -g
则可声明成全局变量。 当前函数名,存在内部变量FUNNAME中。FUNNAME是一个数组,第一个元素为当前函数名,第二个元素为上一层调用函数名,declare - p FUNNAME
或 echo ${FUNAME[*]}
查看调用链。 函数的导出与清除。declare -fx fun1
, 导出函数,等同 export -f fun1
。清除 unset -f 函数名
或unset 函数名
。declare -f fun1
查看函数定义。 函数返回命令return, 可带参数N。执行return后,函数中后续命令不再执行。return不带参数时,函数的返回状态为return上一条命令的状态。retrun N
时,,函数的返回状态为N。可当做默认函数结尾有个隐含的return。 递归函数,可以实现,但运行效率较低,且占用系统资源较多,不提倡写递归函数。
七、通配符、正则表达和文本处理
通配符模式,glob(也叫wildcard)。支持以下几种
通配符 含义 *
表示任意字符 ?
表示一个字符 []
范围替换[abc]
表示a或b或c,[0-9]
,[a-z]
前面的字符不能大于后面的字符。[A-Z0-9]
组合在一起也可以。 [!]
不在范围内,[!abc]
表示不为a或b或c,[!0-9]
表示不为0-9
扩展通配模式,可通过shopt -s extglob
打开扩展(-u 表示关闭),支持:
通配模式 含义 *(pattern)
匹配所给模式零次或多次,如*(m)
,表示0个或多个m ?(pattern)
匹配所给模式零次或一次 +(pattern)
匹配所给模式一次或多次 @(pattern)
匹配所给模式仅一次 !(pattern)
不匹配所给模式
通配符模式使用场景 I. shell命令行里,加单引号,双引号或反斜杠时,失去通配符能力。如ls "*"
找的就是名为*
的文件。 II. find -name file
,中file支持通配符,且本身就放在双引号或单引号中,如find -name "a*.txt"
,通配符能力不失去,加反斜杠才失去通配能力。 III. 条件判断双中括号[[]]
的等号或不等号右边支持通配符,右边如果带双引号或单引号或反斜杠,通配符失效。 IV. 第三章15节的常用内置字符串操作(支持通配符)。如果带双引号或单引号或反斜杠,通配符失效。
> [ [ "ads" == a* ] ]
> echo $?
0
> [ [ "ads" == "a*" ] ]
> > echo $?
1 --双引号,通配符失效
> str = "abcd"
> echo ${str/ a*/ K}
K
> echo ${str/ "a*"/ K}
abcd --双引号,通配符失效
GLOBIGNORE,被忽略的通配模式,用冒号分割。仅作用于命令行。
> touch a.txt b.txt c.txt d.txt
> find -name "*"
.
./c.txt
./a.txt
./d.txt
./b.txt
> ls *
a.txt b.txt c.txt d.txt
> GLOBIGNORE = a*:b*
> ls *
c.txt d.txt
> find -name "*"
.
./c.txt
./a.txt
./d.txt
./b.txt
正则表达式,各语言的正则表达式支持范围略有不同,具体可查阅【中文,英文】。linux中,平常的正则表达式简称BRE(Basic Regular Expressions),扩展的正则表达式叫ERE(Extended Regular Expressions)。Linux里的正则表达式都是贪婪的(当可匹配多个时,都匹配最长)。常用的正则模式有(其中,"扩展"在egrep命令中可使用):
字符 模式含义 .
匹配任何单个字符 *
匹配零次或多次 ?
匹配零次或一次(扩展) +
匹配一次或多次(扩展) \{N\}
匹配N次(扩展用{N}
) \{N,\}
匹配N次或更多次(扩展用{N,}
) \{N,M\}
匹配至少N次之多M次(扩展用{N,M}
) a|b|c
匹配a或b或c(扩展) ( )
分组符号,如r(able|dress)
匹配rable或rdress(扩展) [x-y]
范围,如[0-9]匹配数字 [ ]
匹配一组字符中的一个,如[abc]
匹配a或b或c ^
匹配行首 $
匹配行尾 [^]
匹配取反,[^0-9]
表示非数字,[^abc]
表示非a,b,c \b
单词定界符,包含词首和词尾定界符 \<
词首定界符 \>
词尾定界符 \w
等价 [A-Za-z0-9]
\W
\w
取反,等价 [^A-Za-z0-9]
POSIX字符类。指定匹配字符范围的另一种方法。grep 中和egrep中使用方法一样。tr命令后也可使用单中括号tr [:punct:] X
,但为了保持一致,建议都用双中括号。作用:有些字符类比正则表达简单,有些命令只支持POSIX不支持正则(如tr)。
字符类 含义 [[:alnum:]]
字符和数字,等同于[A-Za-z0-9]
[[:punct:]]
标点符号 [[:cntrl:]]
控制字符 [[:space:]]
空白字符,空格、制表符、竖向制表符、换行、回车等 [[:alpha:]]
字母,等同[A-Za-z]
[[:blank:]]
空格户制表符Tab [[:digit:]]
所有数字,等同[0-9]
[[:lower:]]
所有小写字母,等同[a-z]
[[:upper:]]
所有大写字母,等同[A-Z]
[[:xdigit:]]
所有 16 进位制的数字
grep [ [ :punct:] ] t.txt
八、进程与作业
挂起进程<Ctrl+Z>
,比如进入vi后,在命令模式下,按<Ctrl+Z>
挂起进程。jobs查看作业状态。
> vi test.txt
[ 1 ] + Stopped vi test.txt
> jobs
[ 1 ] + Stopped vi test.txt
九、其他话题
目录栈。通常,目录只保留了OLDPWD和PWD,无法再追踪更早之前的记录。pushd、popd,dirs提供了这方面的能力。使用pushd命令记录的目录栈保存在内置变量DIRSTACK中。
pushd cd到目录,并将目录放入目录栈
+/-N 目录栈的“环滑动”,+N从栈顶的第N个项目滑为栈顶,-N从栈底的第N个目录滑为栈顶
> pwd
/usr
> dirs -c
> pushd /usr/bin
/usr/bin /usr --永远把当前目录放在栈底
> pushd /var/lib
/var/lib /usr/bin /usr
> declare -p DIRSTACK
declare -a DIRSTACK = '([0]="/var/lib" [1]="/usr/bin" [2]="/usr")'
dirs
-p 每行显示目录栈内一个目录
-v p的基础上带编号,编号大的在栈底
+/-N 不改变目录栈的内容,+N表示从栈顶数第N个目录,-N表示从栈底数第N个目录
> dirs
/var/lib /usr/bin /usr
> dirs -v
0 /var/lib
1 /usr/bin
2 /usr
> dirs -p
/var/lib
/usr/bin
/usr
> dirs +1
/usr/bin
popd 移除栈顶目录,并cd到新栈顶目录
+/-N
> popd
/usr/bin /usr
> pwd
/usr/bin
> dirs -v
0 /usr/bin
1 /usr
> dirs -v
0 /opt
1 /usr/lib
2 /usr/bin --从栈顶第2位
3 /usr
> pushd +2
/usr/bin /usr /opt /usr/lib
> dirs -v
0 /usr/bin --原从栈顶第2位
1 /usr
2 /opt --原栈顶滑下来了
3 /usr/lib
波浪号扩展
命令 含义 cd ~
等价cd $HOME
cd ~username
到username的home ~+
等价PWD,cd ~+
~-
等价OLDPWD,cd ~-
~+N
或~N
等价dirs +N
~-N
等价dirs -N
交互式shell和非交互式shell、登录shell和非登录shell expand_aliases别名功能,交互式shell默认打开,非交互式shell默认关闭。 I. 不要在脚本内使用别名,不然要显示打开expand_aliases。 II. 别名不能export,要想在其他shell中使用,将别名放到脚本里,再source 脚本名。 III. 函数内定义的别名,在函数调用前不能使用。 IV. 综上,不要在脚本和函数内使用别名。 并行处理。常用名命令grep,wc,awk和sed等都是单线程的,只能使用一个cpu。服务器有多个cpu时,可以利用GNU的parallel命令,把负载分配到各个cpu上。 I. parrallel命令通常需要下载安装,官网。 II. 使用说明。
mpstat
> cat /proc/cpuinfo | grep processor | wc -l
2
十、Bash调试
正式运行脚本前,先set -n
或set -o noexce
,读命令,解释但不执行,用来检查脚本语法 set -u
或 set -o nounset
,有变量未定义时提示错误信息shopt -s shift_verbose
,shift移动数大于参数个数时,提示错误set -v
或 set -o verbose
,为调试打开verbose模式,脚本运行时显示读入的每行命令,并原样打印set -x
或 set -o xtrace
,为调试打开echo模式,脚本运行时显示变量替换后的每行命令和参数。只调试一段代码时,在代码前后添加:
set -xv
待调试代码段
set +xv
set -x
的提示符为PS4,默认为+
,可修改成PS4=+'$[LINENO]'
打印代码行号。bash -x 脚本
和 set -x
是一样的,相当于在脚本前增加了set -x
。便于脚本测试。利用trap捕获信号来调试 打印内容调试 利用BASH_SOURCE,BASH_SUBSHELL,FUNNAME等内置变量。 输出重定向获取log文件来分析 在代码中增加“调试块”,if语句开控制变量调试信息是否打印
if [ $DEBUG = YES] ; then
打印调试信息
fi
DIR_T = "$( cd "$( dirname "${BASH_SOURCE [ 0] } " ) " && pwd ) "
# 在tmp文件夹下创建脚本test.sh 和a.sh
> cat test.sh
echo BASH_SOURCE=${BASH_SOURCE [ 0] }
echo \$0 =$0
DIR_T=" $( cd "$( dirname "${BASH_SOURCE [ 0] } " ) " && pwd ) "
echo ${DIR_T}
> cat a.sh
echo " This is a.sh"
source test.sh
> ./test.sh
BASH_SOURCE = ./test.sh --相对路径
$0 = ./test.sh --相对路径
/opt/user/tmp --绝对路径
> bash test.sh
BASH_SOURCE = test.sh
$0 = test.sh
/opt/user/tmp
> .. /tmp/test.sh
BASH_SOURCE = .. /tmp/test.sh --相对路径
$0 = .. /tmp/test.sh --相对路径
/opt/user/tmp
> bash .. /tmp/test.sh
BASH_SOURCE = .. /tmp/test.sh
$0 = .. /tmp/test.sh
/opt/user/tmp
> ./a.sh
This is a.sh
BASH_SOURCE = test.sh
$0 = ./a.sh --父脚本的名字
/opt/user/tmp
> echo $BASH_SUBSHELL
0
> ( echo $BASH_SUBSHELL )
1
> ( ( ( ( ( echo $BASH_SUBSHELL ) ) ) ) )
5
> bash
> echo $SHLVL
1
> echo $BASH_SUBSHELL
0