鉴于shell的高效、通用,使用shell编写脚本实现日常使用的一些小功能。
处理生成文件的问题
强烈建议,在生成文件之前,先检测文件是否存在,如果存在就删除这个文件。对于使用管道符>
指定输出文件时,请一定先检查文件是否存在。现在许多程序运行可能出错,需要重新运行,但是用户基本不会手动删除先前生成的错误的文件,所以我们在输出到文件之前,要先检测之前是否存在
输出文件前,可以请使用下面的命令检测是否存在该文件。
#检测是否存在该文件,如果有,则自动删除旧版本
if [ -e $output ]; then
echo "之前存在文件$output,自动删除旧版本。"
rm -rf $output
fi
1.编写前必知的编写规范
#!/bin/bash
##debug
set -x #直接输出每次执行的命令
set -e #程序异常结束时候,输出错误。
1.1 shell脚本开头如上三行。
1.2 检查shell脚本可以使用shellcheck
,
sudo apt-get install shellcheck
shellcheck test.sh #检测test.sh的语法是否正确。
1.3 在bash,如果不加 local 限定词,变量默认都是全局的。
对于在函数内声明的变量,请务必记得加上 local 限定词。
1.4 trap函数的使用
对于程序结束的方式进行判断,如果正常结束,则执行命令1,错误则执行命令2,如果是Ctrl+C终止,可以执行命令3.
trap "echo end analysis" EXIT #程序退出时执行,无论是正常退出,还是错误退出。
trap "echo there have a error" ERR #出错时,执行
2. 基础shell语法
shell脚本的执行方式
方法1:bash test.bash
方法2:
chmod 757 test.bash
./test.bash
变量
shell变量:要求等号后面紧跟变量,不能有空格
例如:Name=Zhangsan
shell中双引号和单引号,如果引号内有变量,使用双引号。单引号内部的字符不会被转变为变量。
引用变量方式:$Name
或者${Name}
,一般推荐变量引用的时候,都加上大括号,不然后面某些时候,拼接字符串的时候会出现无法识别变量名的问题。
只读变量 readonly Name
上面的Name变量就变成只读变量,不能被修改。
删除变量 unset variable_name
局部变量 默认的变量都是全局变量
local age=18
字符串操作
拼接字符串
your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1
#输出内容
#hello, runoob ! hello, runoob !
查找字符串(使用正则expr index )
string="runoob bilideise site"
echo `expr index "$string" o` # 输出 4
获取字符串长度 ${#string}
string="abcd"
echo ${#string} #输出 4
提取字符串,第2:4个。
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo
bash中的index下标从0开始。0是第一个。
数组
array_name=(value0 value1 value2 value3)
或者单独定义array_name[4]=value4
获取数组第1个元素 ${array_name[0]}
获取数组所有元素 ${array_name[@]}
获取数组元素个数 length=${#array_name[@]}
获取某个元素的长度arr1_length=${#array_name[1]}
注释符号
单行注释#
多行注释
<<!
此处是区块注释
多行注释文本
多行注释文本
!
参数传递
传参分为$1,$2,……$n
代表传入的第1,2,n个参数。
$0
代表执行的文件(包括路径)
$#
传入参数的个数
$*
传入的所有参数(以一个单字符显示)
$@
传入的所有参数(依次显示每个参数)
$$
脚本运行的当前进程id
$!
后台运行的最后一个进程id
$?
显示命令最后退出的状态,如果不是0,则代表程序没有正常结束。
``
if [ -n "$1" ]; then
echo "包含第一个参数"
else
echo "没有包含第一参数"
fi
算术比较, 比如一个变量是否为0,[ $var -eq 0 ]
。
文件属性测试,比如一个文件是否存在,[ -e $var ]
, 是否是目录,[ -d $var ]
。
字符串比较, 比如两个字符串是否相同, [[ $var1 = $var2 ]]
。
运算符
算术运算符+
-
*
/
%
加减乘除整除取余
=
是赋值,==
和!=
仅用于数字比较。
运算符使用时,必须在中括号内,且两边均有空格。
例如:[ 5==5.0 ]
关系运算符(仅限于数字之间比较)
-eq
-ne
-gt
-lt
-ge
-le
依次是相等,不等,大于,小于,大于等于,小于等于
逻辑运算符
!
-o
-a
非;或;与
&&
与 ||
或
字符串运算符
=
!=
等于 ;不等于
-z
字符串长度为0,则返回true
-n
字符串长度不为0,则返回true
$
检查字符串是否为空,不为空返回true.
文件运算符
-d
检查文件是否是目录,是,则返回true
-r
检测文件是否可读
-w
检测文件是否可写
-x
检测文件是否可执行
-s
检测文件是否为空,不为空则返回true
-e
检测文件(或目录)是否存在,是,则返回true
-L
检测文件是否存在并且是一个符号链接
echo命令
输出换行echo -e "this is a new line! \n"
shell流程控制
shell里面else里面如果没有东西,就不要写else. 流程控制里不能为空。
if [ $age -ge 18 ] then
echo "成年人!"
else
echo “未成年!”
fi
多组if-elif-else-fi
if [ $age -lt 18 ] then
echo "未成年"
elif [ $age -ge 60 ] then
echo "老年人"
else
echo "青年和中年"
fi
每组的if后一定要有then。
循环
for循环
for var in `ls namefile`
do
echo $var
done
#for循环实例
for((i=1;i<=5;i++));do
echo "这是第 $i 次调用";
done;
#生成0到100的自然数。即0,1,2,3,……,100
for i in {0..100};do
echo "${i}.tar.gz"
done
while循环
int=1
while(( $int<=5 ))
do
echo $int
let "int++"
done
无限循环
while true
do
ping www.baidu.com
done
case
echo "输入1到4之间的数字"
read aNum
case $aNum in
1) echo "输入的是1"
;;
2) echo "输入的是2"
;;
*) echo "您输入的不在范围"
;;
esac
中断或者跳出循环的方法
break
终止后续循环
continue
中止当前循环,后续循环继续运行。
内部函数basename和dirname
basename
用于获取文件名
basename /softwares/test/test1.sh
返回值是test1.sh
basename -s .sh /softwares/test/test1.sh
返回值是test1
dirname
用于获取文件路径
dirname /softwares/test/test1.sh
返回的值是路径
上述2个函数可以用于获取用户输入的文件名的前缀和路径。
函数的定义
##提取指定染色体的bam文件
getchr(){
samtools view -b -h $1 $2 | samtools sort - > $3
}
#调用函数
getchr all.bam chr1 chr1.bam
函数里可以返回值
sum2(){
echo "success!"
return 0
}
一般情况下,return返回的只能是0-255的整数,默认和习惯,返回0是正常,1是错误,用来判断程序是否正常运行。
使用$?
可以获取上一条命令运行的结果,如果是0就正常。
if ! [ $? -eq 0 ] ;
then
echo "上一条命令有错误。"
exit
fi
return用于返回状态码,只能是数字0-255
##定义检测网络连接是否正常
check_net(){
#检测网络
ping -c 3 -w 3 www.baidu.com >network_cache
#-c ping 3次 ,-w 3 ping 3s 后结束
#检测ping的结果状态码
if [ $? -eq 0 ];then
echo "network connect success!"
echo "检测网络,正常!"
return 0
#sleep 3m #休眠3min后再检测
else
echo "network connect Wrong,Please check network!"
return 1
fi
}
#检测网络
check_net
#获取检测网络的返回值(即check_net函数里的return的值)
if [ $? -eq 0 ];then
echo "网络正常!" #此处可以放置,网络正常需要运行的命令脚本。
else
echo "网络未连接!" #此处可以放置,网络登录的命令脚本。
fi
设置参数的默认值
# 设置null或空值时,默认值是10
${number:=10}
#设置null或空值时,报错信息。
${name:?Please input name}
#设置参数不为空时的默认值
${userpasswd:+******}
read函数的使用
示例:用户协议同意的交互设计
###用户协议同意模块
echo "用户协议
1.仅限非商业使用(学术研究免费使用)
2.转载或二次开发,请保留原作者的信息
3.商业用途请联系原作者!"
##-n 1是限制只能输入1个字符,-p "……"用于给用户提示需要输入的内容
read -p "Please make sure to agree with the agreement (Y/N):" -n 1 answer
case $answer in
Y|y)
echo -e "\n Thank you!Install will continue……"
reset
;;
N|n)
echo -e "\n No agree!exit!"
exit
;;
*)
echo -e "\n Please make sure input Y or N"
exit
;;
esac
引入其他脚本
例如:本目录下有commerge和getchr两个脚本。
在getchr里可以使用source commerge
或者source ./commerge
,来引用脚本commerge.
获取输入参数的处理
#运行脚本命令
getchr -i file1 -o file2 -s std
getchr脚本里获取输入参数的方法,
while [ -n "$1" ];do
case $1 in
-i|--input)
inputfile=$2
shift
;;
-o|--output)
output=$2
shift
;;
-s|--select)
select=$2
;;
esac
shift
done
使用shift
作为左移工具,来获取后面的参数和参数的值。
3.实战1:编写两个文件合并的小教本
因时间仓促,写了简单的脚本,以其能够理解其原理和shell语法。脚本在github.下载后,直接运行命令commerge -h
即可。如果不能执行,请添加执行权限即可chmod 757 commer
。
单行命令速查
shell算术运算
num=`expr 12 - 6`
pi=`awk 'BEGIN{print 2*2-0.86 }'`
area=`awk -v pi=$pi -v num=$num 'BEGIN{print pi*num }'`
注意:整数型运算使用expr
而且后面的运算和数字之间要有一个空格。浮点运算需要借用awk来完成。awk
使用shell的变量时,需要使用-v
来引入。
4.高级功能
使用临时目录和临时文件,可以作为中转目录或文件。有些内容需要输出到文件,但是只是作为中间文件,最后需要删除,就可以用临时文件。
创建临时文件
##生成临时文件(示例:临时文件的变量名称是${tempfpkm})
trap 'rm -f "$tempfpkm"' EXIT #退出时,删除临时文件(trap是根据后面的系统信号,执行前面的命令)
tempfpkm=$(mktemp -p ${PWD}) || exit 1 #在工作路径,生成临时文件,如果生成失败,则退出。
创建临时目录
##生成临时目录(示例:临时文件夹变量名称是${fpkm})
trap 'rm -rf "$fpkm"' EXIT
fpkm=$(mktemp -d -p ${PWD}) || exit 1
mktemp
参数:
-p
指定生成文件或文件夹的路径
-d
指定生成的是文件夹,不使用-d
生成的是文件。
tmp.XXXX
制定模板(注意:X必须是大写,而且最少需要三个X,X有几个,输出的随机字符就有几位)
mktemp -d -p ${PWD} zhansan.XXXX
在当前目录创建文件前缀为zhansan.
的文件夹。此处输出为 zhansan.pmJt
每个人运行,后面的四位不一样
mktemp -p ${PWD} temp.XXXXXX
创建临时文件,前缀是temp,后面是6位字符。
创建临时文件夹,并在临时文件夹里创建临时文件
trap 'rm -rf "$zhangsan" ' EXIT
zhangsan=$(mktemp -d -p ${PWD}) || exit 1
temp=$(mktemp -p ${zhangsan} temp.XXXXXX)
trap是捕捉系统退出时执行的命令
trap 'rm -rf "${zhangsan}";rm -rf "${temp}"' EXIT
当有很多临时文件需要清理时:可以自定义清理函数,然后trap执行即可
#定义清理函数
function cleanup(){
rm -rf "$zhangsan"
rm -rf "$bam"
}
trap cleanup EXIT #trap信号调用清理函数
zhangsan=$(mktemp -d -p ${PWD}) || exit 1
temp=$(mktemp -p ${zhangsan} temp.XXXXXX) || exit 1
bam=$(mktemp -p ${PWD} bam.XXXX) || exit 1
上面生成的temp.XXXXXX 文件是在zhangsan.XXXX文件夹里的,通过清理zhangsan文件夹即可清理里面的文件。
bam.XXXX文件是在工作目录,需要指定清理。
trap是系统信号,所以只能执行一次就会退出。如果有多个文件需要清理,就只能是使用上面的函数模式或者是使用;
来连接多个清理命令。
如果在一个进程中写有多个trap函数,只会执行最后一个。
下面的示例:实际就执行第二句,第一个文件$zhangsan
不会被清理。
trap 'rm -rf "$zhangsan" ' EXIT
trap 'rm -rf "$bam" ' EXIT
获取输入文件的路径和文件名
在无法确定用户的输入是绝对或相对路径时使用
1. 获取文件的路径(输入的是绝对路径,输出也是绝对路径,输入是相对路径,输出也是相对路径)
dirname ../genome.fa
2. 获取文件的名称(无论是绝对或相对路径,输出的都只是文件名)
basename ../genome.fa
#输出是genome.fa
获取文件的前缀
basename ../genome.fa .fa
#输出是genome,
3. 获取绝对路径(如果是文件,返回是文件的绝对路径,如果是路径,返回是绝对路径)
readlink -f ../genome.fa
#输出是/share/home/……/genome.fa
在脚本中使用,赋值给变量的时候,两端一定要使用双引号括住。
#获取输入文件的绝对路径的文件
absol_genome="`readlink -f ../genome.fa`"
#获取输入文件的路径
genome_path="`dirname ../genome.fa`"
#获取输入文件的绝对路径
absol_path="`readlink -f ${genome_path}`"
# 修改文件的后缀
genome.gff="`basename ../genome.fa .fa`.gff"
使用${}
来获取文件路径和文件,文件后缀
此部分转载自https://www.cxyzjd.com/article/lifuxiangcaohui/50153207
file=/dir1/dir2/dir3/my.file.txt
我们可以用 ${ }
分别替换获得不同的值:
${file#*/}
:拿掉第一条/
及其左边的字符串:dir1/dir2/dir3/my.file.txt
${file##*/}
:拿掉最后一条 /
及其左边的字符串:my.file.txt
${file#*.}
:拿掉第一个 .
及其左边的字符串:file.txt
${file##*.}
:拿掉最后一个 .
及其左边的字符串:txt
${file%/*}
:拿掉最后条 /
及其右边的字符串:/dir1/dir2/dir3
${file%%/*}
:拿掉第一条 /
及其右边的字符串:(空值)
${file%.*}
:拿掉最后一个 .
及其右边的字符串:/dir1/dir2/dir3/my.file
${file%%.*}
:拿掉第一个 .
及其右边的字符串:/dir1/dir2/dir3/my
记忆的方法为:
#
是去掉左边(在键盘上 # 在 $ 之左边)
%
是去掉右边(在键盘上 % 在 $ 之右边)
单一符号是最小匹配﹔两个符号是最大匹配。
${file:0:5}
:提取最左边的 5 个字节:/dir1
${file:5:5}
:提取第 5 个字节右边的连续 5 个字节:/dir2
我们也可以对变量值里的字符串作替换:
${file/dir/path}
:将第一个 dir 提换为 path:/path1/dir2/dir3/my.file.txt
${file//dir/path}
:将全部 dir 提换为 path:/path1/path2/path3/my
其他shell命令和多线程相关 shell单行命令速查
杀死进程
pkill --ns 332314
杀死pid与332314的进程的程序名相同的所有程序 慎重使用,会杀掉一大批程序,适合批量提交后,需要杀掉批量的任务
kill -9 332314
杀掉pid进程为332314的程序
bkill 332314
用于lsf系统的杀掉JOBID 是332314的任务。
ps uux|grep ascp|awk '{print $2}'|xargs -i kill -9 {}
#批量杀掉包含ascp的任务
定时执行程序
常规做法,写个死循环,定时执行即可
#定时检测执行上传数据到NCBI
runinfo="ascp -i $HOME/cmm.aspera.openssh -QT -l 30m -k1 -d $HOME/upload_to_NCBI subasp@upload.ncbi.nlm.nih.gov:uploads/wangjing_gmail.com_XXXXXXX"
while true;
do
#检测网络
ping -c 3 -w 3 www.baidu.com >network_cache
#-c ping 3次 ,-w 3 ping 3s 后结束
if [ $? -eq 0 ];then
echo "network connect success!"
echo "检测网络,正常!"
aspera=`ps aux|grep ascp|grep -v grep|wc -l`
if [ "$aspera" -eq 0 ];then
`${runinfo}`
fi
sleep 3m #休眠3min后再检测
else
echo "network connect Wrong,Please check network!"
fi
done
优雅的定时使用crontab来控制
使用crontab -e
建立一个新的定时控制
注意:脚本里调用其他程序,程序需要使用完整的路径,同时也用使用完整路径指定运行脚本的位置。如果不指定位置,则可能找不到脚本调用的程序或者找不到脚本的位置。
*/10 * * * * bash /$HOME/keeprun.sh >/dev/null 2>&1 &
保存上面的即可每10分钟,执行一次命令bash keeprun.sh $HOME/.aspera/connect/bin/ascp -i $HOME/cmm.aspera.openssh -QT -l 30m -k1 -d $HOME/upload_to_NCBI subasp@upload.ncbi.nlm.nih.gov:uploads/wangjing_gmail.com_XXXXXXX
keeprun.sh内容如下:
#!/usr/bin/bash
#检测进程是否在运行,并将进程的行数信息传给count变量。
command="$HOME/.aspera/connect/bin/ascp -i $HOME/cmm.aspera.openssh -QT -l 30m -k1 -d $HOME/upload_to_NCBI subasp@upload.ncbi.nlm.nih.gov:uploads/wangjing_gmail.com_XXXXXXX"
file=`echo $command|rev|cut -d "/" -f1|cut -d " " -f2|rev`
count=`ps uux|grep $file|grep -v grep|grep -v keeprun|wc -l`
#如果count值大于0,则表示进程不存在
if [ $count -eq 0 ]
then
#进程不存在则运行下面的命令,尾部加上&使其在后台运行
`${command} &`
fi
# Example of job definition:
# .---------------- minute (00 - 59)
# | .------------- hour (00 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
查看指定用户的指定任务
crontab -u zhangsan -l
查看当前用户的定时任务
crontab -l