BNU-FZH

fengzhenhua@outlook.com

变量a是一个带空格的字符串,现在用"hdpusr400"替换变量a中的"hduser302":

字符串变量的替换方式

定义变量a
1
2
[liusiyi@localhost ~]$ echo $a
hduser302 hduser302 /apps/hduser302/student/

用变量替换${a//}做字符替换

用变量替换${a//}做字符替换
1
2
3
4
5
6
7
#替换第一个匹配的字符串 
[liusiyi@localhost ~]$ echo ${a/hduser302/hdpusr400}
hdpusr400 hduser302 /apps/hduser302/student/

#替换所有匹配的字符串 
[liusiyi@localhost ~]$ echo ${a//hduser302/hdpusr400}  
hdpusr400 hdpusr400 /apps/hdpusr400/student/

用 sed 做字符串替换

用sed做字符串替换
1
2
3
4
5
6
7
#替换第一个匹配的字符串 
[liusiyi@localhost ~]$ echo $a | sed 's/hduser302/hdpusr400/'   
hdpusr400 hduser302 /apps/hduser302/student/

#替换所有匹配的字符串 
[liusiyi@localhost ~]$ echo $a | sed 's/hduser302/hdpusr400/'   
hdpusr400 hdpusr400 /apps/hdpusr400/student/

用 awk 做字符串替换

用awk做字符串替换
1
2
3
4
5
6
7
#替换第一个匹配的字符串 
[liusiyi@localhost ~]$ echo $a | awk '{gsub(/hduser302/,"hdpusr400",$3);print $0}'
hdpusr400 hduser302 /apps/hduser302/student/

#替换所有匹配的字符串 
[liusiyi@localhost ~]$ echo $a | awk '{gsub(/hduser302/,"hdpusr400");print $0}'   
hdpusr400 hdpusr400 /apps/hdpusr400/student/

数组所有元中的字符串替换

数组所有元替换
1
2
Arr=($(ls -d /run/media/$USER/*/*))
Brr=(${Arr[*]//"/run/media/$USER"/"$HOME"})

Arr数组元中,所有元包括绝对路径/run/media/$USER, 而Brr数组将其全部替换为$HOME.

已经完成了ugit.sh程序,利用它可以方便的将仓库建立在U盘上,这也就达成了随身携带U盘仓库而不依赖网络的问题。但是在最初实现时,对Shell的理解不够深入,一些基础的命令没有用好,从而增加了一些循环判断,尽管在使用体验上不会有什么影响,但是仔细测试效率时还是有差别的,同时从维护脚本的角度讲不是足够简洁。本着精益求精的原则,决定进一步精简脚本,实现Shell层次上的效率极大化!在所有的问题中,一步列出所有U盘并且将U盘中的目录都添加绝对路径是一个重要的步骤,本文实现了这个一步操作。代码如下:

列出子目录并添加绝对路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ls -d /run/media/$USER/*/*
# 运行结果
/run/media/feng/BNU-FZH/article.git
/run/media/feng/BNU-FZH/cexam.git
/run/media/feng/BNU-FZH/config.git
/run/media/feng/BNU-FZH/document.git
/run/media/feng/BNU-FZH/latex.git
/run/media/feng/BNU-FZH/python.git
/run/media/feng/BNU-FZH/script.git
/run/media/feng/BNU-FZH/System Volume Information
/run/media/feng/feng/20230203_094347.jpg
/run/media/feng/feng/2023_07_19 17_25 Office Lens (1).jpg
/run/media/feng/feng/2023_07_19 17_25 Office Lens.jpg
/run/media/feng/feng/截图 2024-06-18 18-03-16.png
/run/media/feng/feng/截图 2024-06-18 18-06-42.png
/run/media/feng/feng/PDF文件
/run/media/feng/feng/System Volume Information

可以看到在路径/run/media/feng/下有两个U盘,分别是BNU-FZHfeng, 这是路径/run/media/$USER/*/*中左起第一个*所代表的,而最后一个*表示列出此目录下的内容。-d选项,则控制ls命令列出路径。一条命令实现了列出所有U盘目录的功能,极大的提高了效率!

Rust的官方网站是https://www.rust-lang.org, 它是一门赋予每个人构建可靠且高效软件能力的语言. 按官方介绍,选择Rust的原因主要为:

  • 高性能:Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。
  • 可靠性:Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。
  • 生产力:Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具——包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。

学习Rust语言,本博客提供以下几个途径:

本博客决定边学习边记录,以期完成一篇入门级的简洁实用教程。第一课当然是编写Hello World程序,其源码为:

hello_world.rs
1
2
3
4
5
// hello_world.rs
fn main() {
let s = "hello world!";
println!("{}", s);
}

由于其没有复杂的依赖关系,直接使用rustc编译即可,其他所有选项使用默认值:

编译hello_world.rs
1
rustc hello_world.rs

编译完成后,在本地文件夹中生成了可执行程序:hello_world, 执行程序./hello_world, 控制台上输出了hello world!字符串, 第一个Rust程序已经成功! 根据这个最简单的例子,我们得到一些基本的规则:

  • 一般Rust源代码的后缀名使用.rs表示。源码一定要注意使用utf-8编码。

  • 第一行是注释语句,Rust的注释是C语言系列风格的,行注释采用//开头,块注释使用/**/包围。它还支持更高级的文档注释,将在后文中详细展开说明。

  • fn是一个关键字(key word),函数定义必须以这个关键字开头。函数体使用大括号来包含。fn是单词function的缩写,在Rust中,设计者比较偏向使用单词缩写,即使是关键字也不例外。在代码风格上,某些读者可能开始会有点不习惯。但总体而言,这只是个审美偏好而已,不必过于纠结,习惯就好。

  • 默认情况下,main函数是可执行程序的入口点,它是一个无参数,无返回值的函数。如果我们要定义的函数有参数和返回值,可以使用以下语法(参数列表使用逗号分开,冒号后面是类型,返回值类型使用->符号分隔):

    1
    2
    3
    fn Foo( input1 : i32, input2 : u32) -> i32 {
    ...
    }

  • 局部变量声明使用let关键字开头,用双引号包含起来的部分是字符串常量。Rust是静态强类型语言,所有的变量都有严格的编译期语法检查。关于Rust的变量和类型系统将在后文详细说明。

  • 每条语句使用分号结尾。语句块使用大括号。空格、换行和缩进不是语法规则的一部分。这都是明显的C语言系列的风格。

最简单的标准输出是使用println!宏来完成。请大家一定注意println后面的感叹号,它代表这是一个宏,而不是一个函数。Rust中的宏与C/C++中的宏是完全不一样的东西。简单点说,可以把它理解为一种安全版的编译期语法扩展。这里之所以使用宏,而不是函数,是因为标准输出宏可以完成编译期格式检查,更加安全。

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 支持 支持 不支持