1.3 shell 脚本编程

Unix shell

shell 顾名思义为“壳”,主要是相对于系统内核来说的,可以理解为内核的“壳”,shell 负责将用户的指令解释并传给内核执行,直接接触内核是不被允许并危险的,所以 shell 就是一种命令解释器,常见的 shell有很多,分为图形界面 shell 和命令行 shell,命令行中最常用的就是 bash shell。

系统中,通过修改.bashrc文件来配置使用环境,输入source ~/.bashrc立刻使配置文件生效,如要配置全局生效,可修改/etc/bashrc 文件。

通过 cat /etc/shells 查看当前启用的 shell;通过 echo $SHELL查看用户的默认登录 shell。

Bash shell

在 Bash 中,通过 <Tab> 自动补全,使用 ctrl-r 搜索命令行历史记录(按下按键之后,输入关键字便可以搜索,重复按下 ctrl-r 会向后查找匹配项,按下 Enter 键会执行当前匹配的命令,而按下右方向键会将匹配项放入当前行中,不会直接执行,以便做出修改)。

输入 history 查看 1000 条命令行历史记录,再用 !nn 是命令编号)就可以再次执行该命令。

对文本的操作,有两个小技巧值得记忆:ctrl-a 可以将光标移至行首;ctrl-e 可以将光标移至行尾。

使用cd命令可以切换工作路径,~ 表示用户家目录,要访问家目录中的文件,可以使用前缀 ~,例如 vi ~/.bashrc。在脚本里则用环境变量 $HOME 指代家目录。

在两个工作目录之前来回切换是件很恼人的事情,可以使用cd -命令来快速在二者之前切换。

Vim 编辑器

当程序步骤复杂时,bash 有其局限性,所以要求用户熟悉至少一种基于文本的编辑器,编辑器可以将你的命令按顺序执行,称为脚本。通常 Vim 会是最好的选择(除了 vim 还有 Emacs 等编辑器)。

Vim 是从 vi 发展出来的一个文本编辑器。代码补全、错误跳转等功能特别丰富,在程序员中被广泛使用。vi/vim 分为三种模式,分别是命令模式(Command mode)输入/插入模式(Insert mode)底线命令/命令行模式(Last line mode)。 这三种模式的作用分别是:

命令模式:

用户启动vi/vim,就进入了命令模式。

此状态下敲击键盘动作会被Vim识别为命令,而非输入字符。命令模式只有一些最基本的命令(复制粘贴等等),因此通常需要底线命令模式扩展其功能。

输入模式/插入模式:

在命令模式下按i键,进入输入模式。

按下键盘ESC键,退出输入模式,切换到命令模式。完成文件的编辑后,在命令行模式中输入wq存盘退出,也可使用ZZ来达到同样的效果。

底线命令模式:

在命令模式下按下:(英文冒号),进入底线命令模式。

底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。按ESC键退出底线命令模式。

数据块模式:

使用control+v进入数据块选中模式,I进入插入模式,ESC后会统一在数据块区域插入相同内容。

在Vim中输入":help user-manual"进入用户手册。也有手册中文版可供参考。下面介绍了一些不同模式下的常用命令:

u            # 撤回   
dd           # 剪切单行    
yy           # 复制单行
p            # 粘贴    
nG           # 移动至文档第n行
G            # 移动至文档最后一行
gg           # = 1G 即移动至文档第一行
dd           # 剪切一整行,并使用p来粘贴
D            # 删除该行光标之后的所有字符
ndd          # 向下剪切n行
*            # 查找光标所在短语
<Shift>^     # 光标跳转至行首字符
<Shift>$     # 光标跳转至行未字符

在Vim中完成一个简单的脚本:

使用vim来建立一个名为test.sh的文件时,可以这样做:

$ vim test.sh

并且在插入模式中写出脚本后存盘退出。

#!/bin/bash    # 标记为bash
echo "my first script!"

并用./test.ah运行这个脚本。

echo命令 用于在shell中打印shell变量的值,或者直接输出指定的字符串。Linux 的echo命令,在 shell 编程中极为常用, 在终端下打印变量value的时候也是常常用到的。当echo使用-e参数时,允许使用转译字符,如/t是插入一个制表符。

文件恢复

在编辑文件的过程中,Vim 将会在当前目录中自动生成一个以.swp结尾的临时交换文件,用于备份缓冲区中的内容,以便在意外退出时可以恢复之前编辑的内容。

完成编辑并保存退出后,临时交换文件会被删除;但如果 Vim 意外退出,那么这个临时文件就会留在硬盘中。当 Vim 再次启动时,会检查当前目录中是否存在交换文件。如果存在,则意味着 Vim 正在编辑此文件(他人正在编辑),或者在上次编辑过程中意外退出,这时Vim就会给出警告信息,并要求我们在以下四个选项中做出选择:

  • Open Read-Only(以只读方式打开):如果我们想要查看文件内容或是有另一个编辑过程正在运行,那么可以选择此选项;

  • Edit anyway(编辑文件):请尽量不要选择此选项。因为如果同时有两个或是多个编辑过程同时编辑一个文件,那么只有最后一个保存的编辑过程有效;

  • Recover(恢复):如果在编辑过程中vim意外退出,那么可以选择此选项尝试从交换文件恢复文档;

  • Quit(退出):选择此选项,将取消对此文件的修改。

已知 Vim 意外退出后,使用-r参数直接恢复编辑。如vi -r filename

Help poor children in Uganda !

vim 是一款开源免费软件,不会向用户索取任何费用。但会建议用户向荷兰 ICCF 捐款,用于帮助乌干达的艾滋病患者:vim 的启动页会显示 「 Help poor children in Uganda! 」 的字样,中文版本中则是 「 请帮助乌干达的可怜孩童!」。

这是因为 vim 的作者 Bram Moolenaar 去过乌干达开展志愿服务,在那里他了解了当地儿童的极端生存状况,知道他们需要帮助,于是 Bram Moolenaar 回国后与其他人一起创立了 ICCF 基金会。通过 vim 捐赠的善款将全部用于帮助改善乌干达的儿童的饮食、医疗、教育等状况。

在命令行模式中输入 help uganda可以看到详细的捐款方式。

变量用法与规定

#!/bin/bash
MY_NAME="zhang zi hao"
echo "you name is $MY_NAME"

#可以将命令执行后的输入结果赋值给一个变量

#!/bin/bash
LIST=$(ls)
SEVER_NAME=$(hostname)

假设执行 ./test.sh a b c 这样一个命令,则可以使用下面的值来获取对应参数:

  • $0对应 "./test.sh" 这个值。如果执行的是 ./work/test.sh, 则对应 ./work/test.sh 这个值,而不是只返回文件名本身的部分(脚本中则代指所有域)

  • $1会获取到 a,即$1对应传给脚本的第一个参数

  • $2会获取到 b,即 $2 对应传给脚本的第二个参数

  • $3会获取到 c,即 $3 对应传给脚本的第三个参数。$4、$5 等参数的含义依此类推

  • $#会获取到 3,对应参数个数,统计的参数不包括$0

  • $@会获取到"a" "b" "c",也就是所有参数的列表,不包括$0

  • $*也会获取到"a" "b" "c", 其值和$@相同。但 "$*" 和 "$@" 有所不同。$*把所有参数合并成一个字符串,而 "$@" 会得到一个字符串参数数组

  • $?可以获取到执行./test.sh a b c命令后的返回值。在执行一个前台命令后,可以立即用 $?获取到该命令的返回值。该命令可以是系统自身的命令,可以是shell脚本,也可以是自定义的 bash 函数

输入与输出 I/O

#!/bin/bash
read -p "please enter your name:" NAME    #-p 指定读取时的提字符
echo "your name is $NAME"

逻辑与判断

数值与逻辑判断语法

var1 -eq var2        # true if var1 is equal to var2
var1 -ne var2        # true if var1 not equal to var2
var1 -lt var2        # true if var1 is less than var2
var1 -le var2        # true if var1 is less than or equal to var2
var1 -gt var2        # true if var1 greater than var2
var1 -ge var2        # true if var1 greater than or equal to var2
string1 != string2   # true if string1 not equal to straing2
-d FILE              # True if FILE is a directory
-e FILE              # True if FILE exists
-f FILE              # True if FILE exists and is a regular file
-r FILE              # True if FILE is readable
-s FILE              # True if FILE exists and is not empty
-w FILE              # True if FILE has write permission
-x FILE              # True if FILE is executable

if 语句

if [condition-is-true]
then
    command 1
    command 2
    ...
fi

#if-else
if [condition-is-true]
then
    command 1
elif [condition-is-true]
then
    command 2
fi

迭代与循环

#for 循环
for VARIABLE in ITEM
do
    command 1
    command 2
    ...
done

E.g.

#!/bin/bash
COLORS="red green blue"
for COLOR in $COLORS
do
    echo "the color is: $COLOR"
done

#while 循环
while [ COMMAND IS TRUE ]
do
    command 1
done

错误用法 [0 -eq 1] ;正确用法 [ 0 -eq 1 ]

命令的传递

符号作用

|

管道符:将前一个命令的输出作为第二个命令的参数。

||

逻辑或:前一个命令执行错误后第二个命令才会执行,否则第一个命令不执行。

&&

逻辑与:当前一个命令正确执行后才会执行第二个命令。

;

多命令顺序执行,命令之间无任何逻辑关系。

不是所有的命令都支持管道符,如ls类命令 (但可以配合xargs的命令使用)

E.g. 将cat的输出结果作为grep的参数,只打印出包含“alias”的行

[zhangzh@node01 ~]$ cat .bashrc
# .bashrc
#alias
alias cl='clear'
[zhangzh@node01 ~]$ cat .bashrc | grep alias
#alias
alias cl='clear'

E.g. 若 lab1 不存在,则创建 lab1

[zhangzh@node01 ~]$ ll
total 4
drwx------ 2 zhangzh ybj 4096 May  8 16:17 linux
[zhangzh@node01 ~]$ ls lab1 || touch lab1
ls: cannot access 'lab1': No such file or directory
[zhangzh@node01 ~]$ ll
total 4
-rw-r--r-- 1 zhangzh ybj    0 May  8 16:29 lab1
drwx------ 2 zhangzh ybj 4096 May  8 16:17 linux

E.g. 若存在 lab1 ,则删除 lab1

[zhangzh@node01 ~]$ ll
total 4
-rw-r--r-- 1 zhangzh ybj    0 May  8 16:29 lab1
drwx------ 2 zhangzh ybj 4096 May  8 16:17 linux
[zhangzh@node01 ~]$ ls lab1 && rm lab1
lab1
[zhangzh@node01 ~]$ ll
total 4
drwx------ 2 zhangzh ybj 4096 May  8 16:17 linux

E.g. 使用分号将编译命令和运行命令连接在一起,以便在编译成功后立即运行程序

gcc -o myprogram myprogram.c ; ./myprogram

Linux 文本三剑客

重定向符号

输入重定向符号

符号作用

命令 < 文件

将文件作为命令的标准输入

命令 << 分界符

从标准输入中读入,直到遇到分界符停止

命令 < 文件1 >文件2

将文件1作为命令的标准输入并将标准输出到文件2

输出重定向符号

符号作用

命令 > 文件

将标准输出重定向到文件中(清除原有文件中的数据)

命令 2> 文件

将错误输出重定向到文件中(清除原有文件中的数据)

命令 >> 文件

将标准输出重定向到文件中(在原有的内容后追加)

命令 2>> 文件

将错误输出重定向到文件中(在原有内容后面追加)

命令 >> 文件 2>&1

命令 &>> 文件

将标准输出和错误输出共同写入文件(在原有内容后追加)

MacBook-Pro:code zhangzh$ ls -l xx
ls: xx: No such file or directory
MacBook-Pro:code zhangzh$ cat 1.txt
cat: 1.txt: No such file or directory
MacBook-Pro:code zhangzh$ vi 1.txt
MacBook-Pro:code zhangzh$ cat 1.txt
MacBook-Pro:code zhangzh$ ls -l xx > 1.txt 
ls: xx: No such file or directory
MacBook-Pro:code zhangzh$ cat 1.txt
MacBook-Pro:code zhangzh$ ls -l xx 2> 1.txt 
MacBook-Pro:code zhangzh$ cat 1.txt
ls: xx: No such file or directory
MacBook-Pro:code zhangzh$ 

awk 命令

数据可以来自标准输入,一个或多个文件。支持正则表达

格式:

awk [options] 'script' var filename(s)

实例:

[zhangzh@node01 shell]$ cat student.dat 
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78

脚本通常由''""包含

[zhangzh@node01 shell]$ awk '$0 ~ /LuoHu/' student.dat     # ~ !~ 匹配与不匹配正则运算符
Mary    116018  LuoHu   W   65
[zhangzh@node01 shell]$ awk '/LuoHu/' student.dat 
Mary    116018  LuoHu   W   65
[zhangzh@node01 shell]$ awk '$0 !~ /LuoHu/' student.dat    
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Steven  116030  YanTian M   78

语句块通常由{}包含

[zhangzh@node01 shell]$ awk '{if ($5>80) print $1}' student.dat     
Tom
John

有无空格不影响脚本执行,但如果需要打印空格,可以使用" "包裹起来

[zhangzh@node01 shell]$ awk '{if($5>80)print$1}' student.dat 
Tom
John

指定某一位的字符,真则打印

[zhangzh@node01 shell]$ awk '/n/' student.dat 
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ awk '/^.....n/' student.dat 
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ awk '$0 !~ /^.....n/' student.dat 
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65

NR 表示记录数,执行过程中对应行号

[zhangzh@node01 shell]$ awk 'NR%2==1' student.dat     #打印奇数行
Tom     116001  FuTian  M   90
Mary    116018  LuoHu   W   65

语句块 BEGIN{ commands } pattern{ commands } END{ commands }

  • nlines 行数

BEGIN语句块 在awk开始从输入流中读取行 之前 被执行,这是一个可选的语句块,比如变量初始化、打印输出表格的表>头等语句通常可以写在BEGIN语句块中

END语句块 在awk从输入流中读取完所有的行 之后 即被执行,比如打印所有行的分析结果这类信息汇总都是在END语句块>中完成,它也是一个可选语句块

pattern语句块 中的通用命令是最重要的部分,它也是可选的。如果没有提供pattern语句块,则默认执行 { print } >,即打印每一个读取到的行,awk读取的每 一行都会执行该语句块

[zhangzh@node01 shell]$ awk '{nlines++} END{print nlines}' student.dat
4

E.g. 仅复制文件

[daq@node02 anisotropie]$ ll | awk '!/^d/' | awk '{print $9}' | xargs -I {} cp {} ../anisotropieLowEnergy/

sed 命令

stream editor 流编辑器,支持正则表达,功能非同凡响。处理模式为逐行读入输入文件,并通过执行sed命令或者脚本的操作,然后处理后的结果输出到标准输出。

格式

sed [options] 'commands' filename(s)
sed [options] -f scriptfile filename(s)

指定显示

[zhangzh@node01 shell]$ cat test.dat     
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ sed -n '2,3p' test.dat     # -n:仅显示处理后的结果
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
[zhangzh@node01 shell]$ sed -n '/11/I p' test.dat  # I:按包含字符打印(忽略大小写)
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ sed -n '/^Mary/p' test.dat # ^:打印以Mary开头的行
Mary    116018  LuoHu   W   65
[zhangzh@node01 shell]$ sed -n '/00/I p' test.dat 
Tom     116001  FuTian  M   90
John    116005  NanShan M   85

替换删除

[zhangzh@node01 shell]$ sed 's/11/22/g' test.dat   # s:替换指定字符;g:全文逐行替换
Tom     226001  FuTian  M   90
John    226005  NanShan M   85
Mary    226018  LuoHu   W   65
Steven  226030  YanTian M   78
[zhangzh@node01 shell]$ sed '4c\Key' test.dat     # c\:将选定行修改为新文本
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Key
[zhangzh@node01 shell]$ sed '1d' test.dat 
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ sed '1,3d' test.dat       # d:删除选中行
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ cat test.dat 
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ sed -i '1,3d' test.dat    # -i:直接编辑文件(不可撤回)
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ cat test.dat 
Steven  116030  YanTian M   78

指定插入

[zhangzh@node01 shell]$ sed '4a\Key' test.dat     # a\:在第4行后添加文本
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78
Key
[zhangzh@node01 shell]$ sed '4i\Key' test.dat     # i\:在第4行前添加文本
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Key
Steven  116030  YanTian M   78
[zhangzh@node01 shell]$ sed -e 4i\Key test.dat    # -e:以脚本运行
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Key
Steven  116030  YanTian M   78

匹配标记

[zhangzh@node01 shell]$ sed 's/\w\+/[&]/g' test.dat 
[Steven]  [116030]  [YanTian] [M]   [78]
[zhangzh@node01 shell]$ sed 's/^Steven/&s/g' test.dat 
Stevens  116030  YanTian M   78
[zhangzh@node01 shell]$ sed 's/YanTian/&s/g' test.dat 
Steven  116030  YanTians M   78

组合表达

[zhangzh@node01 shell]$ echo hello world | sed 's/hello/hellow/g' | sed 's/world/word/'
hellow word
[zhangzh@node01 shell]$ echo hello world | sed 's/hello/hellow/g ; s/world/word/'
hellow word

正则表达

[zhangzh@node01 shell]$ cat test.dat 


Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78


[zhangzh@node01 shell]$ sed '/^$/d' test.dat #删除空白行
Tom     116001  FuTian  M   90
John    116005  NanShan M   85
Mary    116018  LuoHu   W   65
Steven  116030  YanTian M   78

引用

如果表达式内包含字符串变量,则需要用双引号

[zhangzh@node01 shell]$ test=hello
[zhangzh@node01 shell]$ echo hello WORLD | sed "s/$test/HELLO/"
HELLO WORLD

grep 命令

可以抓取你想要的字符,grep 命令可以抓取字符所在行的文本。常用的参数有:

-i忽略大小写进行匹配;

-v反向查找,只打印不匹配的行;

-r递归查找。可以同时打印文件名和所在行内容,这在使用通配符查找时很有用。

[zhangzh@node01 ~]$ cat .bashrc
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; 
then
    . /etc/bashrc 
fi
# User specific environment
if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]
then
    PATH="$HOME/.local/bin:$HOME/bin:$PATH"
fi
export PATH
#ROOT environment
source /home/software/cern_root/root_install/bin/thisroot.sh
#alias
alias cl='clear'
[zhangzh@node01 ~]$ cat .bashrc | grep alias
#alias
alias cl='clear'

最后更新于