BNU-FZH

fengzhenhua@outlook.com

linux shell有交互式与非交互式两种工作模式。我们日常使用shell输入命令得到结果的方式是交互式的方式,而shell脚本使用的是非交互式方式。

shell提供了alias功能来简化我们的日常操作,使得我们可以为一个复杂的命令取一个简单的名字,从而提高我们的工作效率。在shellalias扩展功能是,因此我们可以键入自己定义的alias别名来执行对应的命令。

alias扩展功能,此时仍然可以定义alias别名,但是shell不会将alias别名扩展成对应的命令,而是将alias别名本身当作命令执行,如果shell内置命令和PATH中均没有与alias别名同名的命令,则shell会“抱怨”找不到指定的命令。

在编写脚本时为了提高脚本的通用性,一般使用linux内置的通用命令,例如ls,cat等. 由于脚本是, 所以在脚本中直接使用系统命令即可。

现在有人要问了,在非交互模式的脚本中如何启用alias扩展呢? 答案是可以使用shell的内置命令shopt来开启alias扩展选项。

shopt的使用
1
2
3
shopt -s opt_name                 Enable (set) opt_name.
shopt -u opt_name Disable (unset) opt_name.
shopt opt_name Show current status of opt_name.

alias扩展功能的选项名称是expand_aliases,我们可以在交互式模式下查看此选项是否开启:

1
2
3
sw@gentoo ~ $ shopt expand_aliases
expand_aliases on
sw@gentoo ~ $

可见在交互式模式下alias扩展功能的确是开启的,因此我们才能使用alias别名。我们编写一个脚本来验证一下非交互式模式下alias扩展的设置:

验证alias扩展
1
2
3
4
5
6
7
8
9
#!/bin/bash --login

alias echo_hello="echo Hello!"
shopt expand_aliases
echo_hello

shopt -s expand_aliases
shopt expand_aliases
echo_hello

执行结果为:

1
2
3
4
5
6
sw@gentoo ~ $ ./test.sh
expand_aliases off
./test.sh: line 5: echo_hello: command not found
expand_aliases on
Hello!
sw@gentoo ~ $

另外,alias别名只在当前shell有效,不能被子shell继承,也不能像环境变量一样export。可以把alias别名定义写在.bashrc文件中,这样如果启动交互式的子shell,则子shell会读取.bashrc,从而得到alias别名定义。但是执行shell脚本时,启动的子shell处于非交互式模式,是不会读取.bashrc的。

如果你一定要让执行shell脚本的子shell读取.bashrc的话,可以给shell脚本第一行的解释器加上参数:

1
#!/bin/bash --login

我们有三种方法可以使脚本变成交互式:

  • --login使得执行脚本的子shell成为一个login shelllogin shell会读取系统和用户的profilerc文件,因此用户自定义的.bashrc文件中的内容将在执行脚本的子shell中生效。
  • 让执行脚本的shell读取.bashrc,在脚本中主动source ~/.bashrc即可。
  • bash脚本首行加上-i参数就变成交互式了,即#!/bin/bash -i.

ZshZ-shell)是一款用于交互式使用的shell,也可以作为脚本解释器来使用。其包含了 bashkshtcsh 等其他shell中许多优秀功能,也拥有诸多自身特色。Zsh拥有许多功能强大的插件,其中zsh-autosuggestions可以根据历史记录自动补全命令,但是在使用过程中每次重启终端后zsh-autosuggestions总是清空历史记录,这导致了每次补全都会以当前的输入为基础, 这极大的影响了工作效率。经过研究,原来是我的zsh没有配置历史文件,这导致了输入过的命令没有被记录存储下来,所以就出现了清空历史记录的假象!解决方法是在.zshrc文件中加入历史文件配置,具体如下:

~/.zshrc
1
2
3
4
5
6
7
# History file for zsh
HISTFILE=~/.zsh_history
# How many commands to store in history
HISTSIZE=10000
SAVEHIST=10000
# Share history in every terminal session
setopt SHARE_HISTORY

配置好.zshrc后,再使用zsh时它就会自动记录历史命令了,所以zsh-autosuggestions就可以完美的工作了。

在编写shell脚本时往往需要判断变量是否为整数,然后根据其类型执行不同的操作, 借助expr命令可以方便的实现此功能。expr命令是一个手工命令行计数器,用于在UNIX/LINUX下求表达式变量的值,一般用于整数值,也可用于字符串。

语法

语法
1
expr 表达式

表达说明

  • 用空格隔开每个项;
  • 用反斜杠 \ 放在 shell 特定的字符前面;
  • 对包含空格和其他特殊字符的字符串要用引号括起来

使用举例

计算字符串长度

计算字符串长度
1
2
> expr length “this is a test
14

抓取字符串

抓取字符串
1
2
> expr substr “this is a test” 3 5
is is

抓取第一个字符数字串出现的位置

抓取第一个字符数字串出现的位置
1
2
> expr index "sarasara"  a
2

整数运算

整数运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 > expr 14 % 9
5
> expr 10 + 10
20
> expr 1000 + 900
1900
> expr 30 / 3 / 2
5
> expr 30 \* 3 (使用乘号时,必须用反斜线屏蔽其特定含义。因为shell可能会误解显示星号的意义)
90
> expr 30 * 3
expr: Syntax error
> expr a + 1 (当使用一个字符与整数1相加时会返回提示“expr: 参数不是整数”)
expr: 参数不是整数
> echo $? (检测返回码,得到1, 这是后面进行整数判断的依据。)
1

判断整数的实现

整数判断
1
2
3
4
5
6
7
8
9
10
11
12
expr "$TLS_SNum" + 1 &> /dev/null
if [ $? -eq 0 ]; then
if [ $TLS_SNum -lt $1 -o $TLS_SNum -gt $2 ]; then
echo "编号超出范围,请重新选择编号!"
exit
else
NEO_OUT_H=$TLS_SNum
fi
else
echo "输入非数字,请重新输入编号!"
exit
fi

上述代码源于diary.sh脚本,其使用expr计算变量TLS_SNum1之和,如果变量TLS_SNum是数字,则变量$?=0, 否则$?=1, 只有当变量是整数时才能比较它与其他数字的大小关系。这里需要特别注意,网络上一些教程计算TLS_SNum0的和,但是当TLS_SNum=0时,返回的$?=1, 这会导致误判!!所以,我修改其为$TLS_SNum +1.

1. 单括号[ ] 和 test

左括号[test本质是一样的,是shell的内部命令,所以><等会被解释为重定向符号,而不是比较符号:

1
2
3
4
bash-3.2$ type [ [[ test
[ is a shell builtin
[[ is a shell keyword
test is a shell builtin

右括号]表示判断结束,可以通过man [查看支持的判断语句:

1
$ man [

1.1 文件相关判断

  • -d file: True if file exists and is a directory.
  • -f file: True if file exists and is a regular file.
  • -h file: True if file exists and is a symbolic link.
  • -S file: True if file exists and is a socket.
  • file1 -nt file2: True if file1 exists and is newer than file2.

1.2 字符串相关判断

  • string: True if string is not the null string.
  • s1 = s2: True if the strings s1 and s2 are identical.
  • s1 < s2: True if string s1 comes before s2 based on the binary value of their characters.

1.3 整型判断

  • n1 -eq n2: True if the integers n1 and n2 are algebraically equal.
  • n1 -ne n2: True if the integers n1 and n2 are not algebraically equal.
  • n1 -gt n2: True if the integer n1 is algebraically greater than the integer n2.
  • n1 -ge n2: True if the integer n1 is algebraically greater than or equal to the integer n2.
  • n1 -lt n2: True if the integer n1 is algebraically less than the integer n2.
  • n1 -le n2: True if the integer n1 is algebraically less than or equal to the integer n2.

1.4 多个判断连接

  • ! expression: True if expression is false.
  • expression1 -a expression2: True if both expression1 and expression2 are true.
  • expression1 -o expression2: True if either expression1 or expression2 are true.

2. 双括号[[ ]]

双括号是shell的关键字,会返回一个状态码,所以也可以作为判断条件使用。(更加通用)

  • [[支持字符串的模式匹配,=~支持正则匹配
  • [[返回状态码,所以可以与shell中的 &&||一起使用:
1
2
$ [[ 1 < 2 ]] && echo "1 < 2" || echo "1 >= 2"
1 < 2

脚本命令

shell求交、并和差集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
file_list_1=("test1" "test2" "test3" "test4" "test5" "test6")
file_list_2=("test5" "test6" "test7" "test8")

# 获取并集,A ∪ B
file_list_union=(`echo ${file_list_1[*]} ${file_list_2[*]}|sed 's/ /\n/g'|sort|uniq`)
echo ${file_list_union[*]}

# 获取交集,A n B
file_list_inter=(`echo ${file_list_1[*]} ${file_list_2[*]}|sed 's/ /\n/g'|sort|uniq -c|awk '$1!=1{print $2}'`)
echo ${file_list_inter[*]}

# 对称差集,不属于 A n B
file_list_4=(`echo ${file_list_1[*]} ${file_list_2[*]}|sed 's/ /\n/g'|sort|uniq -c|awk '$1==1{print $2}'`)
echo ${file_list_4[*]}

命令解释

在上述三条命令中,首先使用echo 输出由两个数组构成的集合,然后使用sed将空格替换成换行符\n, 再使用sort排序:

  • 使用uniq处理则获得并集.
  • 使用uniq -c处理则. 再使用awk分析第一个参数$1若不等于1就表示这是两个数组中,于是打印$2便得到交集.
  • 使用uniq -c处理则. 再使用awk分析第一个参数$1若等于1就表示这是两个数组中,于是打印$2便得到差集.

命令参考

执行结果

执行结果
1
2
3
test1 test2 test3 test4 test5 test6 test7 test8
test5 test6
test1 test2 test3 test4 test7 test8

添加前后缀

在Linux中使用bash正则表达式来为数组元添加前缀和后缀是一个标准的做法,这在脚本编写中是一个很方便的操作。例如:

bash正则表达式添加前后缀
1
2
3
ARRAY=( one two three )
echo ${ARRAY[@]/#/prefix_}
echo ${ARRAY[@]/%/_suffix}

除了正则表达式外,还有一种漂亮的解决方案:

数组加入前后缀
1
2
3
4
$ ARRAY=(A B C)
$ mapfile -t -d $'\0' EXPANDED < <(printf "prefix_%s_postfix\0" "${ARRAY[@]}")
$ echo "${EXPANDED[@]}"
prefix_A_postfix prefix_B_postfix prefix_C_postfix

mapfile将行读入数组的元素。使用-d $'\0',它将读取以null分隔的字符串,并且-t将从结果中省略分隔符。参见help mapfile

删除前后缀

删除后缀

bash正则表达式删除后缀
1
2
3
4
5
ARRAY=( one.git two.git three.git four.me )
echo ${ARRAY[@]%.git}
> one two three four.me
echo ${ARRAY[@]%.*}
> one two three four

删除前缀

bash正则表达式删除前缀
1
2
3
4
5
ARRAY=( pre.one pre.two pre.three me.four )
echo ${ARRAY[@]#pre.}
> one two three me.four
echo ${ARRAY[@]#*.}
> one two three four

注意:在删除前后缀中,使用的*是正则表达式,表示任意个字符,若要删除指定字符,应当输入具体的字符。

正则表达式可以极大的提高程序效率,本文提供Shell中的正则表达相关知识。

分类

正则表达式最早在 1950 年代由美国数学家 Stephen Cole Kleene 提出,后来被 Unix 操作系统的文本处理工具广泛使用。

经过多年的发展和实践,最终形成两大标准,一个是 POSIX 标准,另一个是 Perl 标准。后者本是为 Perl 语言实现的,由于其功能非常强大,被 JavaJavaScript等语言广泛借鉴,从而被广泛使用。

这里将正则表达式分为三类:

  • 基本正则表达式(Basic Regular Expression 简称 BRE),由 POSIX 标准定义。
  • 扩展正则表达式(Extended Regular Expression 简称 ERE),也由 POSIX 标准定义。
  • Perl 的正则表达式(Perl Regular Expression 简称 PRE),由 Perl 语言定义。

组成部分

基本组成部分

正则表达式 描述 示例 Basic RegEx Extended RegEx Perl regEx
\ 转义符,将特殊字符进行转义,忽略其特殊意义 a.b匹配a.b,但不能匹配ajb,.被转义为特殊意义 \ \ \
^ 匹配行首 ^tux匹配以tux开头的行 ^ ^ ^
$ 匹配行尾 tux$匹配以tux结尾的行 $ $ $
. 匹配除换行符\n之外的任意单个字符 ab.匹配abc或bad,不可匹配abcd或abde,只能匹配单字符 . . .
[] 匹配包含在[字符]之中的任意一个字符 coo[kl]可以匹配cook或cool [] [] []
[^] 匹配1之外的任意一个字符 1232不可以匹配1234或1235,1236、1237都可以 [^] [^] [^]
[-] 匹配[]中指定范围内的任意一个字符,要写成递增 [0-9]可以匹配1、2或3等其中任意一个数字 [-] [-] [-]
? 匹配之前的项1次或者0次 colou?r可以匹配color或者colour,不能匹配colouur 不支持 ? ?
+ 匹配之前的项1次或者多次 sa-6+匹配sa-6、sa-666,不能匹配sa- 不支持 + +
* 匹配之前的项0次或者多次 co*l匹配cl、col、cool、coool等 * * *
() 匹配表达式,创建一个用于匹配的子串 ma(tri)?匹配max或maxtrix 不支持 () ()
{n} 匹配之前的项n次,n是可以为0的正整数 [0-9]{3}匹配任意一个三位数,可以扩展为0-9[0-9] 不支持 {n} {n}
{n,} 之前的项至少需要匹配n次 [0-9]{2,}匹配任意一个两位数或更多位数 不支持 {n,} {n,}
{n,m} 指定之前的项至少匹配n次,最多匹配m次,n<=m [0-9]{2,5}匹配从两位数到五位数之间的任意一个数字 不支持 {n,m} {n,m}
交替匹配 两边的任意一项 ab(c d)匹配abc或abd

POSIX 字符类

POSIX字符类是一个形如[:...:]的特殊元序列(meta sequence),他可以用于匹配特定的字符范围。

正则表达式 描述 示例 Basic RegEx Extended RegEx Perl RegEx
[:alnum:] 匹配任意一个字母或数字字符 [[:alnum:]]+ [:alnum:] [:alnum:] [:alnum:]
[:alpha:] 匹配任意一个字母字符(包括大小写字母) [[:alpha:]]{4} [:alpha:] [:alpha:] [:alpha:]
[:blank:] 空格与制表符(横向和纵向) [[:blank:]]* [:blank:] [:blank:] [:blank:]
[:digit:] 匹配任意一个数字字符 [[:digit:]]? [:digit:] [:digit:] [:digit:]
[:lower:] 匹配小写字母 [[:lower:]]{5,} [:lower:] [:lower:] [:lower:]
[:upper:] 匹配大写字母 ([[:upper:]]+)? [:upper:] [:upper:] [:upper:]
[:punct:] 匹配标点符号 [[:punct:]] [:punct:] [:punct:] [:punct:]
[:space:] 匹配一个包括换行符、回车等在内的所有空白符 [[:space:]]+ [:space:] [:space:] [:space:]
[:graph:] 匹配任何一个可以看得见的且可以打印的字符 [[:graph:]] [:graph:] [:graph:] [:graph:]
[:xdigit:] 任何一个十六进制数(即:0-9,a-f,A-F) [[:xdigit:]]+ [:xdigit:] [:xdigit:] [:xdigit:]
[:cntrl:] 任何一个控制字符(ASCII字符集中的前32个字符) [[:cntrl:]] [:cntrl:] [:cntrl:] [:cntrl:]
[:print:] 任何一个可以打印的字符 [[:print:]] [:print:] [:print:] [:print:]

元字符

元字符(meta character)是一种 Perl 风格的正则表达式,只有一部分文本处理工具支持它,并不是所有的文本处理工具都支持。

正则表达式 描述 示例 Basic RegEx Extended RegEx Perl RegEx
\b 单词边界 \bcool\b 匹配cool,不匹配coolant \b \b \b
\B 非单词边界 cool\B 匹配coolant,不匹配cool \B \B \B
\d 单个数字字符 b\db 匹配b2b,不匹配bcb 不支持 不支持 \d
\D 单个非数字字符 b\Db 匹配bcb,不匹配b2b 不支持 不支持 \D
\w 单个单词字符(字母、数字与_) \w 匹配1或a,不匹配& \w \w \w
\W 单个非单词字符 \W 匹配&,不匹配1或a \W \W \W
\n 换行符 \n 匹配一个新行 不支持 不支持 \n
\s 单个空白字符 x\sx 匹配x x,不匹配xx 不支持 不支持 \s
\S 单个非空白字符 x\S\x 匹配xkx,不匹配xx 不支持 不支持 \S
\r 回车 \r 匹配回车 不支持 不支持 \r
\t 横向制表符 \t 匹配一个横向制表符 不支持 不支持 \t
\v 垂直制表符 \v 匹配一个垂直制表符 不支持 不支持 \v
\f 换页符 \f 匹配一个换页符 不支持 不支持 \f

常见命令中的使用

命令 Basic RegEx Extended RegEx Perl RegEx
grep 支持 需加 -E 参数 需加 -P 参数
egrep 支持 支持 需加 -P 参数
sed 支持 需加 -r 参数 不支持
awk 支持 支持 不支持

添加方式 语法 多元素 下标连续 下标改变 覆盖原元素
直接下标添加 array_name[index]=value
数组长度添加 array_name[${#array_name[@]}]=value
数组长度添加 array_name[${#array_name[*]}]=value
重新创建数组 array_name=("${array_name[@]}" value1 ... valueN)
赋值运算符+= array_name+=(value1 ... valueN)

综上可知,是最通用的方案。

当编写脚本时,可能会遇到需要的命令在系统中没有安装, 此时需要解决命令依赖的问题,解决方法为安装对应的软件。Linux下大量的发行版,且不同发行版一般使用不同的包管理器,这导致了安装软件需要执行不同的安装命令,于是需要解决第二个问题:使用通用Linux命令探测系统安装的包管理器,然后确定安装命令。但是有时候,软件包提供的命令与其包的名称并不一致,这里就需要建立一个命令与包的映射关系,以正确执行安装命令。按照这个思路,本文提供一个基础的脚本:

通用安装脚本
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
#! /bin/sh
#
# readycmd.sh
# Author : fengzhenhua
# Email : fengzhenhua@outlook.com
# Date : 2024-06-14 15:58
# CopyRight: Copyright (C) 2022-2030 FengZhenhua(冯振华)
# License : Distributed under terms of the MIT license.
# Objective: 在不同的发行版中安装命令与软件包名不同的命令
#
# 包管理器列表: 管理器=安装命令
declare -A RD_PKG=(\
["pacman"]="pacman -S --needed --confirm" #arch endeavour manjaro 等
["apt-get"]="apt-get -y install" #debian ubuntu 等
["yum"]="yum -y install" #redhat centos7 及以下
["dnf"]="dnf -y install" #fedora centos8
["zypper"]="zypper -y install" #open suse
)
# 待安装软件列表: 命令=软件名
declare -A RD_CMD=(\
["unar"]="unarchiver" ["ssh"]="openssh" ["nvim"]="neovim"
)
# 选择发行版中的包管理器
for sh_pkg in ${!RD_PKG[*]}; do
which $sh_pkg &> /dev/null
if [ $? = 0 ]; then
RD_PKG_INS=${RD_PKG[$sh_pkg]}
fi
done
# 使用发行版中的包管理器安装系统中缺失的命令
for sh_cmd in ${!RD_CMD[*]}; do
which ${sh_cmd} &> /dev/null
if [ ! $? = 0 ]; then
sudo ${RD_PKG_INS} ${RD_CMD[$sh_cmd]}
fi
done

find , locate, whereis 和which 的区别

  • which:常用于查找可直接执行的命令。只能查找可执行文件,该命令基本只在$PATH路径中搜索,查找范围最小,查找速度快。默认只返回第一个匹配的文件路径,通过选项 -a 可以返回所有匹配结果。

  • whereis:不只可以查找命令,其他文件类型都可以(man中说只能查命令、源文件和man文件,实际测试可以查大多数文件)。在$PATH路径基础上增加了一些系统目录的查找,查找范围比which稍大,查找速度快。可以通过 -b 选项,限定只搜索二进制文件。

  • locate:超快速查找任意文件。它会从linux内置的索引数据库查找文件的路径,索引速度超快。刚刚新建的文件可能需要一定时间才能加入该索引数据库,可以通过执行updatedb命令来强制更新一次索引,这样确保不会遗漏文件。该命令通常会返回大量匹配项,可以使用 -r 选项通过正则表达式来精确匹配。

  • find:直接搜索整个文件目录,默认直接从根目录开始搜索,建议在以上命令都无法解决问题时才用它,功能最强大但速度超慢。除非你指定一个很小的搜索范围。通过 -name 选项指定要查找的文件名,支持通配符。

find , locate, whereis 和which 的用法

1. find

find是最常见和最强大的查找命令,你可以用它找到任何你想找的文件。

find的使用格式如下:

  $ find <指定目录> <指定条件> <指定动作>

  - <指定目录>: 所要搜索的目录及其所有子目录。默认为当前目录。

  - <指定条件>: 所要搜索的文件的特征。

  - <指定动作>: 对搜索结果进行特定的处理。

如果什么参数也不加,find默认搜索当前目录及其子目录,并且不过滤任何结果(也就是返回所有文件),将它们全都显示在屏幕上。

find的使用实例:

  $ find . -name "my*"

搜索当前目录(含子目录,以下同)中,所有文件名以my开头的文件。

  $ find . -name "my*" -ls

搜索当前目录中,所有文件名以my开头的文件,并显示它们的详细信息。

  $ find . -type f -mmin -10

搜索当前目录中,所有过去10分钟中更新过的普通文件。如果不加-type f参数,则搜索普通文件+特殊文件+目录。

2. locate

locate命令其实是“find -name”的另一种写法,但是要比后者快得多,原因在于它不搜索具体目录,而是搜索一个数据库(/var/lib/locatedb),这个数据库中含有本地所有文件信息。Linux系统自动创建这个数据库,并且每天自动更新一次,所以使用locate命令查不到最新变动过的文件。为了避免这种情况,可以在使用locate之前,先使用updatedb命令,手动更新数据库。

locate命令的使用实例:

  $ locate /etc/sh

搜索etc目录下所有以sh开头的文件。

  $ locate ~/m

搜索用户主目录下,所有以m开头的文件。

  $ locate -i ~/m

搜索用户主目录下,所有以m开头的文件,并且忽略大小写。

3. whereis

whereis命令只能用于程序名的搜索,而且只搜索二进制文件(参数-b)、man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。

whereis 命令的使用实例
1
2
$ whereis grep 
grep: /usr/bin/grep /usr/share/man/man1/grep.1.gz /usr/share/man/man1p/grep.1p.gz /usr/share/info/grep.info.gz

4. which

which命令的作用是,在PATH变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。也就是说,使用which命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。

which 命令使用实例
1
2
$ which date
/usr/bin/date