简单bash脚本
通过阅读本章,你将会了解到以下几项内容。
- 理解脚本的概念
- bash进行判断和简单数学计算
- bash 的流程结构
- 实现把UC视频缓存变为普通视频文件的脚本
何谓 shell 脚本
我认为就是最初的程序员一条条命令写得太累了,能不能把每行命令都放在一个文本文件里,让shell自己来读取呢,这样脚本就诞生了。囊括了命令、函数、变量等内容,实现一条命令完成众多工作的功能,可以复杂到启动系统,也可用简单到只有一条命令,类似 Windows 下的批处理文件。
执行和调试
由于 shell 脚本都是文本,可以用任意编辑器打开,可当做bash
或zsh
等 shell 的参数来逐行直接执行,比如我们新建一个文本文件,内写上,
uname -a
,保存为myscript
。可以通过如下方式执行,
$\$$ echo 'uname -a' > myscript
$\$$ bash myscript
Linux litianci-PC 4.15.0-29deepin-generic $\#$31 SMP Fri Jul 27 07:12:08 UTC 2018 x86_64 GNU/Linux
解释
- 第1行,创建
myscript
文件。
- 第2行,执行该脚本文件
- 第3行,输出结果。
另外,也可用给脚本加上可执行权限,直接运行。通常在脚本第一行#!/bin/bash
告诉 shell 使用 /bin/bash
执行该脚本。对于使用Python
或者R
语言等执行的脚本,相应的把/bin/bash
换成相应的脚本解释器Python
或者RScript
等。
在 shell 脚本中,使用 #
表示单行注释,也就是从 #
到行尾的内容为注释内容。当然,有些#
属于字符串的内容或者其他语法格式,不表示单行注释。如果你使用vim
等编辑器打开脚本的时候,会发现注释的颜色是跟其他部分不一样的。通常在脚本第二行开始该脚本的功能注释,也可以添加作者、编辑信息等,然后另起一行注释该脚本的名称。空一行,开始脚本正文内容。如下面所示,
#!/bin/bash
# 本脚本实现 UC 浏览器视频缓存内容转换为一个完整的 MP4 文件
# ucvideo
if [! -e $2]
echo "请按照如下格式调用该脚本"
fi
写完脚本,保存后,一般使用chmod u+x ucvideo
的方式,给该脚本添加可执行权限。这样,可以像普通命令那样直接调用该脚本了。
$ ./ucvideo
当然,跟其他程序类似,脚本不可避免的要调试纠错,下面几种方法可能有用,
- 注释掉某些内容,方法就是在行首加
#
;
- 使用
echo
输出相关参数信息或者其他需要显示的内容。
- 使用
bash -x myscript
,会输出每行命令及执行结果,对于循环或者分支判断语句,可以告诉你具体执行了那些内容。
当然,最主要的还是要做到代码整洁,及时给自己的代码注释,避免后面自己都忘记咋回事了。
shell 变量
为了存储一些输出结果,或者一些参数等,需要用到变量存储,方便脚本书写。对于内容偏大的临时结果,也可用使用文件存储。通常采用如下方式,
NAME=value
变量名NAME
类似C语言的变量名规则,只可数字字母下划线,数字不可开头,区分大小写,中文不能出现在变量名中。对于变量值value
则没有太多要求。通常是字符串、数字等,可以包含中文。比如,
HOME="中国"
e=2.7
对于命令的输出结果,通常采用$(command)
和`command`
的方式实现。比如,
MACHINE=`uname -n`
TODAY=$(date)
如果想获取变量的数值,可以使用$NAME
的方法。
echo $MACHINE
第8章,简要介绍了$
,`
,*
,!
等特殊字符。在脚本中有时需要他们的特殊功能,有时候需要他们保持原样,该怎么做呢,通常使用双引号""
,单引号''
,以及反斜杠\
,看下面例子。
$ echo $HOME
/home/litianci
$ echo "$HOME , today is `date`"
/home/litianci,today is 2018年 11月 16日 星期五 21:25:46 CST
$ echo '$HOME , today is `date`'
$HOME , today is `date`
$ echo \$HOME \`date\`
$HOME `date`
特殊字符可以使用\
转义为本来样子,直接输出即可。如果作为字符串,使用单引号'
则保持原样不变,使用双引号"
则实现转义。这在其他语言中也有类似做法。
特殊变量
作为脚本,作为命令来用时,不可避免的要传入参数,在脚本中,通常使用$0,$1,...,$n
的方式来获取这些参数值。其中 $0
,表示本脚本;$n
,表示第n
个输入参数。不管是bash myscript
还是./myscript
方式执行脚本,上述$n
(\(n!=0\))都是一样的。另外$#
表示共有多少个参数,$@
保存着正行的输入。$?
显示上一个命令的返回状态,一般返回0
表示正常,其他数值表示异常或错误。看下面例子,脚本myscript
的内容为。
#!/bin/bash
# 测试这些特使变量
# myscript
echo "第一个参数为: $\$$1, 第二个参数为 $\$$2."
echo "共有 $\$$$\#$ 个参数"
echo "这些参数是 $\$$@"
echo "该脚本名称为: $\$$0."
分别执行bash myscript first second
和./myscript first second
,结果如下,
$ bash myscript first second
第一个参数为: first, 第二个参数为 second.
共有 2 个参数
这些参数是 first second
该脚本名称为: myscript.
$ chmod u+x myscript # 设置文件执行属性
$ ./myscript first second
第一个参数为: first, 第二个参数为 second.
共有 2 个参数
这些参数是 first second
该脚本名称为: ./myscript.
$ echo $?
0
执行时输入参数
$n
(n>0
)一般都是执行前输入的参数,有时候还需要在执行中跟用户交互,那就用到read
命令了。新建脚本readscript
内容如下,
#!/bin/bash
# 测试交互信息
# readscript
read -p "你叫啥名字,几岁啦?(两个答案请用空格隔开)" name age
echo "我知道啦,你叫 $name, $age 岁了。"
执行该脚本,
$ bash readscript
你叫啥名字,几岁啦?(两个答案请用空格隔开)深度易经 3
我知道啦,你叫 深度易经, 3 岁了。
关于read
命令的更多内容,比如输入密码,或者其他信息,请read --help
查看。
其他需求的参数
有些脚本,输入的参数可能有默认值,使用var1=${var2:-defaultvalue}
的方式,意思是变量var2
如果存在赋值给var1
,否则,把defaultvalue
赋值给var1
。
$ Birthday="2018-11-11"
$ Birthday=${Birthday:-`date`}
$ echo $Birthday
2018-11-11
$ Birthday=${DefaultDate:-`date`}
$ echo $Birthday
echo $Birthday
2018年 11月 19日 星期一 21:48:35 CST
$ Name=${Name:-'尚未设置'}
$ echo $Name
尚未设置
有时,我们需要对参数再处理,比如对于路径的不同取舍,对某些合并参数等的提取,可能使用其他正则表达式截取部分字符串更合适,不过 Shell 还是提供了一些简单的截取功能。
${var#pattern}
:从头删除满足匹配模式pattern
的最短子字符串。
${var##pattern}
:从头删除满足匹配模式pattern
的最长子字符串。
${var%pattern}
:从尾删除满足匹配模式pattern
的最短子字符串。
${var%%pattern}
:从尾删除满足匹配模式pattern
的最长子字符串。
$\$$ readme=/home/litianci/deepin-bible/Readme.md
$\$$ file=$\$${readme$\#$$\#$*/}
$\$$ echo $\$$file
Readme.md
$\$$ dir=$\$${readme%/*}
$\$$ echo $\$$dir
/home/litianci/deepin-bible
$\$$ stringchar="--folder=ucvideo"
$\$$ option=$\$${stringchar%=*}
$\$$ echo $\$$option
--folder
$\$$ value=$\$${stringchar$\#$*=}
$\$$ echo $\$$value
ucvideo
简单计算
bash 认为所有的输入都是字符串或者文本,如果你想让bash理解为数字,貌似只可以整数,就得声明,比如使用let
,expr
,bc
等方式。
Total=1024
let div=$Total/8 # let表达式中间不可以有空格,所有参数必须为整数
div=`echo "$Total / 8" | bc` # bc对空格不敏感,可以有小数,但是结果还是取整
div=`echo "$Total / 9.8 " | bc`
div=`echo "$Total/9.8" | bc`
div=`expr $Total / 8` # expr 必须有空格,所有参数必须为整数
echo $RANDOM # random 生成随机数
同时使用((statement))
也可用实现简单的数学语句,
i=0
((i++))
echo $i
echo $((i++))
echo $((++i))
((i=i+10))
echo $i
其中i++
跟++i
实现i
数值加一,这两者区别类似C语言的规定,i++
是先使用后递增一,++i
是先递增一再使用。
shell 脚本的三大结构
学过编程语言的,应当多多少少都知道结构化编程语言的三大结构:顺序、分支和循环。shell 脚本支持这三种结构的。顺序执行就是逐行执行的命令,这里略过,下面介绍分支结构的语法。
分支结构的语法
if then
语句
语法结构如下,
if [ condition1 ] ; then
statement1
elif [ condition2 ] ; then
statement2
else
statement3
fi
解释
[ condition1 ]
这里是测试语句,用于测试条件是否满足,满足则执行statement1
语句。
elif
是else if
的意思,用于多个测试条件。
- 其中
elif
和else
部分可以不要。
fi
其实是if
的倒序写法。后面的case
的结尾语句esac
是同样的处理方式。
touch emptyfile
if [ ! -s dsffyfile ] ; then
echo "emptyfile 是一个空文件"
fi
解释
[ ! -s emptyfile ]
中-s
判断一个文件存在且非空是为真,!
是逻辑运算取反的意思。
- 测试条件,需要注意
[]
以及各个选项、运算符、参数均用空格
隔开。
- 关于测试条件的更多信息,可以使用
help test
命令查看。
常见测试语句含义(help test
截取)
-a FILE |
True if file exists. |
-b FILE |
True if file is block special. |
-c FILE |
True if file is character special. |
-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. |
-g FILE |
True if file is set-group-id. |
-h FILE |
True if file is a symbolic link. |
-L FILE |
True if file is a symbolic link. |
-k FILE |
True if file has its `sticky’ bit set. |
-p FILE |
True if file is a named pipe. |
-r FILE |
True if file is readable by you. |
-s FILE |
True if file exists and is not empty. |
-S FILE |
True if file is a socket. |
-t FD |
True if FD is opened on a terminal. |
-u FILE |
True if the file is set-user-id. |
-w FILE |
True if the file is writable by you. |
-x FILE |
True if the file is executable by you. |
-O FILE |
True if the file is effectively owned by you. |
-G FILE |
True if the file is effectively owned by your group. |
-N FILE |
True if the file has been modified since it was last read. |
FILE1 -nt FILE2 |
True if file1 is newer than file2 (according to modification date). |
FILE1 -ot FILE2 |
True if file1 is older than file2. |
FILE1 -ef FILE2 |
True if file1 is a hard link to file2. |
-z STRING |
True if string is empty. |
-n STRING |
True if string is not empty. |
STRING |
True if string is not empty. |
STRING1 = STRING2 |
True if the strings are equal. |
STRING1 != STRING2 |
True if the strings are not equal. |
STRING1 < STRING2 |
True if STRING1 sorts before STRING2 lexicographically. |
STRING1 > STRING2 |
True if STRING1 sorts after STRING2 lexicographically. |
-o OPTION |
True if the shell option OPTION is enabled. |
-v VAR |
True if the shell variable VAR is set. |
-R VAR |
True if the shell variable VAR is set and is a name reference. |
! EXPR |
True if expr is false. |
EXPR1 -a EXPR2 |
True if both expr1 AND expr2 are true. |
EXPR1 -o EXPR2 |
True if either expr1 OR expr2 is true. |
arg1 OP arg2 |
Arithmetic tests. OP is one of -eq, -ne, -lt, -le, -gt, or -ge. |
&& ||
条件语句
该条件语句,是if..then
单条语句的简写,语法结构如下,
[ condition ] && statement1 || statement2
其中[ condition ]
跟if
后的判断条件一致,&&
后的语句 statement1
是条件满足的执行语句,||
后的语句statement2
是条件不满足执行的语句。且&&
和||
可以根据需要略掉。
[ -s emptyfile ] || echo "emptyfile 不存在或者是空文件"
[ -e ucdir ] && echo "可以继续执行" || echo "不存在该文件夹,请核实后继续"
case
条件语句
类似C语言的switch
分支语句,语法结构如下,
case $var in
'value1')
{ statement1 }
;;
'value2')
{ statement2 }
;;
*)
{ statement3 }
;;
esac
解释
$var
表示输入的某个变量,或者一个运行结果,
*
表示其他都不满足的执行语句。
case $destination in
'Shanghai')
echo "你想去上海啊!那边风景不错。"
;;
'北京')
echo "北京天气不好,雾霾多!"
;;
*)
echo "不允许去其他地方!"
;;
esac
上面语句实现了对目的地的简单判断,支持汉字输入。
for...do
循环语句
类似C语言的for
循环,语法结构如下,
for i in array ; do
statement
done
实现使用参数i
遍历数组array
的所有子元素,并对每个i
执行statement
语句。看下面例子,
for i in {1..5} ; do
echo $i
done
会输出如下结果,
1
2
3
4
5
再看几个例子,
for file in `ls` ; do
echo $file
done
for destination in 成都 上海 北京 广州 徐州 ; do
echo 我很想去$destination
done
对于循环,除了使用{m..n}
列出从m
到n
的数字外,还可以用下面这种方式,关于(())
作为数学计算的介绍,上面已经提及,这里不再赘述。
for ((i=1; i <= 5 ; i++)) ; do
echo $i
done
while..do
和until..do
循环语句
可能我们更熟悉while
语句,跟其他C系列的语言相似,语法结构如下,
while condition ; do
statement
done
比如下面这段代码,实现了按序输出i
i=0
while ((i<5)) ; do
echo $((++i))
done
until
的语法跟while
类似,语法结构如下,
until condition ; do
statement
done
只不过until
条件是不满足才循环,相当于while
的条件condition
取反。不再举例子了。
流编辑器sed
流编辑器sed
还算常用,操作方式跟vim
有相似之处,下面摘抄几个例子,详细内容请参考man sed
或者网上搜索vim sed
相关资料。
sed 's/Windows/Linux/g' deepin.txt > ok\_deepin.txt
如果你常用vim
会发现,当光标在某一行,在正常模式下输入:s/Windows/Linux/g
实现该行所有的Windows
被Linux
替代。而sed
命令会逐行执行,也就实现了对全文的替换。
shell脚本例子:转换 UC 缓存视频
阿里宝卡正当时,UC 看视频免流量是个多么大的诱惑。当然一些缓存视频,也是相当不错的。一般都会存在于 ./UCDownloads/videodata 文件夹下。文件一般是从0
开始排序,顺序增加,可达几百个文件。另外还有一个index.m3u8
文件。但是,因为网络协议对视频文件的第一要求是及时传达,而不是完整传达,导致部分视频文件丢失,甚至为空,所以需要妥善处理这些文件。
首先,我们需要知道如何把若干个视频文件转化为一个完整的视频文件,参阅网页,我们得知,
如果存在文件file.txt
,其内容为
file '1'
file '2'
则,
ffmpeg -f concat -i input.txt -c copy output.mp4
即可把这些文件给转换为 MP4 格式的单个文件。
生成 file.txt
文件
因为缓存文件都是数字,且文件夹内还有其他文件,包括 index.m3u8
的文件。
$ ls -1v +([0-9]) > file.txt
解释
ls -1v
中v
表示按照把文件按照数字的大小排序,1
表示按行显示。
+([0-9])
只选择纯数字的文件。
对file.txt
文件再处理,生成每行类似file '1'
的样式。
$ sed "s/.*/file '&'/" file.txt > file2.txt
$ mv file2.txt file.txt
解释
- 第一行实现对每一行行首行尾分别添加
file '
和'
内容。
- 第二行,重命名,替换掉中间文件。
参考网页:
生成 MP4 文件
前提需要你安装ffmpeg
软件,如果没有,命令行sudo apt-get install ffmpeg
安装。
$ ffmpeg -f concat -i file.txt -c copy film.mp4
做成一个 bash 脚本
下面是完整的代码
#!/bin/bash
# 本脚本实现 UC 浏览器视频缓存内容转换为一个完整的 MP4 文件
# ucvideo
echo "语法: $0 <UC浏览器视频缓存文件夹> <输出文件>"
output='ucvideo.mp4'
output=${2:-$output} # 读第二个参数作为输出文件
if [ -e $output ]
then
echo "已经存在 $output 文件,请更改输出文件名字!"
exit 1
fi
ucdir=${1:-'.'} # 第一个参数作为缓存文件夹,默认为当前文件夹
if [ ! -d $ucdir ]
then
echo "$ucdir 文件夹不存在!"
exit 2
fi
cd $ucdir
find ./ -size -1b -exec rm {} \; # 删除空文件
ls -1v | grep -E '^[0-9]+$'| sed "s/.*/file '&'/" > file.txt # 把数字文件按序,并加上行首行尾,写入file.txt
ffmpeg -f concat -i file.txt -c copy $output
if [ ! $ucdir -ef $OLDPWD ];then
mv $output $OLDPWD
fi
当然为了方便直接使用,把他放在/usr/local/bin/ucvideo。
并为之添加执行权限
$ chmod u+x /usr/local/bin/ucvideo
当你从手机上复制缓存文件夹过来,就可以
$ ucvideo /<指向缓存文件夹>/ <电影名称>.mp4
期间发生了一个小问题,bash命令在脚本和终端下运行不一致。比如ls -1v +([0-9])
可以在终端下运行,但是在脚本里就会报错。
bash <<<'ls -1v +([0-9])'
是无法运行的。所以上面解决方案就出现了部分调整。
也就是说一般在 deepin 下,sh
或者/bin/sh
指向/bin/dash
,可以通过sudo dpkg-reconfigure dash
关闭dash
,改为默认的/bin/bash
。其中dash
跟bash
还是存在一些小差异。
不过,通过echo $SHELL
和查看/etc/passwd
文件,看当前用户的shell,均为bash
。但是在终端默认的shell可以执行ls -1v +([0-9])
,在脚本里使用/bin/bash
执行/bin/bash <<<'ls -1v +([0-9])'
执行就是报错。原因待查。
后来无意间在unixstackexchange发现,是没有启用glob
扩展,在该行代码前一行加上shopt -s extglob
即可。
关于glob
的更多介绍,
总结
本章简要介绍了bash的若干语法结构,并给出了一个小例子。