学习笔记|Shell

本文最后更新于:3 years ago

本文在 linux 命令的基础上,具体描述 Shell 脚本的相关知识。

1.shell格式

一堆命令写在在同一个文件里。

注意:第一行 #! 与 shell 之间有空格。

1
2
3
4
5
6
7
#! /bin/sh

# this is my annotation

echo “Our frist an empty line in output”

/bin/pwd

2.执行脚本

source 是 bash shell 的内置命令。(与 . 等价)

1
2
3
4
5
6
bash my_shell
zsh my_shell
./my_shell
source my_shell

. ~/learn_shell/my_shell # .后面有空格,加绝对路径

3.内建命令

在使用 export PATH= 后,仍能执行的命令就是内建命令。

1
man bash-builtins  # 查看内建命令

4.小括号

执行 cd 后,不改变工作目录。

相当于父进程为当前路径,子进程执行该命令。

1
2
(cd ..; ls -al)
(cd .. && ls -al)

5.变量

5.1 数据类型

只有一种数据类型:字符串

1
2
a=1
echo a # 虽然结果得到的是 1,但是也是字符串类型

5.2 变量类型

两种变量:环境变量、本地变量。

  • 环境变量:环境变量存在于所有进程中。环境变量可以从父进程传给子进程,因此 Shell 进程的环境变量可以从当前 Shell 进程传递给 fork 出来的子进程。
1
2
3
$PATH   # 特殊的环境变量(命令程序所在目录)
$HOME # 特殊环境变量(家目录)
env # 查看所有环境变量
  • 本地变量:只存在于当前 Shell 进程中。

    注意:等号两边不能有空格,否则会被解释成命令和命令参数。

1
2
set   # 查看所有本地变量
var_name = value # 定义本地变量

5.3 变量导出与删除

  • 本地变量只能在当前 Shell 进程中使用,export 可以将本地变量导出为环境变量,使本地变量在其他进程中可以使用。
1
2
3
4
5
6
# 定义、导出,两步完成
aaaa=2
export aaaa

# 一步完成
export aaaa=2
  • 删除已定义的环境变量和本地变量。
1
unset aaaa

6.通配符

Wildcard

通配符含义
*匹配 0 个或者多个任意字符
?匹配 1 个任意字符
[]匹配方括号中任意一个字符
  • 通配符与正则表达式的区别
  1. 用途不同

    通配符用于通配文件名,正则表达式用于匹配文本内容

  2. 使用地点不同

    通配符通常只能用于shell,被shell自解释。

    正则表达式需要被正则引擎解析,需要用于支持正则表达式的代码或命令中。

  3. 元字符不同

    通配符只有三个元字符,正则表达式根据正则引擎不同会多种多样。
    并且相同元字符表示的含义也可能不同。如通配符中表示匹配任意字符,正则中表示前面的字符重复0或任意次。

  • **实例:**使用 grep是字符串命令,需使用正则表达式;文件名通配使用通配符。
1
2
3
4
ls *.txt               # 匹配全部后缀为.txt的文件
ls file?.log # 匹配file1.log, file2.log, ...
ls [a-z]*.log # 匹配a-z开头的.log文件
ls [^a-z]*.log # 上面的反向匹配
  • 注意

反斜杠(\)或引号(', ")都会使通配符失效。

如: \*, "*", '*'都表示*本身,不通配任何文件。

7.命令代换

  • 由反引号括起来的是一条命令,Shell 应先执行该命令,再将执行结果代换到当前命令中。
  • 等号两侧无空格。
1
2
DATE=`date`
echo $DATE
  • 命令代换可以使用 $() 表示。
1
2
DATE=$(date)
echo $DATE

8.算术代换

算术代换要写两个括号或者一个中括号。

1
2
3
4
5
var=10
echo $(($var+3))
echo $((var+3))
echo $[var+3]
echo $[$var+3]

PS:二进制的100 + 十进制的3

1
echo $[2#100+1]   # 输出 7

9.转义字符

  • 连接符:将第一行命令和第二行命令连接起来。
1
2
ls \
-l
  • 转义符:将特殊字符转义

    例如:创建 $ $200.txt 文件。其中 $ 和空格均需要转义。

1
touch \$\ \$200.txt
  • PS:如何创建和删除 ---a.txt 文件?
    • 方式一:写全路径!
    • 方式二:使用 –
1
2
3
4
5
6
7
8
9
# 方式一
touch ./---a.txt # 创建
rm -rf ./---a.txt # 删除

touch ---a.txt # 错误,---a会被识别成命令参数

# 方式二
touch -- ---a.txt
rm -rf -- ---a.txt

10.引号

引号是字符串的界定符。引号中的内容就是纯粹的字符串。

1
echo '$SHELL'
  • 例:想要输出 hello 'hello'

    输出单引号使用双引号作为界定符,输出双引号使用单引号作为界定符。

1
echo "hello 'hello'"
  • 单引号和双引号区别

    双引号防止通配符扩展,但是允许变量扩展。

1
2
echo "$SHELL"   # 环境变量路径
echo '$SHELL' # 单纯的字符串

PS:作为好的 Shell 编程习惯,应该总是把变量取值放在双引号内。

11.shell脚本语法

11.1 条件测试 test

  • $? :上一个进程退出返回的值。
1
echo $?
  • 例1:使用 test 比较大小
参数含义
-eq等于
-ne不等于
-lt小于
-le小于等于
-gt大于
-ge大于等于

PS:返回 0 表示真,返回 1 表示假。【和其他语言相反】

注意:比较双方只能是整数或者整数变量。

1
2
3
4
5
6
7
8
# 定义变量
var=20

# 比较 var 是否大于 100
test $var -gt 100

# 打印上个进程的结果
eoch $? # 返回 0,表示假,表示 var 不大于 100
  • 例2:使用 test 判断类型
参数含义
-d是否是目录
-f是否是普通文件
-zstging 是否为零
-nstging 是否为非零
=两个字符串是否相同
!=两个字符串是否不相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.判断文件夹
test -d my_dir

# 2.判断普通文件
test -f my_fifo

# 3.判断字符串长度是否为0
var=aa
test -z var

# 4.判断字符串是否相等
var1=a
var2=b
test $var1 = $var2 # 等号两端必须有空格
test 2 = 2
  • 例3:使用 [] 代替 test
1
2
3
4
5
var1=a
var2=b
[ $var1 = $var2 ] # 中括号和等号两端需要有空格

[ -f my_fifo ]
  • 例4:链接符 -a、-o
1
2
3
[ -f my_fifo -a -d my_dir ]  # and

[ -f my_fifo -o -d my_dir ] # or

11.2 模糊匹配 if

  • if、then、elif、else、fi 的使用
1
2
3
4
5
6
7
if [ -f frist_shell ]; then
echo "This is common file"
elif [ -d frist_shell ]; then
echo "This is dir"
else
printf "unkonw\n"
fi

PS:then 可以分行写,这样 ; 不用写,如下所示。

1
2
3
4
5
6
7
8
9
if [ -f frist_shell ]
then
echo "This is common file"
elif [ -d frist_shell ]
then
echo "This is dir"
else
printf "unkonw\n"
fi
  • 写成一行
1
if [ -f frist_shell ]; then echo "This is common file"; else printf "unkonw\n"; fi
  • : 是一个特殊的命令
1
2
3
if :; then
echo "always true"
fi

11.3 精准匹配 case

  • ;; 表示语句结束,类似于 C 语言中的 break。【一定别忘了】

  • 例:根据输入做 case 判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /bin/zsh

# 输入数据保存到 BUF 中
echo "Enter a yes or no"
read BUF

# 进行 case 判断
case "$BUF" in
yes|Yes|YES|y|Y) # 第一种情况
echo "It is a yes";;
[nN]?) # 第二种情况
echo "It is a no";;
*) # 其他情况
echo "other case";;
esac # 结束

echo "Going to return"
return 0
  • 实际应用

服务 start、stop、status 等均是通过参数来控制,可以用 case 做脚本进行控制。

1
2
3
4
5
6
7
8
9
10
11
12
case "$parameter" in 
start)
...;;
stop)
...;;
restart)
...;;
status)
...;;
*)
...;;
esac

11.4 循环 for

类似于 foreach,枚举。

  • 例1:在三种水果中进行枚举。
1
2
3
for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done
  • 例2:批量改名

    找到所有的 chap1、chap2,更改为 chap1~、chap2~。

1
for FILE_NAME in chap?; do mv "$FILE_NAME" "$FILE_NAME~"; done

11.5 循环 while

count 自减,循环输出 count 值。

1
2
3
4
5
count=3
while [ $count -gt 0 ]; do
count=$[count-1] # 赋值过程中,左侧不写$ # 算术代换使用 []
echo "count = $count"
done

11.6 break 与 continue

break:可以指定跳出几层循环。

continue:跳过本次循环,但不会跳出循环。

12.特殊变量与输入输出

特殊变量是由 Shell 自动赋值的。

自动变量含义
$0相当于 c 中的 argv[0]
$1、$2…相当于 c 中的 argv[1]、argv[2]…
$#参数个数,argc - 1
$@表示参数列表 $1、$2、$3、$4…
例如可以使用在 for in 后面
$*同上
$?上一条命令的 Exit Status
$$当前进程号

shift 是参数左移出队的操作。

  • shift 相当于 shift1
  • shift3:左移出队三个参数
1
2
3
./my.sh aa bb cc dd ee

# my.sh 中有 shift2 后,my.sh 的数就变为 cc、dd、ee

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
printf "第一个参数%s 是 %s\n" '$0' "$0"
printf "参数个数%s 是 %s\n" '$#' "$#"
echo "循环打印参数列表"

for arg in $@; do
echo "$arg"
done

# 作为好的 Shell 编程习惯,应该总是把变量取值放在双引号内。(引号内是纯粹的字符串
,但双引号允许变量扩展)

# 使用变量前面应加上 $

echo "使用 shift 进行参数左移"

shift # 左移一次
for arg in $@; do
echo "$arg"
done
  • echo 有两个参数:-e 和 -n
参数含义
echo默认有一个回车
-e解析转义字符
-n不换行
1
2
echo "hello\n"      # 无回车
echo -e “hello\n” # 有回车

13.管道

1
ls | grep more

14.tee

将标准输出同时输出到指定的文件中。

1
ls | tee out.txt

15.文件重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# cmd > file
# 例:将 ls 输出结果重定向给 file。
ls > file

# cmd >> file
# 例:将 ls 输出结果追加在 file 文件末尾。
ls >> file

# cmd > file 2>&1
# 例:将 ls abc.txt 的结果重定向到 file 中(如果命令没有执行成功,产生报错信息,将把报错信息写入 file 中)。
# 注意:2>&1 符号两侧没有空格,表示:文件描述符2(标准错误)重定向到文件描述符1(标准输出)。
ls abc.txt > file 2>&1

# cmd < file > new_file
# 例:[拷贝] 从 file 中读取内容,并写入 new_file。
cat < file > new_file

# 例:[统计] 将 file 的行数进行统计,并将统计结果写入 new_file 中。
wc -l < file > new_file

# 技巧:重定向至黑洞中。
ls > /dev/null 2>&1

16.函数

有函数定义,但是没有函数返回值、函数列表。

先定义、再使用。

1
2
3
4
5
6
7
8
foo()     # 定义函数
{
echo "This is function!"
}

echo "----start----"
foo # 使用函数
echo "----end----"
  • 如何使用参数?

使用 $1、$2、$3…来表示函数的参数。

注意:函数内部使用 $1、$2、$3…只能使用函数的参数。(同时 $#、$@ 也是表示函数的参数列表,而不是整个脚本的参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
foo()
{
echo $0
echo $1
echo $2
echo $3
echo "This is function!"
}

echo "----start----"
foo 11 22 33
echo "----end----"

# 打印结果:
----start----
foo
11
22
33
This is function!
----end----

**实例:**批量创建文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
is_dir()
{
dir_name=$1
if [ ! -d $dir_name ]; then # 不是目录 ==》 真 ==》 test 返回 0 ==》 if 假 ==》 return 0;
return 1;
esle
return 0;
fi
}

for dir_name in "$@"; do
if is_dir $dir_name; then
echo$dir_name is exits.”
else
echo "$dir_name is not exits,creating it now."
mkdir $dir_name > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "error: Cannot create $dir_name."
exit 1;
fi
fi
done

17.调试 shell 脚本

参数含义
-n
-v
-x

18.正则表达式

查找文件中的内容,字符串匹配(grep,egrep)见基础正则和扩展正则区别>。

注意:Linux 中使用,一定要使用双引号。

(1)字符类

字符含义举例说明
.匹配任意一个字符abc.匹配 abcd、abc9等
[]匹配括号中任一字符[abc]只能匹配a、b、c
-在[]括号内表示字符范围[0-9a-fA-F]匹配一位十六进制的数字
^位于[]括号内的开头,匹配除括号中的字符之外的任意一个字符[^xy]匹配除 xy 之外的任一字符,可以匹配 a1、b1,但不能匹配 x1、y1
[[:xxx:]]grep 工具预定义的一些命名字符类[[:alpha:]] /[[:digit:]]匹配一个字母 / 匹配一个数字
  • . 匹配任意一个字符。
1
yum list installed | grep .um  # um 前面必须有一个字符。
  • [mn] 匹配 [] 里的任意一个字符。
1
2
3
4
➜  ~ yum list installed | grep "[mn]aria"   # 注意双引号
mariadb.x86_64 1:5.5.65-1.el7 @base
mariadb-libs.x86_64 1:5.5.65-1.el7 @anaconda
mariadb-server.x86_64 1:5.5.65-1.el7 @base
  • [^yd] 匹配除 [] 中字符的其他任一字符。
1
2
3
yum list installed | grep "[^yd]um"     # 注意双引号
numactl-libs.x86_64 2.0.12-5.el7 @anaconda
perl-Data-Dumper.x86_64 2.145-3.el7 @base
  • \w 【小写w】匹配文字和数字字符,也就是 [A-Za-z0-9]。
1
grep "\w" 1.txt  # 必须有双引号
  • \W 【大写W】匹配非文字和数字字符,“\w” 的反置形式。
1
grep "\W" 1.txt  # 必须有双引号

(2)数量限定符

字符含义举例说明
紧跟在它前面的单元应匹配零次或一次[0-9]?\.[0-9]可匹配 0.0、2.3、.5
+紧跟在它前面的单元应匹配一次或多次[0-9a-zA-Z_.-]+@[0-9a-zA-Z_.-]+\.com匹配邮箱
*紧跟在它前面的单元应匹配零次或多次
{N}紧跟在它前面的单元应匹配 N 次[1-9][0-9]{2}匹配100-999的整数
{N,}紧跟在它前面的单元应匹配至少 N 次[1-9][0-9]{2,}匹配大于等于100的数
{,M}紧跟在它前面的单元应匹配最多 M 次
{N,M}紧跟在它前面的单元应匹配至少 N 次,最多 M 次[0-9]{1,3}(.[0-9]{1,3}){3}匹配 ip,()内重复三次
  • * *前面的字符可以重复 0 次或者 多次。
1
grep "a*b"   # 表示 a 可以出现多次或者0次
  • x\{m\} 匹配 字符 x重复 m 次 的行。
1
2
3
4
5
6
7
➜  ~ yum list installed | grep "0\{3\}"   # 查找重复 3 次 0 的行。
iwl1000-firmware.noarch
iwl2000-firmware.noarch
iwl5000-firmware.noarch
iwl6000-firmware.noarch
iwl6000g2a-firmware.noarch
iwl6000g2b-firmware.noarch
  • x\{m,\} 匹配 字符 x 重复 m 次或者 m 次以上 的行。[m, +∞)
1
grep "0\{2,\}" 1.txt   # 查找 0 重复 2 次以上的行。
  • x\{m, n\} 匹配 字符 x 重复 m 次或者 m 次以上,不超过 n 次 的行。 [m, n]
1
grep "0\{2,3\}" 1.txt  # 查找 重复次数在 [2, 3] 之间的行。

(3)位置限定符

字符含义举例说明
^匹配行首的位置^Content匹配位于一行开头的 Content
$匹配行末的位置answer:$匹配位于一行结尾的 answer:
\<匹配单词开头的位置\<ab匹配 ab 开头的单词
\>匹配单词结尾的位置ab\>匹配 ab 结尾的单词
\b匹配单词开头或结尾的位置可以完全取代 \< 和 \>
\B匹配非单词开头和结尾的位置\Bab\Bab 既不在开头,也不在结尾
  • 匹配行开头。
1
2
yum list installed | grep ^a
grep ^a 1.txt
  • 匹配行结尾。
1
yum list installed | grep a$
  • 匹配单词开头。
1
yum list installed | grep "\<maria"  # 匹配以 db 开头的单词所在的行,例如mariadb,注意要有双引号
  • 匹配单词结尾。
1
yum list installed | grep "db\>"     # 匹配以 db 结尾的单词所在的行,例如mariadb,注意要有双引号
  • 匹配单词。
1
yum list installed | grep "mariadb"  # 只能匹配完整的单词 mariadb,例如:可以匹配 mari

(4)其他特殊字符

字符含义举例
\转义字符,将特殊字符转义为普通字符;普通字符转义为特殊字符。文本中出现 . 需要使用 \. 来进行转义。
普通字符 <> 转义为匹配开头和结尾 \< \>
()将正则表达式一部分括起来组成一个单元,可以对整个单元使用数量限定符。[0-9]{1,3}(.[0-9]{1,3}){3}
|连接两个子表达式,表示或的关系。n(o|either),匹配 no、neither

(5)基础正则和扩展正则
egrep 可以直接使用扩展正则。grep 若要使用扩展正,则有两种方式:

  • 将 ?+{}|() 使用 \ 进行转义。
  • 加 -E 参数,grep -E
1
2
3
egrep "[0-9]{1,3}(\.[0-9]{1,3}){3}" text
grep "[0-9]\{1,3\}\(\.[0-9]\{1,3\}\)\{3\}" text
grep -E "[0-9]{1,3}(\.[0-9]{1,3}){3}" text

19.sed

筛选行数据。

19.1 命令格式

  1. sed 参数 ’脚本语句‘ 待操作文件
1
2
sed '/echo/s/echo/printf/g' my_shell.sh
sed 's/echo/printf/g' my_shell.sh
  • echo:在 my_shell.sh 中找到 echo
  • s:表示字符串替换
  • echo/printf:将 echo 替换成 printf
  • g:一行出现多次 echo,均全部替换
  1. sed 参数 -f ’脚本文件‘ 待操作的文件

19.2 命令参数

参数含义
-n静默输出,sed 程序在所有脚本指令执行完毕后,自动打印模式空间中的内容。-n屏蔽自动打印。
-f从文件中读取脚本指令
-i直接修改源文件。经过脚本指令处理的内容直接输出至源文件。
-r使用扩展正则。

19.3 脚本格式

1
/pattern/action
  • pattern 是正则表达式,action 是编辑操作。

  • sed 一行行读取待处理文件,如果某一行与 pattern 匹配,则执行相应的 action

  • 如果一条命令没有 pattern,而只有 action,这个 action 将作用于待处理文件的每一行。

举例:

1
2
3
4
5
/pattern/p    # 打印匹配 pattern 的行
/pattern/d # 删除匹配 pattern 的行

/pattern/s/pattern1/pattern2/ # 查找符合 pattern 的行,将该行第一个匹配 pattern1 的字符串替换成 pattern2
/pattern/s/pattern1/pattern2/g # 查找符合 pattern 的行,将该行所有匹配 pattern 字符串替换成 pattern2

19.4 脚本参数

参数含义
aappend 追加
iinsert 插入
ddelete 删除
ssubstitution 替换
  • append

在 my_shell.sh 的第 4 行后,追加一行,append_line 在第 5 行显示。

1
sed '4a append_line' my_shell.sh
  • insert

在 my_shell.sh 的第4行,插入一行,insert_line 在第 4 行显示。

1
sed '4i insert_line' my_shell.sh
  • delete

注意:替换修改源文件需要加参数 -i。

删除 my_shell.sh 的第 4 行。

1
sed '4d' my_shell.sh

删除 my_shell.sh 的第 2 到第 7 行。

1
sed '2,7d' my_shell.sh
  • substitution

注意:替换修改源文件需要加参数 -i。

将 my_shell.sh 的 echo 替换成 printf。

1
2
3
4
5
sed 's/echo/printf/g' my_shell.sh

# s:表示字符串替换
# echo/printf:将 echo 替换成 printf
# g:一行出现多次 echo,均全部替换

19.5 替代符

  • &

在 abc 的两侧加入 ==

1
sed 's/abc/==&==/' my_shell.sh
  • \1

sed 只支持 basic 正则表达式规范。

匹配两个数字,第一个数字部分用 \1 表示,第二个数字数字用 \2 表示,在 \1 和 \2 位置两侧分别加入 == 和 @@。

1
2
3
4
5
6
# sed 's/([0-9])([0-9])/==\1==@@\2@@/g' my_shell.sh
# 上面脚本并不能成功执行,需要加 (),加 () 才能表示一个部分,并转义。
# 或者使用 -r 进行转义。

sed 's/\([0-9]\)\([0-9]\)/==\1==@@\2@@/g' my_shell.sh
sed -r 's/([0-9])([0-9])/==\1==@@\2@@/g' my_shell.sh

19.6 其他

  • 同一个文件,两个替换命令。
1
sed 's/a/A/;s/b/B/' my_shell.sh
  • 同一个文件,两个替换命令。(使用 -e 指定)
1
sed -e 's/a/A' -e 's/b/B' my_shell.sh
  • **案例:**将 html 的标签删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script data-compress=strip>
function h(obj){
obj.style.behavior='url(#default#homepage)';
var a = obj.setHomePage('//www.baidu.com/');
}
</script>
<script>
_manCard = {
asynJs : [],
asynLoad : function(id){
_manCard.asynJs.push(id);
}
};
window._sp_async = 1;
</script>

思路:

  1. [^<>]:匹配任意一个非 <> 的字符。
  2. [^<>]*:确保匹配到 <> 中的一串字符。
  3. <[^<>]*>:强制要求所匹配到的字符串两端需要有 <>。
1
sed 's/<[^<>]*>/g' html

20.awk

sed:处理行数据

awk:处理列数据

20.1 命令格式

  1. awk 参数 ‘脚本语句’ 待操作文件
1
2
# 打印文件的第一列
awk '{print $1}' file
  1. awk 参数 -f ‘脚本语句’ 待操作文件

20.2 命令参数

参数含义
-F:指定列的分隔符为 :。冒号可以换成其他符号。
  • -F

在 /etc/passwd 中,以 : 分割每一列。

1
awk -F: '{print $1}' /etc/passwd

20.3 脚本格式

1
2
/pattern/{action}
condition{action}

**例:**有以下三行数据,筛选大于等于 50 的行。

1
2
3
product 30
product 76
product 55
1
2
awk '$2<50 {print $0} $2>=50 {print $0 " reorder"}' file
awk '$2<50 {print $0} $2>=50 {print("%s reorder", $0)}' file

20.4 使用变量

**例:**统计文本的空行数。

1
awk '/^ *$/ {count=count+1} END {print count}' file
  • /^ *$/:匹配空格开头,空格重复零次或多次,并以此结尾。
  • {count=count+1}:使用 count 进行计数。
  • END:匹配结束进行打印。

**例:**统计 id 在 [2600, 2700) 的行数。

1
ps aux | awk '$2>=2600 && $2<2700 {count=count+1} END {print count}'

补充:

  • END 是在匹配最后进行操作。
  • BEGIN 是在匹配之前进行操作。

在匹配之前,对文本进行拆分。(以: 为分割进行拆分)

FS 是内建变量,实际上是宏。表示分隔符,缺省是来纳许的空格和 tab。

1
awk 'BEGIN {FS=":"} {print $1}' /etc/passwd

21.C语言使用正则

  1. 编译正则表达式 regcomp()
  2. 匹配正则表达式 regexec()
  3. 释放正则表达式 regfree()

学习笔记|Shell
https://www.aimtao.net/linux-shell/
Posted on
2021-05-07
Licensed under