跳转至

鉴于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

回到页面顶部