bash
7. 脚本¶
7.1. 介绍¶
从这一章开始,我们开始放飞自我,根据你问什么问题来讲什么问题。
这一章我们来理解一下什么是脚本。
早期的计算机普遍都是命令行的,因为显示一些字符串需要的数据比较少,如果显示复杂的图形,就需要很多的数据了。
早期的计算机处理能力不强,处理不了那么多数据,所以尽量都是输入字符,输出字符。所以早期的人机接口都是字符串。甚至早期我们玩的游戏都是字符串的。如果你要删除一个文件,你运行::
rm myfile
这样我们只是传进入十几个字节的字符串,如果计算机没有删除成功,只要给你返回十几个字节::
myfile cannot be deleted
这很容易就搞定了,但如果要显示给你一个这样的窗口:
这个就不是十几个字节的事情了。
所以,计算机人机接口的历史就是从命令行发展过来的。到了现在,这种十几个字节的事情已经不是问题了,大部分时候,人们不在乎用几百兆的数据去进行人机交互。但命令行还是很重要的,因为有时我们会遇到很多极端的环境,比如我们需要通过一条卫星通道去控制我们计算机,那就很难通过卫星给你传递很大的数据了。就算不是卫星,如果我们从宿舍去控制我们学校机房的计算机,我们也希望越快越好,这时,命令行就很重要了。
所以这就是为什么我一开始先不教你用图形界面,而是用命令行,因为那个是最后的保证,会命令行你一定能找到方法搞定那个图形界面的,但会图形界面,你不一定能用好命令行。
但省大小不是命令行一直流传下来的关键原因,关键原因是:命令行可以编程序。比如,你要删除的不是一个文件,你要删除100个文件,你可以这样写这些命令::
for file in `ls myfile*.cc`
if test -e $file
rm $file
你看,这个和你用C写代码,是一样的。命令行这个好处,是你用图形界面怎么都搞不定的,图形界面没法自动化,但命令可以自动化:写一个简单的程序就可以了。
正如我们一开始说的,作为一个程序员,我们就很少把重复的事情做很多很多遍了,所有有可能要重复,写起来很麻烦的事情,我们就都写成脚本。这样有两个好处:
下次运行什么都不用记了,直接运行脚本就可以了。
脚本本身变成了一个笔记,要查笔记看脚本就行了。
我举个例子,假设你每次编译完你的程序,你都要拷贝到windows的目录下,你每次都要运行这些命令::
cp myapp /mnt/c/用户/qing/编译好的应用程序/
cp myapp2 /mnt/c/用户/qing/编译好的应用程序/
cp myapp3 /mnt/c/用户/qing/编译好的应用程序/
那么你就可以打开一个文本文件(比如就叫cp.sh),把这些命令都写进去,然后你每次要做这些动作的时候,直接运行::
sh cp.sh
(sh是一个shell,我们用这个shell去运行cp.sh这个“脚本”。)
就可以了。这个cp.sh,就是一个“脚本”。以后如果你不记得你文件都放在哪里了,打开这个文件也可以知道了,就不会出现不记得的情况。
学习脚本首先要学习命令,我上次给你那本大部头,你要对着操作一次,才知道这些命令。多用几次就会记住怎么用了。至于生癖的命令,可以到用的时候再去查。
那些for啊,if啊,其实也是命令,这些都需要学。那个我这里讲不完,你要自己去用才知道。
我这里只给你一个Cheatsheet::
ls # 列出当前目录下的文件
ls -l # 用long的方式列出当前目录的文件和它们的属性
cp file1 path # 把file1拷贝到路径path
cp -a path1 path2 # 把path1中的所有文件拷贝到path2中
cp file1 path/file2 # 把file1拷贝到path中,名字改成叫file2
rm -Rf path1 # 删除path1中的全部文件
rm file1 file2 file3 # 删除文件file1, file2, file3
mv file1 file2 # 把file改名叫file2
mv file1 file2 path # 把file1和file2移动到path目录中
mkdir path # 创建一个叫path的目录
cd path # 当前目录移动到path
cd .. # 移动到当前目录的上一级目录
sh file # 用sh(就简单的shell)运行file这个脚本
. file # 用当前shell运行file这个脚本
exit # 退出当前shell
adduser qing # 添加用户qing
deluser qing # 删除用户qing
passwd qing # 修改qing的密码
passwd # 修改当前用户的密码
chmod +x file # 让file变得可执行
chmod -x file # 让file变得不可执行
chmod o-x file # 让file对于other的人不能执行
chmod u+x file # 让file对于file的所有人可以执行
man ls # 看ls的手册
man -k user # 查找有哪些包含user这个关键字的的手册名字,如果你隐约记得有一个命令,但记不住确切的名字,可以用这个方法找
其他的等你问我再补充吧。
在Unix世界中,和你交互的那个界面,叫一个shell,表示它是操作系统的“外壳”,shell有很多种,图形界面也是一种shell。最传统的shell叫sh,功能很简单,我们平时用得比较多的,是bash,你的Windows上的ubuntu默认就是这个Shell。其他的还有csh,tsh等各种shell。windows也有自己的命令行的PowerShell,它们每个语法都不同,我建议你先从bash shell入门。其他的shell基本语法很接近,只是高级语法不同。反正所有计算机语言都是这样,先学一种语言,需要的时候再学一种新的,多学几种以后,大部分套路就都知道了。
脚本也可以像命令一样运行。这有两个条件:
在脚本最前面加上这一句::
#!/bin/bash这是为了保证操作系统知道你要用哪个shell去执行你的脚本
用chmod +x命令把这个文件修改成可执行的。
这样以后,你的脚本就可以用这种办法运行了::
./my_script.sh
路径是必须的,因为Linux和Windows不同,Linux不认“当前目录的可执行文件”的,你运行一个命令,如果不在PATH这个环境变量中声明路径,它是不会找当前路径的。
上一章我们为了让你的VS Code找到gcc的安装位置,我们就修改了Windows的Path环境变量了。
那到底什么是环境变量呢?
还记得我们之前说过的“库函数”吗?你调用cout >> “Hello world”,调用的就是库里面的函数,假设,我们的cout库支持打印不同的颜色,但你这个调用没有指明颜色,我们有什么办法让这个库知道你要显示什么颜色呢?
为了解决这个问题,Shell通过操作系统给你的程序的内存里面放了一组预定的变量,比如可能是这样的::
PATH=/bin;/sbin;/usr/bin
COUT_COLOR=RED
LC_ALL=zh_CN.GB18030
...
这样,你这个cout的库可以从约定的位置读一下,就可以获得这种参数了。在bash shell中,你运行env,就可以找到所有的环境变量,你可以运行export COUT_COLOR=BLUE这样设置新的变量,如果你要固定设置下来,就可以好像修改.vimrc那样,把这句话写到.bashrc中。
我们运行命令的时候,可以动态修改一个命令的环境的,比如你可以试试分别运行下面两个命令::
LC_ALL=zh_CN.GB18030 date
LC_ALL=C date
LC_ALL是个环境变量表示当前的语言,第一个命令表示现在是中文,所以date输出的就是中文,而第二个说用的是通用语言(这个C我也不知道是不是表示C语言),这样输出就是英文的。
当你运行一个Linux的命令,如果你写了路径,Shell就会从路径去找到这个文件来运行,但如果你没有写路径,Shell从PATH变量写的路径里找这个命令,如果找不到,那就是没有了,通常当前目录,是不写在PATH中的,你当然可以强行写进去,比如这样::
export PATH=.;/bin;/sbin;/usr/bin
my_script.sh
这样,如果你的my_script.sh在当前目录下,shell也是能找到的。但我建议你不要这样做,因为这是一个被证明的不安全的习惯。等你未来学计算机安全的时候,我们再来讨论这个话题吧。
7.2. bash例子¶
本章我们通过例子来学习一些脚本知识。例子的好处是它相当于切开一个问题的剖面,把很多知识连起来。我们可以通过这些例子了解到一些没想到的理念,没有接触过的命令,或者没考虑到的解决方案。
我们用bash做例子,一方面是bash的使用很广泛,学习bash可以直接用,另一方面它兼容sh,而sh是POSIX标准,所以很多基本原理都是和很多其他Shell是相通的。可以认为它代表了Unix看待问题的方法。现在有些Shell,比如微软的Power Shell,趋向于走另一个理念方向,它们会更接近Python这样的编程语言。这里核心的区别在于:传统的Unix Shell的理念是基于字符串的。而Power Shell是基于数据结构的。我认为这是两者最大的区别。
我们用Python来举例,在Python中,你也可以调用命令的,比如我们在bash上运行命令ls::
kenny@linux-desktop:~/work/cpp_aux_tutorial (main *)$ ls
01.rst 03.rst 05.rst 07.rst 09.rst 11.rst 13.rst 15.rst _build conf.py index.rst Makefile _templates
02.rst 04.rst 06.rst 08.rst 10.rst 12.rst 14.rst 16.rst codes _extensions LICENSE _static
用python你也能做一样的事情,我们启动python,在它的命令行上运行::
>>> import os
>>> os.system('ls')
01.rst 03.rst 05.rst 07.rst 09.rst 11.rst 13.rst 15.rst _build conf.py index.rst Makefile _templates
02.rst 04.rst 06.rst 08.rst 10.rst 12.rst 14.rst 16.rst codes _extensions LICENSE _static
这种情况下python也是一个shell,它也可以运行命令,处理命令。
你更愿意用前一个还是后一个呢?其实后一个更灵活,它可以使用各种函数,调用加减乘除,数组,向量等各种算法。但如果日常使用,其实我们更愿意使用第一个,因为第一个敲的东西少啊。
这就是区别,bash几乎没有复杂的数据结构的,它的所有数据结构都是字符串,命令也是字符串,输入输出也是字符串,你看见什么就是什么,这样,你知道命令输出什么就够了,根本不用懂具体的数据结构,这样学习成本就很低,所以,简单的自动化工作,用这种shell就是最方便的。复杂的时候,我们才会用Python类的Shell来写程序。
在Python这种shell里面,你一眼看过去,都不知道os.system(‘ls’)的输出是ls这个命令输出的,还是os.system()的返回值,你要学。bash没有这个问题,bash是你看见了,就是可以处理的。所以,在bash中,你要循环,就是这样的::
for i in `ls`; do
echo 找到了 $i
done
这个循环直接用ls的输出作为循环序列(bash自动用空格当分割符,这是可以改的,我们遇到再说),然后一行行输出“找到了xxx文件”。如果你要用Python写这样的东西,就要这样写::
for i in os.popen("ls").read().split():
print('找到了', i)
后面这个你首先得懂popen(这里用system不行,因为system没有返回值),知道read(),split()这些每个函数的用法以及它们的返回类型。虽然写bash脚本你也需要知道命令,但命令是你本来就要知道的。而且命令输出的字符串处理方法也就是那些,但函数返回的各种数据结构就没有那么简单了。
bash的这些特点,可以从后面的例子中慢慢体会。
7.2.1. 例1:切割图片¶
假定我们现在需要分割一个图片,把它转成NxM的多个小图片,我们已经查到了,ImageMagick的convert命令可以完成这个工作,比如我们有一个200x200的图片叫full.jpg,我们要分成平均切成4份,那么命令上我们可以这样::
convert -extract 100x100+0+0 full.jpg p1.jpg
convert -extract 100x100+100+0 full.jpg p2.jpg
convert -extract 100x100+0+100 full.jpg p3.jpg
convert -extract 100x100+100+100 full.jpg p4.jpg
这写成脚本,直接放在一个extract.sh中,你可以这样写::
#!/bin/bash
convert -extract 100x100+0+0 full.jpg p1.jpg
convert -extract 100x100+100+0 full.jpg p2.jpg
convert -extract 100x100+0+100 full.jpg p3.jpg
convert -extract 100x100+100+100 full.jpg p4.jpg
这是这个脚本最简单的写法,虽然用于,但至少你不用每次输错了还要改半天,改这个文件就行了。所以,在Unix下工作,基本上我们都用脚本,否则效率太低。其实这个工作,你直接用gimp手工一个个截图也行啊,但你应该也感觉到了,这没有命令行方便。
现在我们消除那些重复的东西,我们引入变量::
#!/bin/bash
CMD="convert -extract"
SOURCE_IMG=full.jpg
$CMD 100x100+0+0 $SOURCE_IMG p1.jpg
$CMD 100x100+100+0 $SOURCE_IMG p2.jpg
$CMD 100x100+0+100 $SOURCE_IMG p3.jpg
$CMD 100x100+100+100 $SOURCE_IMG p4.jpg
bash的变量很简单,就是xxx=yyy这样就可以了,但要注意,等号前后不能有空格。我们前面说过了,bash的所有处理都是字符串,你加个空格,它以为你就是要运行CMD这个命令呢,写bash程序,对于字符串怎么断开的,一定要非常敏感。这也是
使用变量就加个$在前面就可以了,这个在例子中我们已经看到了。
现在解决第二个问题:能不能换一个图片,不要每次都要修改脚本?这就涉及到命令行输入的问题了。bash有一些内置的变量,就是$0, $1, $2……这样的,表示命令行参数。比如你这样运行你的程序::
./extract.sh full.jpg p
那么,你的$0就是./extract.sh,$1就是full.jpg, $2就是p。这样,我们的程序就可以写成这样了::
#!/bin/bash
CMD="convert -extract"
$CMD 100x100+0+0 $1 $2-1.jpg
$CMD 100x100+100+0 $1 $2-2.jpg
$CMD 100x100+0+100 $1 $2-3.jpg
$CMD 100x100+100+100 $1 $2-4.jpg
不过这样,如果你没有输入参数怎么办呢?那你的命令就会变成这样::
convert -extract 100x100+0+0 -i -2.jpg
因为$1和$2都没有了,所以,一般我们会做一个检查::
if [ -z "$1" ]; then
echo "没有输入文件名"
exit -1
fi
if [ -z "$2" ]; then
echo "没有输出文件名前缀"
exit -1
fi
这里很有趣的地方是:[其实也是命令,if其实在判断[这个命令的返回值(所以后面要加空格。而且bash中,返回值0表示true,其他值表示false)。 而-z用来检查后面的字符串是不是空。所以,我们可以两个判断组合在一起,这样::
if [ -z "$1" -o -z "$2" ]; then
echo "用法:$0 输入文件 输出前缀
exit -1
fi
其中的-o表示or。同理-a表示and。
还要注意的是,这里的双引号不能省略也不能换成单引号。如果省略,这句话就变成::
if [ -z -o -z ]; then
了,这样-z就没有参数了。如果换成单引号,单引号的作用是不让解释变量,所以你得到的是::
if [ -z $1 -o -z $2 ]; then
而不是::
if [ -z full.jpg -z p ]; then
所以,也是不对的。
好,现在我们走一步大的,我们想更简单一点,最好可以这样运行命令::
./extract.sh full.jpg p 4x4
这样我们可以自动完成所有的循环。为此,我们需要知道图片的大小,我们知道ImageMagick里面的identify命令可以看图片信息,我们可以看看它的manpage,知道可以这样获得一个图的大小::
kenny@desktop> identify full.jpg
full.jpg JPEG 640x480 640x480+0+0 8-bit sRGB 68625B 0.000u 0:00.000
这里第三个单词就是它的大小,Linux有一个命令(awk)可以按空格分离第几项的,我们可以这样::
kenny@desktop> identify full.jpg | awk '{print $3}'
640x480
kenny@desktop> identify full.jpg | awk '{print $3}' | awk -Fx '{print $1}
640
kenny@desktop> identify full.jpg | awk '{print $3}' | awk -Fx '{print $2}
480
首先是这个”|”管道操作符,它把identify的输出作为awk的输入,awk后面是一个脚本,输出第三项,就得到640x480,然后我们再用x作为分隔符,输出第一和第二项,就得到具体的图片的宽和高了。现在我们可以通过这个命令来求图片的原始长度了::
SIZE=`identify $1 | awk '{print $3}'`
WIDTH=`echo $SIZE | awk -Fx '{print $1}'`
HEIGHT=`echo $SIZE | awk -Fx '{print $2}'`
其中这个反引号``表示把命令打印的东西全部当作变量的内容。这里我们先求SIZE,就是那个640x480,然后我们再把它拆开成640和480,得到宽和高。
一样的方法我们可以拆开那个4x4::
N=`echo $3 | awk -Fx '{print $1}'`
M=`echo $3 | awk -Fx '{print $2}'`
剩下的问题是怎么算小图的长和宽了,我们说过,bash只有字符串,没有其他类型,所以,$WIDTH,$HEIGHT这些值虽然看起来是个数字,其实是个字符串。要做计算,要不传给一个命令,比如bc::
kenny@desktop> echo 10+10 | bc
20
要不用bash的内置方法::
VALUE=$((10+10))
我们选后者,现在整个程序就可以这样写了::
#!/bin/bash
set -e
if [ -z "$1" -o -z "$2" -o -z "$3" ]; then
echo "用法:$0 输入 输出前缀 分解要求"
exit -1
fi
SIZE=`identify $1 | awk '{print $3}'`
WIDTH=`echo $SIZE | awk -Fx '{print $1}'`
HEIGHT=`echo $SIZE | awk -Fx '{print $2}'`
N=`echo $3 | awk -Fx '{print $1}'`
M=`echo $3 | awk -Fx '{print $2}'`
W1=$(($WIDTH/$N))
H1=$(($HEIGHT/$M))
for w in `seq 0 $(($N-1))`; do
for h in `seq 0 $(($M-1))`; do
SPEC=${W1}x${H1}+$(($w*$W1))+$(($h*$H1))
echo "extract $SPEC"
convert -extract $SPEC $1 $2-$w-$h.jpg
done
done
其中:
set -e表示脚本任何一个命令运行失败了,就退出
前面说过$XXX这样使用变量,但如果你要避免和后面的字符拼在一起了,可以这样写:${XXX},这里我们在处理convert -extrace的参数的时候就用了这样的语法。
这里我们用了一个seq命令,它可以产生一个序列,比如你写seq 1 5,它就会打印1, 2, 3, 4, 5。或者你写seq 1 10 2,它会一次加2,变成:1,3,5,7,9。
由于现在我们只写了一次convert命令,我们就不需要定义一个变量了
调试的时候,其实可以先去掉convet这个命令,先看那个echo输出的参数对不对,等参数正确了,才正式调用这些实际起作用的命令。
现在我们把这个脚本写在文本文件中(比如叫extract.sh),chmod +x extrace.sh,然后运行::
./extrace.sh myfile.jpg p 3x2
就可以得到需要的分割了。