Bash Cookbook 学习笔记 【基础】

图片描述

Read Me

  • 本文是以英文版<bash cookbook> 为基础整理的笔记,力求脱水
  • 2017.11.23 更新完【基础】,内容涵盖bash语法等知识点。
  • 本系列其他两篇,与之互为参考

    • 【中级】内容包括工具、函数、中断及时间处理等进阶主题。传送门
    • 【高级】内容涉及脚本安全、bash定制、参数设定等高阶内容。传送门
  • 所有代码在本机测试通过

    • Debian GNU/Linux 9.2 (stretch)
    • GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)

约定格式

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

# 例如:
$ 命令 参数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

一、基本定义和I/O

在linux眼里,一切皆文件

输入/输出

文件描述符【简表】

类型标识描述符编号
标准输入STDIN0
标准输出STDOUT1
标准错误STDERR2
用户自定义 3...

I/O重定向【简表】

命令备注
命令 <输入.in读入
命令 >输出.out覆盖写
命令 >l输出.out在noclobber作用域内强制覆盖写
命令 >>输出.out追加写
命令 <<EOF 输入 EOF将"输入"内嵌到脚本内
命令a l 命令b l 命令c单向管道流
命令a l tee 输出a l 命令bT型管道流 (三通接头)
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变量的
因为两个控制台是相互隔离的运行环境

参数的访问计数 ${} * @ {#}

  • 基本用法
# 访问脚本的第{1}个参数
$ cat simplest.sh
echo ${1}
$ ./simplest.sh you see what I mean
you
$ ./simplest.sh one more time
one
$
    
# 索引是单数的时候,花括号可以省略
# $10其实是$1+0
$ cat tricky.sh
echo $1 $10 ${10}
$ ./tricky.sh I II III IV V VI VII VIII IX X XI
I I0 X
$
#!/bin/bash
# actall.sh 批量更改文件权限
for FN in $*
do
    echo changing $FN
    chmod 0750 $FN
done
$ ./actall.sh abc.tmp another.tmp allmynotes.tmp
    
# $*相当于把参数序列按原样放入循环
for FN in abc.tmp another.tmp allmynotes.tmp
do
    echo changing $FN
    chmod 0750 $FN
done
  • 处理意外的空格 "" $@
首先要感谢苹果公司。是苹果,普及了文件名带空格的写法。
当你给文件命名时,可以优雅地写成My Report或是Our Dept Data
而不是丑陋的MyReport或是Our_Dept_Data了。
这给脚本的处理带来不便。因为空格对于脚本而言,是个最基础的分隔符
# 用引号包裹参数
$ touch "Oh the Waste"          # 先建一个名称含空格的演示文件
$ cat quoted.sh
ls -l "${1}"                    # 这个引号告诉ls,将脚本传进来的参数视为整体
$
$ ./quoted.sh "Oh the Waste"    # 这个引号告诉脚本,将用户给的参数视为整体
-rw-r--r-- 1 jimhs jimhs 0 Nov 11 22:57 Oh the Waste
$
# 处理循环的时候,前边例子中的$*识别不了带空格的参数,这时要使用加引号的"$@"
    
# 先建三个名称含空格的演示文件 
$ touch abc.tmp "yet another".tmp "all my notes".tmp
    
#!/bin/bash
# actall.sh 批量更改文件权限
for FN in "$@"
do
    echo changing $FN
    chmod 0750 $FN
done
$ ./actall.sh *.tmp
  • 计数 $#
#!/bin/bash
# check_arg_count 计算参数个数
#
if [ $# -lt 3 ]
then
    # 参数少于三个
elif [ $# -gt 3 ]
    # 参数多于三个
else
    # 参数等于三个
fi
    
# 注意三者的不同含义
${#}        # 参数的个数
${#VAR}     # VAR值的长度
${VAR#alt}  # 替换
  • 访问下一个参数 shift
# 专业的脚本往往会涉及两种参数:
# 一种作为选项开关,用于控制脚本的行为;另一种才是待处理处理
# 选项开关一旦被解析完成,就可以丢弃
# 这时,shift就派上了用场
    
#!/bin/bash
# actall.sh 批量更改文件权限
# 通过-v开关控制echo行为
VERBOSE=0;
if [[ $1 = -v ]]
then
    VERBOSE=1;
    shift;  # 移位: -v参数处理完可以丢弃了
fi
    
for FN in "$@"
do
    if (( VERBOSE == 1 ))
    then
        echo changing $FN
    fi
    chmod 0750 "$FN"
done
$ ./actall.sh -v *.tmp
    
# 这个例子比较简单,只能处理单个的选项
# 实际应用中,会涉及更复杂的情况
# 例如多参数,myscript -a -p,这时参数顺序应该不影响行为
# 以及重复的参数,是该忽略还是提示错误。等等
# 后续谈到getopts时,会给出解决方案
  • 数组变量 [ ]
# bash可以处理一维数组变量
    
$ MYRA=(first second third home)
$ echo runners on ${MYRA[0]} and ${MYRA[2]}
runners on first and third
    
# 如果只写$MYRA,就表示${MYRA[0]}

默认值 ${:- := =}

  • 基本用法
# ${:-}用于设置命令行参数的默认值
# 位置1的参数如果为空,则使用/tmp
FILEDIR=${1:-"/tmp"}
    
# 变量的默认值
# 如果HOME变量没有设置,则设置为/tmp
cd ${HOME:=/tmp}
    
# 做个小实验
$ echo ${HOME:=/tmp}
/home/jimhs
$ unset HOME            # 删除HOME
$ echo ${HOME:=/tmp}
/tmp
$ echo $HOME
/tmp
$ cd ; pwd
/tmp
$
    
# 为了方便记忆和区分,可以这样理解:
# ${HOME:=/tmp}用等号(=),表示赋值和返回这个值
# ${1:-"/tmp"}只有半个等号(-),只是返回,没有赋值。因为位置参数本身也没法被赋值
  • 空值 null
# ${=}用于允许空值的情况
# 空值null,也即""这样的空字符串
# 空值,和不存在是两个概念
    
# 继续上边的实验
$ echo ${HOME=/tmp} # 不会替换,因为HOME值存在
/home/jimhs
$ HOME=""
$ echo ${HOME=/tmp} # 也不会替换,因为HOME值为""
$ unset HOME
$ echo ${HOME=/tmp} # 会替换,因为HOME变量不存在了
/tmp
$ echo $HOME
/tmp
$
  • 不只是常量
关于${:=}等号的右边,除了常量,还可以放什么? 引用bash手册的原话是 “is subject to tilde expansion, parameter expansion, command substitution, and arithmetic expansion.”
# tilde expansion 波浪符展开 (不能带引号"")
${BASE:=~uid17}
    
# parameter expansion 参数展开
${BASE:=${HOME}}
    
# command substitution 命令替代
cd ${BASE:="$(pwd)"}
    
# arithmetic expansion 算术运算
echo ${BASE:=/home/uid$((ID+1))}

参数缺失 ${:?}

# ${:?}可以检查参数是否提供,该如何处理
    
#!/bin/bash
# check_unset_parms 检查参数是否设置
USAGE="使用格式:myscript 路径 文件 方式“
FILEDIR=${1:?"错误。未提供路径。"}
FILESRC=${2:?"错误。未提供文件。"}
CVTTYPE=${3:?"错误。${USAGE}"}
    
# 运行的效果是这样的:
$ ./myscript /tmp /dev/null
./myscript: line 5: 3: 错误。使用格式:myscript 路径 文件 方式
$
    
# 对于第三个分支,可以通过嵌入子进程$()实现更复杂的功能    
CVTTYPE=${3:?"错误。$USAGE. $(rm $SCRATCHFILE)"}
    
# 也可以牺牲点紧凑,写成下边这样更易读的结构
if [ -z "$3" ]
then
    echo "错误。$USAGE"
    rm $SCRATCHFILE
fi
刚才运行时的提示,显示了行号和错误内容,像个脚本错误。虽然其实是用户自己使用不当造成的。所以,出于用户友好的考虑,商业级脚本并不会这样使用。对脚本编写和调试人员来说,倒是蛮实用的。
./myscript: line 5: 3: 错误。使用格式: myscript 路径 文件 方式

部分替代 ${% # //}

# ${%}可以对变量内容做部分的修改
    
#!bin/bash
# suffixer 把后缀.bad改成.bash
for FN in *.bad
do
    mv "${FN}" "${FN%bad}bash"
done
    
#                           分解来看各个步骤:
#                           # FN=subaddon.bad
NOBAD="${FN%bad}"           # NOBAD="subaddon."
NEWNAME="${NOBAD}bash"      # NEWNAME="subaddon.bash"
mv "${FN}" "${NEWNAME}"     # mv "subaddon.bad" "subaddon.bash"
# 用vi和sed等编辑器的替代格式可以吗?比如这样:
mv "${FN}" "${FN/.bad/.bash}"
# 编辑器中,末尾还要加个/来封闭这条语句
# 但这里不需要了,因为右半花括号}已经做了句法封闭
    
# ${FN/.bad/.bash}会把我们的subaddon.bad替换为subashdon.bad
# 如果写成${FN//.bad/.bash},就会替换为subashdon.bash
# 因为这两种写法,都无法像%那样实现替换位置的锚定。

字符串操作【简表】

${...}动作
name:number:number提取子串,起始位置:长度
#name返回字串长度
name#pattern删除最短匹配,左向右
name##pattern删除最长匹配,左向右
name%pattern删除最短匹配,右向左
name%%pattern删除最长匹配,右向左
name/pattern/string替代第一处匹配
name//pattern/string替代所有匹配

算术

bash支持整数运算

语法 ((...)) let

# 双括号内的表达式各项间对空格不敏感
# 各变量不需要再用$前缀,位置参数除外
COUNT=$(( COUNT + 5+MAX * 2 - $2))

# 除法取整数部分。不支持浮点运算
$ echo $((- 7/- 3))
2
     
# 多条表达式可以通过逗号级联
# 逗号运算符返回的是它右边的值,这里是3
$ echo $(( X+=5 , Y*=3 ))
3
     
# 双括号内的括号和星号已经是算术运算符的本义了
# 所以不需要再用\进行转义
Y=$(( ( X + 2 ) * 10 ))
    
# 双括号与let等效
# let语句的整条表达式内不能有空格
let Y=(X+2)*10

运算符【全表】

算术运算符描述用法等价于
=赋值a=ba=b
*=a*=ba=(a*b)
/=a/=ba=(a/b)
%=余数a%=ba=(a%b)
+=a+=ba=(a+b)
-=a-=ba=(a-b)
<<=左位移a<<=ba=(a<<b)
>>=右位移a>>=ba=(a>>b)
&=按位与a&=ba=(a&b)
^=按位异或a^=ba=(a^b)
l=按位或al=ba=(alb)

三、测试/流程控制

测试 [...] [[...]]

  • 测试体有三类

    • file 文件
    • string 字符串
    • expr 算术表达式
  • 使用说明

    • 运算符分为单目,和双目两种
    • 各运算符,用于条件测试,或在结构体[ ... ]和[[ ... ]]内使用
    • 多个运算符之间可以通过-a(逻辑与)和-o(逻辑或)进行级联
    • 也可以成组地用于转义过的( \( ... \) )内部
    • 用于字符串比较的<和>,以及结构体[[ ... ]],在bash 2.0版之前不可用
    • 正则表达式=~,只适用于bash 3.0及后续版本的结构体[[ ... ]]内部

测试运算符【全表】

测试运算符真值
-a file文件存在, 弃用, 同 -e
-b file文件存在,且为 块设备
-c file文件存在,且为 字符设备
-d file文件存在,且为 目录
-e file文件存在; 同 -a
-f file文件存在,且为 常规文件
-g file文件存在 且已 设置setgid位
-G file文件存在,且由 有效组id拥有
-h file文件存在,且为 符号链接, 同 -L
-k file文件存在 且已 设置粘滞位
-L file文件存在,且为 符号链接, 同 -h
-N file文件自从上次读后做过修改
-O file文件存在,且由 有效用户id拥有
-p file文件存在,且为 管道或命名管道(FIFO文件)
-r file文件存在,且为 可读
-s file文件存在,且为 非空
-S file文件存在,且为 套接字
-t N文件描述符N指向终端
-u file文件存在 且已 设置setuid位
-w file文件存在,且为 可写
-x file文件存在,且为 可执行, 或是可搜索的目录
fileA -nt fileBfileA 文件修改时间 晚于 fileA
fileA -ot fileBfileA 文件修改时间 早于 fileA
fileA -ef fileBfileA 和 fileB 指向同一文件
-n string字符串非空
-z string字符串长度为零
stringA = stringBstringA 匹配 stringB (POSIX版)
stringA == stringBstringA 匹配 stringB
stringA != stringBstringA 不匹配 stringB
stringA =~ regexpstringA 匹配 扩展正则表达式regexp
stringA < stringBstringA 小于 stringB 字典序
stringA > stringBstringA 大于 stringB 字典序
exprA -eq exprBexprA 等于 exprB
exprA -ne exprBexprA 不等于 exprB
exprA -lt exprBexprA 小于 exprB
exprA -gt exprBexprA 大于 exprB
exprA -le exprBexprA 小于等于 exprB
exprA -ge exprBexprA 大于等于 exprB
exprA -a exprBexprA 真 且 exprB 真
exprA -o exprBexprA 真 或 exprB 真

条件

单分支 if...then...else

  • 基本结构
#紧凑的写法:
if list; then list; [ elif list; then list; ] ... [ else list; ] fi
    
#更具可读性的写法:
if (( $# < 3 ))
then
    # 分支1
    exit 1
elif (( $# > 3 ))
then
    # 分支2
    exit 2
else
    # 参数个数等于3时。。
fi
  • 测试体
# 1. 方括号: 
# 主要用于文件测试
if [ -d file ]
    
# 也可用于简单的算术测试
# 如果算术表达式本身含括号,则还需要对括号进行转义或加引号区别
# 这时就不如用双圆括号方便
if [ $# -lt 3 ]
    
# 2. 双圆括号: 
# 只限于算术表达式
if (( $# < 3 ))
    
# 3. 命令:
# 各种命令的exit返回值可用于测试:
if ls; pwd; cd $1;
then
    echo success;   # cd成功时
else
    echo failed;    # cd失败时
fi
  • 【应用】逆波兰表达式
【摘自百度百科】逆波兰表达式(Reverse Polish Notation),简称为RPN,由J. Lukasiewicz (12/21/1878 – 02/13/1956)发展而来,在避免使用括号的情况下,完成表达式的有优先级的运算。RPN表达式由操作数(operand)和运算符(operator)构成,不使用括号,即可表示带优先级的运算关系,但是须使用元字符,如空格。一般在计算机中,使用栈操作进行RPN表达式的计算。遇到操作数就入栈,遇到运算符,就对当前栈顶元素进行相应的一元或者二元运算。
#!/bin/bash
# rpncalc.sh
#
# 实现简单的RPN命令行整数计算
# 普通写法:(5+4)*2
# 逆波兰写法:5 4 + 2 *
#
    
# 先检查参数个数是否正确(不能小于3,或者为偶数)
if [ \( $# -lt 3 \) -o \( $(($# % 2)) -eq 0 \) ]
then
    echo "用法: ./rpncalc 操作数 操作数 运算符 [ 操作数 运算符 ] ..."
    echo "乘法用x或*表示"
    exit 1
fi
    
ANS=$(($1 ${3//x/*} $2))    # 用x或*通配乘法
shift 3
    
# 循环直至参数耗尽,返回最终结果$ANS
while [ $# -gt 0 ]
do
    ANS=$((ANS ${2//x/*} $1))
    shift 2
done
echo $ANS
如果不喜欢逆波兰表达式,可以有另一种方案:
function calc
{
    awk "BEGIN {print \"结果: \" $* }";
}
这个函数通过awk内置的计算功能实现,且支持浮点数。比较使用,可以存到/etc/bashrc或~/.bashrc
$ calc 2 + 3 + 4.5
结果: 9.5
    
# 括号和乘号因为是bash的元字符,所以需要用\取消转义
$ calc \(2+2-3\)\*4
结果: 4
    
# 或者直接将整个表达式包裹进单引号内
$ calc '(2+2-3)*4.5'
结果: 4.5

多分支 case...in

case语句的强大,归功于右半括号):不光可以进行简单的字符串比较,还可以实现复杂的模式匹配
  • 基本结构
case $FN in
    *.gif) gif2png $FN
        ;;  # 双分号告诉bash,该分支到此结束(break)
    *.png) pngOK $FN
        ;;
    *.jpg) jpg2gif $FN
        ;;
    *.tif | *.TIFF) tif2jpg $FN
        ;;
    *) printf "格式无法识别: %s" $FN # 都不匹配时执行这里(default)
        ;;
esac 
    
# esac比end-case省字符
# 等价于:
    
if [[ $FN == *.gif ]]
then
    gif2png $FN
elif [[ $FN == *.png ]]
then
    pngOK $FN
elif [[ $FN == *.jpg ]]
then
    jpg2gif $FN
elif [[ $FN == *.tif || $FN == *.TIFF ]]
then
    tif2jpg $FN
else
    printf "格式无法识别: %s" $FN
fi
    
# elif比elseif省字符

循环

while

bash不同于其他编程语言,比如c或java,的地方是:当循环测试体返回零值时,判断为真。因为在bash的语境中,零代表一切正常,非零才代表异常。这点与其他语言正好相反。
  • 基本结构
# 1.无限循环
# 注意:因为expr非零,所以((expr))为零,所以while判断为真
while (( 1 ))
{
    ...
}
    
# 2.算术测试
# 各变量前无需再加$前缀
while (( COUNT < MAX ))
do
    some stuff
    let COUNT++
done
    
# 3.文件测试
# 方括号用法同if语句
while [ -z "$LOCKFILE" ]
do
    循环体
done
    
# 4.读取文件输入
# 文件末尾的值是-1,此时while非真,退出循环
while read lineoftext
do
    process $lineoftext
done
  • 如何将文件传给while-read循环
    
# 1.文件重定向给脚本
$ 脚本 <文件
    
# 2.对于固定的文件,可以直接在脚本内做重定向
while read lineoftext
do
    循环体
done < 文件
    
# 3.或者通过管道的方式
# 注意:管道左边的cat和右边的整个循环体都将是独立的子进程
# 循环内定义的任何变量,出循环体后都无法继续使用了
# 反斜线\进行分行,使结构更具可读性
cat 文件 | \
while read lineoftext
do
    循环体
done
  • 【应用】清理垃圾文件
当使用版本控制命令svn查看一个工作目录内的文件变动时,常见的输出格式如下。
每个文件的前缀标识:M有修改、A新添加、?无法识别。
标识?的文件通常为运行后留下的临时或其他碎片文件。
$ svn status bcb
M      bcb/amin.c
?      bcb/dmin.c
?      bcb/mdiv.tmp
A      bcb/optrn.c
M      bcb/optson.c
?      bcb/prtbout.4161
?      bcb/rideaslist.odt
?      bcb/x.maxc
$
有两种方法清理这些垃圾文件:
  • 方法一
# svn输出|grep取前缀为?的行|cut取出文件名|rm删除
    
svn status mysrc | grep '^?' | cut -c8- | \
    while read FN; do echo "$FN"; rm -rf "$FN"; done
  • 方法二
# 因为bash根据空格作为参数分隔符
# 所以,分别用两个参数TAG和FN接收前缀标识和文件名
    
svn status mysrc | \
while read TAG FN
do
    if [[ $TAG == \? ]]
    then
        echo $FN
        rm -rf "$FN"
    fi
done

for

for常用于需要计数的循环
  • 基本结构
# 采用类c的风格
for (( expr1 ; expr2 ; expr3 )) ; do list ; done
    
# 支持多变量
for (( i=0, j=0 ; i+j < 10 ; i++, j++ ))
do
    echo $((i*j))
done
  • 浮点数循环
# 可以结合seq命令实现浮点数循环
# 要注意seq的参数顺序,递增量是写在中间的
    
$ seq 1.0 .03 1.1
1.00
1.03
1.06
1.09
# for结构
    
for fp in $(seq 1.0 .01 1.1)
do
    echo $fp; 其他语句
done
    
# 先通过一个$()子进程展开seq列表,全部展开完了再传给for处理
# 原seq列表内的"换行"都被子进程替换为了空格
# 这样,每一个浮点值,传给for的时候都变成了字符串 "1.01" "1.02"
# while结构
    
seq 1.0 .01 1.1 | \
while read fp
do
    echo $fp; 其他语句
done
    
# seq与while通过管道|连接,各自为独立的子进程,是并行的关系
# 这样,对于特别大而耗时的序列,seq的处理不会阻塞while循环
# 而前一个的for结构,是串行的关系,存在阻塞的隐患
  • 早期的循环风格
# 早期的bash, for循环只能写成展开的形式
# 类c的风格是从版本2.04往后才有的
    
for i in 1 2 3 4 5 6 7 8 9 10
do
    echo $i
done

select...in

select可以对一个列表进行循环选择,实现简单的目录功能
$ cat dblist
testDB
simpleInventory
masterInventory
otherDB
#!/bin/bash
# dbinit.sh
DBLIST=$(cat dblist)
select DB in $DBLIST
do
    echo 初始化数据库: $DB
    # 其他操作
done
$ ./dbinit.sh
1) testDB
2) simpleInventory
3) masterInventory
4) otherDB
#? 2
初始化数据库: simpleInventory
#?
$
select会对列表各项添加数字编号。以#?作为提示输入符,无限循环列表,直到用户按Ctrl+D结束输入并退出。对于系统默认的#?提示符,可以通过修改PS3变量进行个性化。(注:PS1就是标准控制台的输入提示符,PS2是跨行输入时显示的提示符。)
#!/bin/bash
# dbinit.sh
PS3="0 inits >"
select DB in $DBLIST
do
    if [ $DB ]
    then
        echo 初始化数据库: $DB
        PS3="$((i++)) inits >"
        # 其他操作
    fi
done
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Shell scripting is a way to harness and customize the power of any Unix system, and an essential skill for Unix users, system administrators, and even professional Mac OS X developers. But beneath this simple promise lies a treacherous ocean of variations in Unix commands and standards. This thoroughly revised edition of bash Cookbook teaches shell scripting the way Unix masters practice the craft. Three Unix veterans present a variety of recipes and tricks for all levels of shell programmers so that anyone can become a proficient user of the most common Unix shell—the bash shell—and cygwin or other popular Unix emulation packages. This cookbook is full of useful scripts that readers can use right away, along with examples that explain how to create better scripts. Table of Contents Chapter 1. Beginning Bash Chapter 2. Standard Output Chapter 3. Standard Input Chapter 4. Executing Commands Chapter 5. Basic Scripting: Shell Variables Chapter 6. Shell Logic And Arithmetic Chapter 7. Intermediate Shell Tools I Chapter 8. Intermediate Shell Tools Ii Chapter 9. Finding Files: Find, Locate, Slocate Chapter 10. Additional Features For Scripting Chapter 11. Working With Dates And Times Chapter 12. End-User Tasks As Shell Scripts Chapter 13. Parsing And Similar Tasks Chapter 14. Writing Secure Shell Scripts Chapter 15. Advanced Scripting Chapter 16. Configuring And Customizing Bash Chapter 17. Housekeeping And Administrative Tasks Chapter 18. Working Faster By Typing Less Chapter 19. Tips And Traps: Common Goofs For Novices Appendix A Reference Lists Appendix B Examples Included With Bash Appendix C Command-Line Processing Appendix D Revision Control
Shell scripting is a way to harness and customize the power of any Unix system, and an essential skill for Unix users, system administrators, and even professional Mac OS X developers. But beneath this simple promise lies a treacherous ocean of variations in Unix commands and standards. This thoroughly revised edition of bash Cookbook teaches shell scripting the way Unix masters practice the craft. Three Unix veterans present a variety of recipes and tricks for all levels of shell programmers so that anyone can become a proficient user of the most common Unix shell—the bash shell—and cygwin or other popular Unix emulation packages. This cookbook is full of useful scripts that readers can use right away, along with examples that explain how to create better scripts. Table of Contents Chapter 1. Beginning Bash Chapter 2. Standard Output Chapter 3. Standard Input Chapter 4. Executing Commands Chapter 5. Basic Scripting: Shell Variables Chapter 6. Shell Logic And Arithmetic Chapter 7. Intermediate Shell Tools I Chapter 8. Intermediate Shell Tools Ii Chapter 9. Finding Files: Find, Locate, Slocate Chapter 10. Additional Features For Scripting Chapter 11. Working With Dates And Times Chapter 12. End-User Tasks As Shell Scripts Chapter 13. Parsing And Similar Tasks Chapter 14. Writing Secure Shell Scripts Chapter 15. Advanced Scripting Chapter 16. Configuring And Customizing Bash Chapter 17. Housekeeping And Administrative Tasks Chapter 18. Working Faster By Typing Less Chapter 19. Tips And Traps: Common Goofs For Novices Appendix A Reference Lists Appendix B Examples Included With Bash Appendix C Command-Line Processing Appendix D Revision Control

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值