Bash Cookbook 学习笔记

前端之家收集整理的这篇文章主要介绍了Bash Cookbook 学习笔记前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

Read Me

  • 本文是以英文版<bash cookbook> 为基础整理的笔记,或速记,力求脱水

  • cookbook特点是实用,有很多可以复用的代码框架

  • 假设读者已有一定的脚本基础知识,没有也没关系

  • 我没找到中文版,所以决定diy,按自己的风格来写一遍

  • 类似bash是啥等问题,不涉及。。

  • 另推荐两本比较好的教材:

    • <Linux Shell Scripting Cookbook> 有中文版。零基础的读者可以先看这本

    • <Advanced Bash-Scripting Guide> 旧版的有中文。圣经级

  • 争取周更一章 【11.12 更新第二章:变量】

一、基本定义和I/O

约定格式

# 注释:前导的$表示命令提示符
# 注释:无前导的第二+行表示输出

# 例如:
$ 命令 参数1 参数2 参数3 # 行内注释
输出_行一
输出_行二
    
$ cmd par1 par1 par2 # in-line comments
output_line1
output_line2

获取帮助

天助自助者

命令查询 man help

# cmd表示任意命令
$ man cmd
    
# 手册第7章(这一章是内容总览)
$ man 7 cmd
    
$ cmd -h
    
$ cmd --help
    
# 查看bash内置命令的帮助文档
$ help builtin-cmd

删除 rm

# 文件删除前询问确认(误删文件会很麻烦的)
$ rm -i abc.file
rm: remove regular file 'abc.file'?

命令(精确)查找 type which locate

# 从$PATH的路径列表中查找:可执行的别名、关键字、函数、内建对象、文件等。
$ type ls
ls is aliased to `ls -F -h`
    
$ type -a ls # 查找全部(All)匹配的命令
ls is aliased to `ls -F -h`
ls is /bin/ls
    
$ which which
/usr/bin/which
    
# 也用于判断命令是bash内置(built-in)或是外部的(external)
$ type cd
cd is a shell builtin
# 从cron job维护的系统索引库中查找。
$ locate apropos
/usr/bin/apropos
/usr/share/man/de/man1/apropos.1.gz
/usr/share/man/es/man1/apropos.1.gz
/usr/share/man/it/man1/apropos.1.gz
/usr/share/man/ja/man1/apropos.1.gz
/usr/share/man/man1/apropos.1.gz
    
# slocate (略)

命令(模糊)查找 apropos

# 从man手册中查找匹配的命令关键字。
$ apropos music
cms (4) - Creative Music System device driver
    
$ man -k music # 效果同上
cms (4) - Creative Music System device driver

输入/输出

在linux眼里,一切皆文件

文件描述符

类型 标识 描述符编号
标准输入 STDIN 0
标准输出 STDOUT 1
标准错误 STDERR 2
用户自定义 3...

I/O常用符号速查表

命令 备注
命令 <输入.in 读入
命令 >输出.out 覆盖写
命令 >l输出.out 在noclobber作用域内强制覆盖写
命令 >>输出.out 追加写
命令 <<EOF 输入 EOF 将"输入"内嵌到脚本内
命令a l 命令b l 命令c 单向管道流
命令a l tee 输出a l 命令b T型管道流 (三通接头)
2 >&1 &的意思是,将1解释为描述符而不是文件
2 >&3- -的意思是 : 自定义描述符3用完后释放

I/O的流向

$ 命令 1>输出文件.out 2>错误日志.err
    
# 单向管道流
$ cat my* | tr 'a-z' 'A-Z' | uniq | awk -f transform.awk | wc
    
# 通过tee实现管道分流,将uniq的输出写入x.x,同时也传给awk处理
$ ... uniq | tee /tmp/x.x | awk -f transform.awk ...
    
# 对于不接受标准输入作为参数的命令,比如rm
# 此时无法像这样写管道流
$ find . -name '*.c' | rm
# 解决办法是,将输入通过$(...)打包为子进程
$ rm $(find . -name '*.class')
    
# 通过引入一个自定义的临时描述符3,可以实现STDOUT和STDERR的对调
$ ./myscript 3>&1 1>stdout.logfile 2>&3- | tee -a stderr.logfile
# 简化的结构
$ ./myscript 3>&1 1>&2 2>&3

单行多命令 sub-shell

# 一是用{},因为花括号是保留字,所以前后括号与命令间都要留一个空格
$ { pwd; ls; cd ../elsewhere; pwd; ls; } > /tmp/all.out
    
# 二是用(),bash会把圆括号内的序列打包为一个子进程(sub-shell)
# 子进程是个很重要的概念,这里暂不展开
# 如果说bash是个壳,sub-shell就是壳中壳了
# 类比python的闭包
$ (pwd; ls; cd ../elsewhere; pwd; ls) > /tmp/all.out

here document

here document是个linux脚本语言所特有的东西
对这个专有名词,我在网上也没找到现成的翻译
这里的here可以理解为"here it is"
即把原本需要从外部引用的输入文件
用一对EOF标识符直接包裹进脚本
这样就免去了从命令行再多引入一个外部文件的麻烦
如果把输入文件比作脚本需要的电池
就相当于“自带电池”的概念了(又借用了python的词)

# bash会对内容块内一些特殊标识进行不必要的解析和转义,进而可能导致一些异常行为
# 所以作为一个良好的习惯,建议改用<<\EOF,或<<'EOF',甚至可以是<<E\OF
    
$ cat ext
# here is a "here" document ## 巧妙的双关语
grep $1 <<EOF
mike x.123
sue x.555
EOF
$
$ ext 555
sue x.555
$
# tab缩进:<<-'EOF'
# -(减号)会告知bash忽略EOF块内的前导tab标识
# 最后一个EOF前内务必不要留多余的空格,否则bash将无法定位内容块结束的位置
    
$ cat myscript.sh
...
    grep $1 <<-'EOF'
        lots of data
        can go here
        it's indented with tabs
        to match the script's indenting
        but the leading tabs are
        discarded when read
        EOF # 尾巴的EOF前不要有多余的空格
    ls
...
$

获取用户输入 read

# 直接使用
$ read
    
# 通过-p参数设置提示符串,并用ANSWER变量接收用户的输入
$ read -p "给个答复 " ANSWER
    
# 输入与接收变量的对应原则:
# 类比python中元组的解包(平行赋值)
# 参数:       PRE MID POST
# 输入比参数少:one
# 参数:       PRE(one),MID(空),POST(空)
# 输入比参数多:one two three four five
# 参数:       PRE(one),MID(two),POST(three four five)
$ read PRE MID POST
    
# 密码的接收
# -s关闭明文输入的同时,也屏蔽了回车键,所以通过第二句显式输出一个换行
# 
# $PASSWD以纯文本格式存放在内存中,通过内核转储或查看/proc/core等方式可以提取到
$ read -s -p "密码: " PASSWD ; printf "%b" "\n"

一些实用的脚本框架

# 文件名: func_choose
# 根据用户的输入选项执行不同命令
# 调用格式: choose <默认(y或n)> <提示符> <选yes时执行> <选no时执行>
# 例如:
# choose "y" \
# "你想玩个游戏吗?" \
# /usr/games/spider \
# 'printf "%b" "再见"' >&2
# 返回: 无
function choose {
    
    local default="$1"
    local prompt="$2"
    local choice_yes="$3"
    local choice_no="$4"
    local answer
    
    read -p "$prompt" answer
    [ -z "$answer" ] && answer="$default"
    
    case "$answer" in
        [yY1] ) exec "$choice_yes"
            # 错误检查
            ;;
        [nN0] ) exec "$choice_no"
            # 错误检查
            ;;
         *    ) printf "%b" "非法输入 '$answer'!"
    esac
} # 结束
# 文件名: func_choice.1
# 把处理用户输入的逻辑单元从主脚本中剥离,做成一个有标准返回值的函数
# 调用格式: choice <提示符>
# 例如: choice "你想玩个游戏吗?"
# 返回: 全局变量 CHOICE
function choice {
    
    CHOICE=''
    local prompt="$*"
    local answer
    
    read -p "$prompt" answer
    case "$answer" in
        [yY1] ) CHOICE='y';;
        [nN0] ) CHOICE='n';;
        *     ) CHOICE="$answer";;
    esac
} # 结束
    
# 主脚本只负责业务单元:
# 不断返回一个包的时间值给用户确认或修改,直到新值满足要求
until [ "$CHOICE" = "y" ]; do
    
    printf "%b" "这个包的时间是 $THISPACKAGE\n" >&2
    choice "确认? [Y/,<新的时间>]: "
    
    if [ -z "$CHOICE" ]; then
        CHOICE='y'
    elif [ "$CHOICE" != "y" ]; then
        # 用新的时间覆写THISPACKAGE相关的事件
        printf "%b" "Overriding $THISPACKAGE with ${CHOICE}\n"
        THISPACKAGE=$CHOICE
    fi
done
    
# 这里写THISPACKAGE相关的事件代码
# 以下总结三种常用的预定义行为:

# 1. 当接收到'n'之外的任何字符输入时,向用户显示错误日志
choice "需要查看错误日志吗? [Y/n]: "
if [ "$choice" != "n" ]; then
    less error.log
fi
    
# 2. 只有接收到小写'y',才向用户显示消息日志
choice "需要查看消息日志吗? [y/N]: "
if [ "$choice" = "y" ]; then
    less message.log
fi
    
# 3. 不论有没有接收到输入,都向用户做出反馈   
choice "挑个你喜欢的颜色,如果有的话: "
if [ -n "$CHOICE" ]; then
    printf "%b" "你选了: $CHOICE"
else
    printf "%b" "没有喜欢的颜色."
fi

二、命令/变量/逻辑/算术

命令

抛开窗口和鼠标的束缚

运行的机制 $PATH

# 当输入任意一条命令时
$ cmd
    
# bash会遍历在环境变量$PATH定义的路径,进行命令匹配
# 路径串用冒号分隔。注意最后的点号,表示当前路径
$ echo $PATH
/bin:/usr/bin:/usr/local/bin:.
    
# 做个小实验:
$
$ bash # 首先,开一个bash子进程
$ cd # 进到用户的home路径
$ touch ls # 创建一个与ls命令同名的空文件
$ chmod 755 ls # 赋予它可执行权限
$ PATH=".:$PATH" # 然后把当前(home)路径加入PATH的头部
$
    
# 这时,在home路径下执行ls命令时,会显示一片空白
# 因为你所期望的ls已经被自创的ls文件替换掉了
# 如果去到其他路径再执行ls,一切正常
    
# 实验做完后清理现场
$ cd
$ rm ls
$ exit # 退出这个bash子进程
$
    
# 所以,安全的做法是,只把当前路径附在PATH的尾部,或者干脆就不要附进去
# 一个实用的建议:
# 可以把自己写的所有常用脚本归档在一个自建的~/bin目录里
PATH=~/bin:$PATH
# 通过自定义的变量操作命令:
# 比如定义一个叫PROG的通用变量
$ FN=/tmp/x.x
$ PROG=echo
$ PROG $FN
$ PROG=cat
$ PROG $FN

变量的取名是很有讲究的。有些程序,比如InfoZip,会通过$ZIP和$UNZIP等环境变量传参给程序。如果你在脚本中擅自去定义了一个类似ZIP='/usr/bin/zip'的变量,会怎么想也想不明白:为什么在命令行工作得好好的,到了脚本就用不了? 所以,一定要先去读这个命令的使用手册(RTFM: Read The Fxxking Manual)。

运行的顺序 串行 并行

三种让命令串行的办法

# 1. 不停的手工输入命令,哪怕前一条还没执行完,Linux也会持续接收你的输入的
    
# 2. 将命令串写入一个脚本再批处理
$ cat > simple.script
long
medium
short
^D # 按Ctrl-D完成输入
$ bash ./simple.script
    
# 3. 更好的做法是集中写在一行:
# 顺序执行,不管前一条是否执行成功
$ long ; medium ; short
# 顺序执行,前一条执行成功才会执行下一条
$ long && medium && short

命令的并行

# 1. 用后缀&把命令一条条手工推到后台
$ long &
[1] 4592 
$ medium &
[2] 4593
$ short
$
    
# 2. 写在一行也可以
$ long & medium & short
[1] 4592
[2] 4593  # [工作号] 进程号
$
$ kill %2 # 关闭medium进程,或者kill 4593
$ fg %1   # 把long进程拉回前台
$ Ctrl-Z  # 暂停long进程
$ bg      # 恢复long进程,并推到后台

linux其实并没有我们所谓“后台”的概念。当说“在后台执行一条命令”时,实际上发生的是,命令与键盘输入脱开。然后,控制台也不会阻塞在该命令,而是会显示下一条命令提示符。一旦命令“在后台”执行完,该显示的结果还是会显示回屏幕,除非事先做了重定向

# 不挂断地运行一条后台命令
$ nohup long &
nohup: appending output to 'nohup.out'
$

用&运行一条后台命令时,它只是作为bash的一个子进程存在。当你关闭当前控制台时,bash会广播一个挂断(hup)信号给它的所有子进程。这时,你放在后台的long命令也就被“意外”终止了。通过nohup命名可以避免意外的发生。如果决意要终止该进程,可以用kill,因为kill发送的是一个SIGTERM终止信号。控制台被关闭后,long的输出就无处可去了。这时,nohup会被输出追加写到当前路径下的nohup.out文件。当然,你也可以任意指定这个重定向的行为。

脚本的批量执行

# 如果有一批脚本需要运行,可以这样:
for SCRIPT in /path/to/scripts/dir/*
do
    if [ -f $SCRIPT -a -x $SCRIPT ]
    then
        $SCRIPT
    fi
done
# 这个框架的一个好处是,省去了你手工维护一个脚本主清单的麻烦
# 先简单搭个架子,很多涉及robust的细节还待完善

返回状态 $?

用$?接收命令返回

# $?变量动态地存放“最近一条”命令的返回状态
# 惯例:【零值】正常返回;【非零值】命令异常
# 取值范围: 0~255,超过255会取模
    
$ badcommand
it fails...
$ echo $?
1           # badcommand异常
$ echo $?
0           # echo正常
$
    
$ badcommand
it fails...
$ STAT=$?   # 用静态变量捕获异常值
$ echo $STAT
1
$ echo $STAT
1
$

$?结合逻辑判断

# 例如:
# 如果cd正常返回,则执行rm
cd mytmp
if [ $? -eq 0 ]; 
    then rm * ; 
fi
    
# 更简洁的表达:
# A && B:逻辑与
# 如果cd正常返回,则执行rm
$ cd mytmp && rm *
    
# A || B:逻辑或
# 如果cd异常返回,则打印错误信息并退出
cd mytmp || { printf "%b" "目录不存在.\n" ; exit 1 ; }
    
# 如果不想写太多的逻辑判断,在脚本中一劳永逸的做法是:
set -e      # 遇到任何异常则退出
cd mytmp    # 如果cd异常,退出
rm *        # rm也就不会执行了

变量

一些常识 $

  • 变量是:

    • 存放字符串和数字的容器

    • 可以比较、改变、传递

    • 不需要事先声明

# 主流的用法是,全用大写表示变量,MYVAR
# 以上只是建议,写成My_Var也可以
    
# 赋值不能有空格 变量=值
# 因为bash按空格来解析命令和参数
$ MYVAR = more stuff here    # 错误
$ MYVAR="more stuff here"    # 正确
    
# 变量通过$来引用
# 抽象来看,赋值语句的结构是:左值=右值
# 通过$,告诉编译器取右值
# 而且,$将变量和同名的字面MYVAR做了区分
$ echo MYVAR is now $MYVAR
MYVAR is now more stuff here
    
for FN in 1 2 3 4 5
do
    somescript /tmp/rep$FNport.txt      # 错误 $FNport被识别为变量
    somescript /tmp/rep${FN}port.txt    # 正确 {}界定了变量名称的范围
done

导出和引用 export

# 查看当前环境定义的所有变量
$ env
$ export -p
  • 导出变量的正确方式

# 可以把导出声明和赋值写在一起
export FNAME=/tmp/scratch
    
# 或者,先声明导出,再赋值
export FNAME
FNAME=/tmp/scratch
    
# 再或者,先赋值,再声明导出
FNAME=/tmp/scratch
export FNAME
    
# 赋过的值也可以修改
export FNAME=/tmp/scratch
FNAME=/tmp/scratch2
  • 正确的理解变量引用

# 通过上边的声明,我们有了一个FNAME的环境变量
$ export -p | grep FNAME
declare -x FNAME="/tmp/scratch2"
# 我们暂称它是父脚本
    
# 现在如果父脚本内开了(调用)一个子脚本去访问和修改这个变量,是可以的
# 但是,这个修改行为,对于父脚本是透明的
# 因为子脚本访问和修改的,只是从父脚本copy过来的环境变量复本
# 这是单向的继承关系,也是linux的一种设计理念(或称为安全机制)
    
# 父脚本有没有什么办法去接收到这个改动呢?
# 唯一的取巧办法是:
# 让子脚本将修改echo到标准输出
# 然后,父脚本再通过shell read的方式去读这个值
# 但是,从维护的角度来讲,并不建议这样做
# 如果真的需要这么做,那原来的设计就有问题

所谓环境,指的是当前环境,也即当前控制台
如果你新开一个bash控制台,是根本看不到这个FNAME变量的
因为两个控制台是相互隔离的运行环境

逻辑

算术

猜你在找的Bash相关文章