shell脚本的编写

鉴于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 [email protected]: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 [email protected]: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 [email protected]: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

你可能感兴趣的:(shell脚本的编写)