Bash脚本教程
阮一峰老师的写的Bash 脚本教程 (opens new window)已经非常完善了,这里是我做的一个笔记。
# Bash历史
Shell是一个命令行环境,也是一个命令解释器。
常用的Shell有:
- Bourne Shell(sh)
- Bourne Again shell(bash)
- Z Shell(zsh)
- Friendly Interactive Shell(fish)
bash是最常用的Shell,主要讲解它。
# 基本语法
echo命令:
- 支持多行,使用双引号
- -n 取消末尾回车符
- -e 解释执行,默认的双引号或单引号会把特殊字符编程普通字符,比如\n
命令格式:
- 组成:命令+空格+参数
- 分号表示结束
- && 表示前命令成功,后命令才执行
- || 表示前命令失败,后命令才执行
快捷键:
- Ctrl + L:清除屏幕并将当前行移到页面顶部。
- Ctrl + C:中止当前正在执行的命令。
- Shift + PageUp:向上滚动。
- Shift + PageDown:向下滚动。
- Ctrl + U:从光标位置删除到行首。
- Ctrl + K:从光标位置删除到行尾。
- Ctrl + W:删除光标位置前一个单词。
- Ctrl + D:关闭 Shell 会话。
- ↑,↓:浏览已执行命令的历史记录。
# 模式扩展
波浪线~:
- 自动扩展成当前用户的主目录
- ~user表示扩展成用户user的主目录
- ~+会扩展成当前所在的目录,等同于pwd命令
字符扩展,正则的简易版本:
- ?代表单个字符
- *代表多个字符
- [...]多字符
- [start-end]范围
- {...}序列
- {start..end} 连续序列
shopt -s extglob
开启之后,可以控制模式匹配的次数- ?(pattern-list):模式匹配零次或一次。比如 ls abc?(.)txt
- *(pattern-list):模式匹配零次或多次。
- +(pattern-list):模式匹配一次或多次。
- @(pattern-list):只匹配一次模式。
- !(pattern-list):匹配给定模式以外的任何内容。
字符类扩展:
- [[:alnum:]]:匹配任意英文字母与数字
- [[:alpha:]]:匹配任意英文字母
- [[:blank:]]:空格和 Tab 键。
- [[:cntrl:]]:ASCII 码 0-31 的不可打印字符。
- [[:digit:]]:匹配任意数字 0-9。
- [[:graph:]]:A-Z、a-z、0-9 和标点符号。
- [[:lower:]]:匹配任意小写字母 a-z。
- [[:print:]]:ASCII 码 32-127 的可打印字符。
- [[:punct:]]:标点符号(除了 A-Z、a-z、0-9 的可打印字符)。
- [[:space:]]:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)。
- [[:upper:]]:匹配任意大写字母 A-Z。
- [[:xdigit:]]:16进制字符(A-F、a-f、0-9)
变量与命令扩展:
- $开头视为变量,也可以${}表示
- $(...)扩展为命令运行,也可以放在反引号之中(古老写法)
# 引号和转义
转义:
- echo $date输出的变量,echo $date,输出的字符串
- 反斜杠还可以表示一些不可打印的字符。注意使用-e
- \a:响铃
- \b:退格
- \n:换行
- \r:回车
- \t:制表符
- 反斜杠还可以把换行符转义为特殊字符,从而实现一行命令写为多行
引号:
- 单引号会把所有特殊字符都变成普通字符。比如echo '$SHELL' = $SHELL
- 双引号同单引号,但美元符号($)、反引号(`)和反斜杠(\)除外,这三个符合还是会被扩展。比如echo "$SHELL" = /bin/bash
- 双引号会转义换行符,用来实现多行文本
- 双引号会保留输出格式,比如echo "$(cal)",会显示一个格式化的日历
here文档:
<< token
text
token
一般配合tee或cat命令使用。 还有一个变体,使用三个小于号(<<<)表示字符串:cat <<< 'hi there'
# 变量
变量操作:
- evn或printenv显示所有环境变量
- set显示所有变量和bash函数
- 创建变量:variable=value
- 变量本身也是变量,可以使用${!varname}的语法将其展开
- unset用来删除变量
- export命令用来向子 Shell 输出变量
- 一些特殊变量
- $? 上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功
- $$ 当前 Shell 的进程 ID
- $_ 上一个命令的最后一个参数
- $! 最近一个后台执行的异步命令的进程 ID
- $0 当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)
- $- 当前 Shell 的启动参数
- $# 脚本的参数数量
- $@ 脚本的参数值
- 变量的默认值
- ${varname:-word} 如果变量varname存在且不为空,则返回它的值,否则返回word
- ${varname:=word} 如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word
- ${varname:+word} 如果变量名存在且不为空,则返回word,否则返回空值
- ${varname:?message} 如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行
- 四种语法如果用在脚本中,变量名的部分可以用数字1到9,表示脚本的参数
- declare声明一些特殊类型的变量
- let命令可以直接执行算术表达式
# 字符串操作
字符串操作:
- ${#varname} 取长度
- ${varname:offset:length} substring方法
- ${variable#pattern} 头部模式匹配,删除非贪婪匹配到的部分,返回剩余部分。
- ${variable##pattern} 头部模式匹配,删除贪婪匹配(最长匹配)到的部分,返回剩余部分。
- ${variable/#pattern/string} 头部模式的替换
- ${variable%pattern} 尾部模式匹配,非贪婪
- ${variable%%pattern} 尾部模式匹配,贪婪
- ${variable/%pattern/string} 尾部模式的替换
- ${variable/pattern/string} 全局替换,但只替换第一个匹配
- ${variable//pattern/string} 全局替换,替换全部出现的匹配
- ${varname^^} 转为大写
- ${varname,,} 转为小写
# 算术运算
算术运算:
- ((foo = 5 + 5)),只支持计算整数
- $[...] 是以前的写法,不建议使用
- echo $((2 + 2))
- 支持的算术运算符:+ - * / % ** ++ --
- 支持的位运算符:<< >> & | ~ ^
- 支持的逻辑运算符:> < >= <= == != && || ! expr1?expr2:expr3
# 脚本入门
Shebang行:
- 也就是第一行,用于指定解释器。一般为
#!/bin/sh
或#!/bin/bash
#!/usr/bin/env NAME
,表示查找解释器的目录。比如#!/usr/bin/env node
是指定node解释器,但node必须在环境变量中才行
脚本参数:
- 对于 script.sh word1 word2 word3 来说,脚本内部有一些特殊变量来引用参数
- $0:脚本文件名,即script.sh。
- $1~$9:对应脚本的第一个参数到第九个参数。
- $#:参数的总数。
- $@:全部的参数,参数之间使用空格分隔。
- $*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
- shift用来改变脚本参数,每次执行都会移除脚本当前的第一个参数($1),使得后面的参数向前一位,即$2变成$1、$3变成$2、$4变成$3,以此类推
- getops取出脚本所有的带有前置连词线(-)的参数。示例:
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;
h)
echo "h stands for h"
;;
a)
avalue="$OPTARG"
echo "The value provided is $OPTARG"
;;
?)
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"
$OPTARG保存的就是参数值
- 参数终止符 --,用来转义,把-变为普通参数。比如
cat -- -f``cat -- --f
基本指令:
- exit:退出当前脚本
- source:重新加载一个配置文件,也可以在脚本中加载一个外部库。简写是
.
- alias:别名
read指令: 将用户的输入存入一个变量
# 读取输入到一个变量
read text
# 读取输入到多个变量
read fn ln
# 不跟变量,使用$REPLY来取值
read
echo "REPLY = '$REPLY'"
# 读取文件内容,按行
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
参数:
- -t:设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行
- -p:指定用户输入的提示信息
- -a:把用户的输入赋值给一个数组,从零号位置开始
- -n:指定只读取若干个字符作为变量值,而不是整行读取
- -d:delimiter:定义字符串delimiter的第一个字符作为用户输入的结束,而不是一个换行符
- -r:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符
- -s:使得用户的输入不显示在屏幕上,这常常用于输入密码或保密信息
- -u fd:使用文件描述符fd作为输入
IFS用来指定分割标识,默认是空格
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
# 条件判断
if结构:
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
if test $USER = "foo"; then
echo "Hello foo."
else
echo "You are not foo."
fi
# then换行,则不需要分号
if true
then
echo 'hello world'
fi
# 写为一行
$ if true; then echo 'hello world'; fi
test命令:
# 写法一
test expression
# 写法二,注意两侧必须有空格
[ expression ]
# 写法三
[[ expression ]]
test的判断表达式是非常丰富的
- 文件判断
- [ -a file ]:如果 file 存在,则为true。
- [ -b file ]:如果 file 存在并且是一个块(设备)文件,则为true。
- [ -c file ]:如果 file 存在并且是一个字符(设备)文件,则为true。
- [ -d file ]:如果 file 存在并且是一个目录,则为true。
- [ -e file ]:如果 file 存在,则为true。
- [ -f file ]:如果 file 存在并且是一个普通文件,则为true。
- [ -g file ]:如果 file 存在并且设置了组 ID,则为true。
- [ -G file ]:如果 file 存在并且属于有效的组 ID,则为true。
- [ -h file ]:如果 file 存在并且是符号链接,则为true。
- [ -k file ]:如果 file 存在并且设置了它的“sticky bit”,则为true。
- [ -L file ]:如果 file 存在并且是一个符号链接,则为true。
- [ -N file ]:如果 file 存在并且自上次读取后已被修改,则为true。
- [ -O file ]:如果 file 存在并且属于有效的用户 ID,则为true。
- [ -p file ]:如果 file 存在并且是一个命名管道,则为true。
- [ -r file ]:如果 file 存在并且可读(当前用户有可读权限),则为true。
- [ -s file ]:如果 file 存在且其长度大于零,则为true。
- [ -S file ]:如果 file 存在且是一个网络 socket,则为true。
- [ -t fd ]:如果 fd 是一个文件描述符,并且重定向到终端,则为true。 这可以用来判断是否重定向了标准输入/输出/错误。
- [ -u file ]:如果 file 存在并且设置了 setuid 位,则为true。
- [ -w file ]:如果 file 存在并且可写(当前用户拥有可写权限),则为true。
- [ -x file ]:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true。
- [ FILE1 -nt FILE2 ]:如果 FILE1 比 FILE2 的更新时间更近,或者 FILE1 存在而 FILE2 不存在,则为true。
- [ FILE1 -ot FILE2 ]:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为true。
- [ FILE1 -ef FILE2 ]:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为true。
- 字符串判断
- [ string ]:如果string不为空(长度大于0),则判断为真。
- [ -n string ]:如果字符串string的长度大于零,则判断为真。
- [ -z string ]:如果字符串string的长度为零,则判断为真。
- [ string1 = string2 ]:如果string1和string2相同,则判断为真。
- [ string1 == string2 ] 等同于[ string1 = string2 ]。
- [ string1 != string2 ]:如果string1和string2不相同,则判断为真。
- [ string1 '>' string2 ]:如果按照字典顺序string1排列在string2之后,则判断为真。
- [ string1 '<' string2 ]:如果按照字典顺序string1排列在string2之前,则判断为真。
- 整数判断
- [ integer1 -eq integer2 ]:如果integer1等于integer2,则为true。
- [ integer1 -ne integer2 ]:如果integer1不等于integer2,则为true。
- [ integer1 -le integer2 ]:如果integer1小于或等于integer2,则为true。
- [ integer1 -lt integer2 ]:如果integer1小于integer2,则为true。
- [ integer1 -ge integer2 ]:如果integer1大于或等于integer2,则为true。
- [ integer1 -gt integer2 ]:如果integer1大于integer2,则为true。
- 正则判断
- [[ string1 =~ regex ]]
逻辑运算符
- AND运算:符号&&,也可使用参数-a。
- OR运算:符号||,也可使用参数-o。
- NOT运算:符号!。
写法上的小技巧
# 否定最好用转义引号括起来
[ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]
# 正则否定
if [[ ! "a" =~ ^[a-z] ]]; then echo 'true'; else echo 'false'; fi
算数判断
- ((...))作为算术条件
- 算术计算的结果是非零值,则表示判断成立。((1))=true
- 可以为变量赋值。(( foo = 5 ))
case结构
case $character in
1 ) echo 1
;;
2 ) echo 2
;;
3 ) echo 3
;;
* ) echo 输入不符合要求
esac
case "$OS" in
FreeBSD) echo "This is FreeBSD" ;;
Darwin) echo "This is Mac OSX" ;;
AIX) echo "This is AIX" ;;
Minix) echo "This is Minix" ;;
Linux) echo "This is Linux" ;;
*) echo "Failed to identify this OS" ;;
esac
Bash 4.0之前,只要条件匹配就会退出。4.0之后,可以使用;;&
来防止跳出
# 循环
while循环。符合条件,则一直执行
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
while true
do
echo 'Hi, while looping ...';
done
while true; do echo 'Hi, while looping ...'; done
until循环。不符合条件,则一直执行
until false; do echo 'Hi, until looping ...'; done
for...in循环
for i in word1 word2 word3; do
echo $i
done
for i in *.png; do
ls -l $i
done
# in list的部分可以省略,这时list默认等于脚本的所有参数$@
for filename; do
echo "$filename"
done
for循环
for (( i=0; i<5; i=i+1 )); do
echo $i
done
select结构。生成一个带编号的菜单,由用户选择编号,然后执行对应编号内容的指令
select brand in Samsung Sony iphone symphony Walton
do
echo "You have chosen $brand"
# 这里一般和case指令搭配使用
done
# 函数
函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。
函数的定义
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
删除函数使用unset -f functionname
列出当前shell所有的函数declare -f
函数参数取值,和脚本参数取值一致
变量
# 直接声明的变量是全局变量
fn () {
foo=1
echo "fn: foo = $foo"
}
# 使用local命令创建局部变量
fn () {
local foo
foo=1
echo "fn: foo = $foo"
}
# 数组
创建
# 逐个赋值
array[0]=val
array[1]=val
array[2]=val
# 一次性赋值
array=(a b c)
array=([2]=c [0]=a [1]=b)
names=(hatter [5]=duchess alice)
mp3s=( *.mp3 )
读取
# 根据索引读取
echo ${array[0]}
# 不指定索引,就是默认0位
echo ${array}
# 读取所有成员
echo ${foo[@]}
# 或
echo ${foo[*]}
#配合for使用的话,要加双引号
for act in "${activities[@]}";...
# 拷贝数组最方便的就是
hobbies=( "${activities[@]}" )
长度
${#array[*]}
${#array[@]}
序号
${!array[@]}
${!array[*]}
for i in ${!arr[@]};do
echo ${arr[i]}
done
提取、追加和删除
${array[@]:position:length}
foo+=(d e f)
unset foo[2]
新版本的关联数组:Map
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
echo ${colors["blue"]}
# set、shopt命令
set用来修改子 Shell 环境的运行参数
- set -u。遇到不存在的变量就会报错,并停止执行。和set -o nounset等值
- set -x。在运行结果之前,先输出执行的那一行命令
- set -e。脚本只要发生错误,就终止执行。默认情况下,会继续执行
- set -o pipefail。适用于管道命令的-e加强版
- set -E。-e会导致函数内部的错误不会被trap捕获,-E可以纠正这个行为
- set -n。等同于set -o noexec,不运行命令,只检查语法是否正确
- set -f。等同于set -o noglob,表示不对通配符进行文件名扩展
- set -v。等同于set -o verbose,表示打印 Shell 接收到的每一行输入
- set -o noclobber。防止使用重定向运算符>覆盖已经存在的文件
set一般放在脚本的头部,或者执行的时候传入这些参数
shopt和set作用类似,区别是set是从Ksh继承的,是POSIX规范的一部分。而shopt是bash特有的
# 脚本除错
有时候脚本出错会是致命的,比如
dir_name=/path/not/exist
cd $dir_name
rm *
如果cd出错,那么可能会删除当前目录中的所有文件。正确的写法应该是这样
[[ -d $dir_name ]] && cd $dir_name && rm *
有一些环境变量常用于除错
- LINENO。返回脚本里命令所在的行号
- FUNCNAME。返回一个数组,内容是当前的函数调用堆栈
- BASH_SOURCE。返回一个数组,内容是当前的脚本调用堆栈
- BASH_LINENO。返回一个数组,内容是每一轮调用对应的行号
# mktemp、trap命令
我们常常会要使用到临时文件,应该遵循以下规则
- 临时文件名要是唯一的,防止冲突和被人利用
- 临时文件要有权限的控制
- 脚本退出时,要删除临时文件
mktemp命令用来创建临时文件
trap 'rm -f "$TMPFILE"' EXIT
# 为了保证安全,如果失败,则退出
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
mktemp创建的临时文件,文件名是随机的,权限只有本人能读写。
常用参数
- -d。创建临时目录
- -p。指定临时目录所在
- -t。指定文件名模板,比如-t mytemp.XXXXXXX
trap命令用来在 Bash 脚本中响应系统信号。
最常见的就是SIGINT(中断),即按 Ctrl + C 所产生的信号。
trap -l列出所有的系统信号,常用的信号有
- HUP:编号1,脚本与所在的终端脱离联系。
- INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行。
- QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
- KILL:编号9,该信号用于杀死进程。
- TERM:编号15,这是kill命令发出的默认信号。
- EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。
trap命令的格式
trap [动作] [信号1] [信号2]
# 脚本遇到EXIT信号会执行删除临时文件。一般都写在行首
trap 'rm -f "$TMPFILE"' EXIT
# 退出的时候执行多条命令,则需要封装一个函数
function egress {
command1
command2
command3
}
trap egress EXIT
# 启动环境
Session登录,会进行整个环境的初始化,启动的初始化脚本依次如下。
- /etc/profile:所有用户的全局配置脚本。
- /etc/profile.d/目录里面所有.sh文件
- ~/.bash_profile:用户的个人配置脚本。如果该脚本存在,则执行完就不再往下执行。
- ~/.bash_login:如果~/.bash_profile没找到,则尝试执行这个脚本(C shell 的初始化脚本)。如果该脚本存在,则执行完就不再往下执行。
- ~/.profile:如果~/.bash_profile和~/.bash_login都没找到,则尝试读取这个脚本(Bourne shell 和 Korn shell 的初始化脚本)。
一般不要修改/etc/profile,因为Linux发行版更新的时候,会更新它。
如果想修改你个人的登录环境,一般是写在~/.bash_profile里面
非登录Session的初始化脚本依次如下。
- /etc/bash.bashrc:对全体用户有效。
- ~/.bashrc:仅对当前用户有效。
~/.bash_logout脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作
Bash 允许用户定义自己的快捷键。全局的键盘绑定文件默认为/etc/inputrc,你可以在主目录创建自己的键盘绑定文件.inputrc文件
# 命令提示符
环境变量PS1,是当前命令提示符的定义。
默认的PS1一般为[\u@\h \W]\$
,命令提示符的定义如下。
- \a:响铃,计算机发出一记声音。
- \d:以星期、月、日格式表示当前日期,例如“Mon May 26”。
- \h:本机的主机名。
- \H:完整的主机名。
- \j:运行在当前 Shell 会话的工作数。
- \l:当前终端设备名。
- \n:一个换行符。
- \r:一个回车符。
- \s:Shell 的名称。
- \t:24小时制的hours:minutes:seconds格式表示当前时间。
- \T:12小时制的当前时间。
- @:12小时制的AM/PM格式表示当前时间。
- \A:24小时制的hours:minutes表示当前时间。
- \u:当前用户名。
- \v:Shell 的版本号。
- \V:Shell 的版本号和发布号。
- \w:当前的工作路径。
- \W:当前目录名。
- !:当前命令在命令历史中的编号。
- #:当前 shell 会话中的命令数。
- $:普通用户显示为$字符,根用户显示为#字符。
- [:非打印字符序列的开始标志。
- ]:非打印字符序列的结束标志。
也可以设置颜色:PS1='\[\033[0;41m\]<\u@\h \W>\$\[\033[0m\] '
环境变量PS2是命令行折行输入时系统的提示符,默认为>。 环境变量PS3是使用select命令时,系统输入菜单的提示符。 环境变量PS4默认为+。它是使用 Bash 的-x参数执行脚本时,每一行命令在执行前都会先打印出来,并且在行首出现的那个提示符。