Bash 15 分钟

背景

偶尔翻了之前的bash 编程的书,一些语法和细节发现可能还需要再review 下,然后就翻在线的ABS(Advanced Bash Scripting) ,发现这篇文档还是太长了,而且英文的东西,当时看了明白了,后面忘记了再去查,就有点需要花点时间找找在哪里看过。而且记得10年前好像看过一个word版本的bash 指南,写的相当简练,现在也忘记名字了,所以想想还是弄个简表,第一个是自己总结,第二个方便以后查找。我本来想直接用这个Learn X in Y ,因为他的格式比较符合我想记录的方式,但是内容上感觉有些地方没有说全,而且有些地方有错误的写法。我想我就以这篇为基础,然后扩充修改下这个bash教程吧,我同时也参考了其他的Bash 15分钟教程之类的内容,以免遗漏。

在线练习

如果你需要一个在线的比较好的练习环境来学习bash,这里有个好地方 还有之前老版本,通过老版本里的评论和回答你可以诊断下自己所学是否足够真材实料。后面或者会根据类似的问题列表,整理出来一个FAQ,相信这样对BASH编程理解会更深入

内容


#!/bin/bash
# 脚本的第一行叫 shebang,用来告知系统如何执行该脚本:
# 参见: http://en.wikipedia.org/wiki/Shebang_(Unix)
# 如你所见,注释以 # 开头,shebang 也是注释。

# 单行注释
echo "A comment will follow"  # 语句后的注释
   # 注释前面有空格也OK
echo "# this is string start with shebang" # 在引号中不起作用
ehco '# this is anothing string start with shebang' # '#'在引号中不起作用
echo the \# string      # 不在引号中的'#'要转义

# 每一句指令以换行或分号隔开:
echo 'This is the first line'; echo 'This is the second line'

# 声明一个变量:
Variable="Some string"

# 下面是错误的做法:
Variable = "Some string"
# Bash 会把 Variable 当做一个指令,由于找不到该指令,因此这里会报错。

# 也不可以这样:
Variable= 'Some string'
# Bash 会认为 'Some string' 是一条指令,由于找不到该指令,这里再次报错。
# (这个例子中 'Variable=' 这部分会被当作仅对 'Some string' 起作用的赋值。)

# 使用变量:
echo $Variable
echo "$Variable"
echo '$Variable'
# 当你赋值 (assign) 、导出 (export),或者以其他方式使用变量时,变量名前不加 $。
# 如果要使用变量的值, 则要加 $。
# 注意: ' (单引号) 不会展开变量(即会屏蔽掉变量)。

# 变量加双引号和不加双引号的区别
para="a b c d"
ls $para                    # 使用a,b,c,d 四个参数执行ls 相当于ls a b c d
ls "$para"                  # 使用一个参数执行ls ,相当于 ls "a b c d"

## 双括号结构中可以使用类似C语言的 cond?result-if-true:result-if-false
(( var = 1>2?3:4))

: # 冒号为空指令文件,返回为0

# 单花括号中的?可以用来检测变量是否被设置
# ${var?error_info},可以用来做必要参数检查

: ${var?}
: ${var?test message}

# 空指令版本的注释
: <<MULTILINECOMMENT
coomandd
\sds
~2@*
MULTILINECOMMENT

# 扩展单引号中被转义的字符串为ACSII
echo $'\n\n\n'              

# 变量赋值,使用命令执行结果,等同于echo `cat /etc/hostname`
echo $(cat /etc/hostname)

# 变量扩展或者置换

${param}                    # 等同$param,特定情况下这种${param}的更严格的书写模式才工作         
${param:-default}           # 建议这种,当param 被decare 但是没有赋值的时候,该表达式工作良好
${param:=default}           # If parameter not set, set it to default
${param:+default}           # If parameter set ,use alt, else use null
${param?error_mesage}       # If param not set ,error message
${#var}                     # var string length,if var is array ,return the first
${var#pattern}              # 从var中前面移除最短的pattern匹配
${var##pattern}             # 从var中前面移除最长的pattern匹配
${var%pattern}              # 从var中后面移除最短的pattern匹配
${var%%pattern}             # 从var中前面移除最长的pattern匹配

# 变量截取
${var:pos}                  # 从var的pos 位置开始截取,直到最后
${var:pos:len}              # 从var的pos 位置开始截len长度
${var/pattern/replace}      # 从var中以replace替换第一个Pattern匹配
${var//pattern//replace}    # 从var中以replace替换所有Pattern匹配
${var/#pattern/replace}     # 如果var的开头匹配pattern,拿replace 替换第一个匹配
${var/%pattern/replace}     # 如果var的结尾匹配pattern,拿replace 替换第一个匹配

${!varprefix*}              # 获取所有以prefix开头的变量 同${!varprefix@}

# 内置变量:
# 下面的内置变量很有用
echo "Last program return value: $?"
echo "Script's PID: $$"
echo "Number of arguments: $#"
echo "Scripts arguments: $@"
echo "Scripts arguments separated in different variables: $1 $2..."

: << SPECIALVARS
"$*"                        # 所有命名参数  
"$@"                        # TODO
"$?"                        # 命令、脚本的退出状态
"$!"                        # 最后一个后台执行的任务的PID
"$_"                        # 上一个命令的最后一个内容参数,如果命令没给任何参数,返回命令本身
"$$"                        # 脚本PID
$#                          # 脚本传入参数数量
$@                          # 脚本传入的所有参数
${!#}                       # 脚本最后一个传入参数
$1                           # 脚本第一个参数,对于1-9参数可以使用$1- $9
${10}                       # 第九个参数之后,用 ${10} ${11} 的方式
$1_                         # 位置参数变量后跟下划线,可以防止参数没有输入的情况

SPECIALVARS

# 读取输入:
echo "What's your name?"
read Name # 这里不需要声明新变量
echo Hello, $Name!

# 根据上一个指令执行结果决定是否执行下一个指令
echo "Always executed" || echo "Only executed if first command fails"
echo "Always executed" && echo "Only executed if first command does NOT fail"

# 数值比较单方括号内
a=2
[ 1 -eq $a ] && echo true || echo false  # false
[ 1 -ne $a ] && echo true || echo false  # true 
[ 1 -gt $a ] && echo true || echo false  # false
[ 1 -ge $a ] && echo true || echo false  # false
[ 1 -lt $a ] && echo true || echo false  # true
[ 1 -le $a ] && echo true || echo false  # true
# 数值比较也可以用双方括号,和上面基本一致。只不过把单方扩号换成双方括号而已

# 数值比较双括号内,此处的 > < 不用转义
(( 1 < "$a" )) && echo true || echo false # true
(( 1 > "$a" )) && echo true || echo false # false
(( 1 <= "$a" )) && echo true || echo false # true
(( 1 >= "$a" )) && echo true || echo false # false

# 特别注意点,bash 中数值比较不支持浮点数,只支持整数的比较,比如 
[ 1.23 -le 3 ] && echo true #将报错 -bash: [: 1.23: integer expression expected
(( 1.23 < 3 )) && echo true #将报错 -bash: ((: 1.23 < 3 : syntax error: invalid arithmetic operator (error token is ".23 < 3 ")

# 字符串比较
s="A"
[ "s" == "$s" ] && echo true || echo false # false
[ "s" != "$s" ] && echo true || echo false # true

# 这里有个地方要特别注意
#下面本意是比较ascii, 小a 应该大于大A,虽然执行结果也返回true,但是实际执行的却和用户意图不一样
# 下面这句实际是把"a"的值重定向到 A文件中了,重定向执行成功,所以方括号里面的 结果返回true
[ "a" > "$s" ] && echo true || echo false # true 

# 正确应该对方括号中的> < 都进行转义,否则系统会理解成重定向符号。
[ "X" \< "$s" ] && echo true || echo false # true
[ "a" \> "$s" ] && echo true || echo false # true 
[ -n "$s" ] && echo "string is not null" || echo "string is null" # string is not null
[ -z "$s" ] && echo "string is null" ||echo "string is not null"  # string is not null

# 文件test

[ -x /bin/bash ] && echo "bash is executable"
# 其他的test 可以用help test 列举。

# 通常的 if 结构看起来像这样:
# 'man test' 可查看更多的信息,由于$USER 为字符串,所以我们需要用!= 这些字符串比较符号
if [ $Name != $USER ]
then
    echo "Your name isn't your username"
else
    echo "Your name is your username"
fi

# 在 if 语句中使用 && 和 || 需要多对方括号
if [ $Name == "Steve" ] && [ $Age -eq 15 ]
# 也可以写成(注意双方括号中的&&) if [[ $Name == "Steve" && $Age -eq 15 ]]
# 也可以写成(注意单方括号中的-a) if [ $Name == "Steve" -a $Age  -eq 15 ]
then
    echo "This will run if $Name is Steve AND $Age is 15."
fi

if [ $Name == "Daniya" ] || [ $Name == "Zach" ]
# 也可以写成(注意双方括号中的||) if [[ $Name == "Daniya" || $Name == "Zach" ]]
# 也可以写成(注意单方括号中的-o) if [ $Name == "Daniya" -o $Name == "Zach" ]
then
    echo "This will run if $Name is Daniya OR Zach."
fi

# 花括号扩展
touch a{1,2,3}              # 会在当前目录创建a1,a2,a3
touch {a,b,c}{1,2,3}        # 会创建a1,a2,a3,b1,b2,b3,c1,c2,c3
echo {a..z}                 # 从a扩展到z .a,b,c,d,e....x,y,z
echo {1..9}                 # 从1至9

# Inline group,或者可以成为匿名函数
{ a=123; } ; echo $a        # 花括号中的变量可以在脚本后续代码中使用,不像function中的变量,出了function的作用域,function里面的变量无法在外部使用

# 数学计算,有三种方式
z=1
z=`expr $z + 3` # backtricks
echo $z
z=$((z+3))      # 双括号
echo $z
let "z += 3"    # let
echo $z

# 随机数
MAX=7
MIN=5

# 生成 0-5随机数
echo $(($RANDOM % $MIN)) 
# 生成5-7 随机数
echo $(($RANDOM %($MAX-$MIN) +$MIN ))

# 与其他编程语言不同的是,bash 运行时依赖上下文。比如,使用 ls 时,列出当前目录。
ls

# 指令可以带有选项:
ls -l # 列出文件和目录的详细信息

# 前一个指令的输出可以当作后一个指令的输入。grep 用来匹配字符串。
# 用下面的指令列出当前目录下所有的 txt 文件:
ls -l | grep "\.txt"

# 重定向输入和输出(标准输入,标准输出,标准错误)。
# 以 ^EOF$ 作为结束标记从标准输入读取数据并覆盖 hello.py :
# EOF这种方式为HERE String 
cat > hello.py << EOF
#!/usr/bin/env python
from __future__ import print_function
import sys
print("#stdout", file=sys.stdout)
print("#stderr", file=sys.stderr)
for line in sys.stdin:
    print(line, file=sys.stdout)
EOF

# HereString 前面带-号,可以抑制文档内部的开头tab,注意不是space
cat <<-ENDOFMESSAGE
    This is line 1 of the message.
    This is line 2 of the message.
    This is line 3 of the message.
    This is line 4 of the message.
    This is the last line of the message.
ENDOFMESSAGE

# 重定向可以到输出,输入和错误输出。
python hello.py < "input.in"
python hello.py > "output.out"
python hello.py 2> "error.err"
python hello.py > "output-and-error.log" 2>&1
python hello.py > /dev/null 2>&1
# > 会覆盖已存在的文件, >> 会以累加的方式输出文件中。
python hello.py >> "output.out" 2>> "error.err"

# 覆盖 output.out , 追加 error.err 并统计行数
info bash 'Basic Shell Features' 'Redirections' > output.out 2>> error.err
wc -l output.out error.err

# 以 "#helloworld" 覆盖 output.out:
cat > output.out <(echo "#helloworld")
echo "#helloworld" > output.out
echo "#helloworld" | cat > output.out
echo "#helloworld" | tee output.out >/dev/null

# 清理临时文件并显示详情(增加 '-i' 选项启用交互模式)
rm -v output.out error.err output-and-error.log

# Bash 的 case 语句与 Java 和 C++ 中的 switch 语句类似: 注意结尾的双;;是为了转义;
case "$Variable" in
    # 列出需要匹配的字符串
    [[:upper:]]) echo "The letter is upper.";;
    [[:lower:]]) echo "The letter is lower.";;
    [0-9]) echo "It 's a number";;
    *) echo "may be special letter ";;
esac

# 循环遍历给定的参数序列:

for Variable in {1..3}
# 或 for Variable in "A" "B" "C"
# 或 for Variable in `seq 2 6`
# 或 for Variable in $(ls)
# 或 for variable in *.sh;  # *.sh 在bash中会扩展成本目录下所有.sh结尾的文件
do
    echo "$Variable"
done

# 或传统的 “for循环” :
for ((a=1; a <= 3; a++))
do
    echo $a
done

# while 循环:
while [ true ]
do
    echo "loop body here..."
    break # break 可以跳出整个循环 # continue ,可以跳过该次循环
done

# Util 循环

until false
do 
    echo "loop body here..."
done

# Seletc 实现菜单选择

select vegetable in "A" "B" "C" "D"
do 
    echo "your fav is $vegetable"
    echo "Yuck!"
    echo 
    break
done

# 你也可以使用函数
# 定义函数:
function foo ()
{
    echo "Arguments work just like script arguments: $@"
    echo "And: $1 $2..."
    echo "This is a function"
    return 0
}

# 更简单的方法
bar ()
{
    echo "Another way to declare functions!"
    return 0
}

# 调用函数
foo "My name is" $Name

# 正则表达式(在双方括号中使用)

t="abc123"
[[ "$t" == abc* ]]         # true (globbing比较)
[[ "$t" == "abc*" ]]       # false (字面比较)
[[ "$t" =~ [abc]+[123]+ ]] # true (正则表达式比较)
[[ "$t" =~ "abc*" ]]       # false (字面比较)

# 数组操作
base64_charset=( {A..Z} {a..z} {0..9} + / = )
echo $base64_charset                # 只会输出数组的第一个项目的值
echo ${base64_charset[*]}           # 输出A-Z a-z 0-9 + / =
echo ${base64_charset[0]}           # 输出A
echo ${#base64_charset[*]}          # 输出数组长度65
echo ${base64_charset[*]:1:2}       # 数组分片,输出B C
base64_charset[2]="O"               # 对指定项目赋值,或者修改值   
echo ${!base64_charset[*]}          # 对指定输出所有数组索引

# for 遍历数组
for i in ${base64_charset[*]}
do 
    echo $i
done

for i in ${!base64_charset[*]}
do 
    echo ${base64_charset[$i]}
done

# 有很多有用的指令需要学习:
# 打印 file.txt 的最后 10 行
tail -n 10 file.txt
# 打印 file.txt 的前 10 行
head -n 10 file.txt
# 将 file.txt 按行排序
sort file.txt
# 报告或忽略重复的行,用选项 -d 打印重复的行
uniq -d file.txt
# 打印每行中 ',' 之前内容
cut -d ',' -f 1 file.txt
# 将 file.txt 文件所有 'okay' 替换为 'great', (兼容正则表达式)
sed -i 's/okay/great/g' file.txt
# 将 file.txt 中匹配正则的行打印到标准输出
# 这里打印以 "foo" 开头, "bar" 结尾的行
grep "^foo.*bar$" file.txt
# 使用选项 "-c" 统计行数
grep -c "^foo.*bar$" file.txt
# 如果只是要按字面形式搜索字符串而不是按正则表达式,使用 fgrep (或 grep -F)
fgrep "^foo.*bar$" file.txt 

# 以 bash 内建的 'help' 指令阅读 Bash 自带文档:
help
help help
help for
help return
help source
help .

# 用 man 指令阅读相关的 Bash 手册
apropos bash
man 1 bash
man bash

# 用 info 指令查阅命令的 info 文档 (info 中按 ? 显示帮助信息)
apropos info | grep '^info.*('
man info
info info
info 5 info

# 阅读 Bash 的 info 文档:
info bash
info bash 'Bash Features'
info bash 6
info --apropos bash

# Bash 调试
set -u #  Treat unset variables as an error when substituting 对未设置值的变量报告错误
set -e #  如果命令的执行返回不为0则退出
set -n # 执行语法检查而不要运行脚本,等同于bash -n script.sh
set -v # 输出每个命令,在执行每个命令之前,等同于bash -v script.sh
set -x # 和-v 类似,但是输出时,会在每个命令前添加+,这样可以快速区分出命令和输出

# trap ,在接收到指定信号后,执行特定action 
# 信号可以trap -l 列出
trap 'echo script exit' EXIT                            # 在脚本退出时,打印上面内容
trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT   # 在按CTRL+C时,执行清理任务