Bash学习笔记
学习阮一峰《Bash脚本教程》笔记
快捷键:
ctrl+L: 清除屏幕并移到顶部ctrl+U: 删到行首(剪) ctrl+A: 移到行首ctrl+K: 删到行尾(剪) ctrl+E: 移到行尾ctrl+b, f, (左移, 右移)alt+b, f(前, 后移一个单词)ctrl+1等同于clearctrl+ddelete, ctrl+w 删到单词首alt+t词换位, ctrl+t 字符换位 (与前一个)alt+d,alt+backspace剪切至词首, 词尾ctrl+y粘贴
"Something wrong happend" >&2解释:
- >代表重定向输出
- &表示接下来的是一个文件描述符数字(file descriptor number)
- 2表示
stderr - 所以就是把上述字符串输出到stderr里去的意思, 如果是
”str” >2(没有&), 则会输出到一个叫2的文件里去
echo
- -n 可以取消结尾的换行
- -e 解释字符串里的转义符
扩展(通配符):
~/walker表示用户目录下的walker目录~walker表示名为walker的目录~+扩展为当前目录, 等同于pwdecho d{a,e,i,o,u}g输出: dag, deg, dig, dog, dug. —>大括号内不要有空格(否则会当成参数)echo {j{p,pe}g, png}—> 嵌套
波浪, 方括号的括号都是基于”路径”的, 如果当前路径没有匹配到对应的文件名, 则会(变成字符串)原样输出,
而大括号则不然, 是基于”逻辑”的, 只管扩展, 不会去探测扩展后对应的路径存不存在, 因此可能报错文件不存在
如:
echo [a,b].txt, 如果不存在a.txt, b.txt, 则会变成”[a,b].txt”这样一个输出,- 而
{a,b}.txt则一定地会扩展成a.txt, b.txt - 例外: 在用..来扩展时, 如果无法理解, 则不会扩展, 如
{1..5}会扩展成1,2,3,4,5, 但{ab..123}, 则会变成字符串 - 但是前导0不参与路径匹配:
{01…5} # 01,02,03,04,05(几个零都可以) - 步长:{0..8..2} (未演示成功) # 要打开哪个shopt开关?
活用
echo .{mp{3..4},m4{a,b,p,v}}# 匹配了: .mp3 .mp4 .m4a .m4b .m4p .m4vmkdir {2007..2009}-{01..12}# 建了2007-2009每年12个目录 for I in {1..4}echo ${!S*}# 返回所有以S开头的”变量名”, 如SHELL, SSH..等
另两种转义(string interpolation):
echo date is $(date), 即包在$(…)中echo date is `date`, 包在反引号中 但是要计算2+2, 只有echo $((2+2)) 这种形式(两层括号)
[[:alnum:]], [[:digit:]]等预置的字符类扩展见: https://wangdoc.com/bash/expansion.html
-
(?, *, +, @, !)则为匹配的个数, 分别是(0或1, 0或多, 一或多, 一个, 非一个), 如song@(.)mp3等同于song.mp3, (需要打开shopt -s extglob)
-
双引号碰到
$, `和反斜杠都会自动扩展, 所以echo “$SHELL”等同于echo echo $SHELL -
双引号能保留”输出”的格式, 比如
echo `cal`, 格式就没了, 但是echo “$(cal)”则不同, 自己试
command << token
Texts
token
等同于: echo Texts | command 即把echo的输出作为command的输入, 这个一般用于多行文本(texts里面可以进行使用变量, 但是如果把token用双引号包起来就不能解释变量了).
如果只是简单字符串, 用下面更明确:
command <<< ‘text’ 如: cat <<< “hello world”, 一样等同于echo “hello world” | cat
<<<还有一个作用就是把变量值用这种方式能变成标准输入, 这样被”计算”出来的值也能用于只接受标准输入的命令了, 比如(read)
变量
环境变量:
printenv PATH 与 echo $PATH等同
解释变量中的变量, 比如$PATH, 可以写成:
$ myvar=PATH
$ echo ${!myvar},
即多加一个惊叹号, 而不是想象中的$嵌套: ${${myvar}}
$?上一个命令的退出码(0成功, 1失败)$$当前Shell进程的ID$_上一个命令的最后一个参数$!最后一个后台执行的异步命令的进程ID$0当前Shell的名称$-当前shell的启动参数,$@,$#表示脚本的参数数量
取变量
${varname-:value}取值, 如果不存在则返value, 但不赋值${varname=:value}取值, 如果不存在则返value, 顺便赋值${varname+:value}如果有值则返value(而不是值本身), 没有值则为空, 所以这个时候的value一般用一个标识符号就好了${varname?:value}取值, 如果不存在就报错并把value作为错误错误打印出来 比如filename=${$1:?”filename missing”}从脚本中取第一个参数作为文件名, 发现没有文件名就报错退出
变量值都是字符串, 可以用declare来进行一些限定
$ declare -I v1=13 v2=14 v3=v1+v2
$ echo $v3
# 这样更快:
$ let v=13+14 如果用空格, 则加引号: $ let “v = 13 + 14”
字符串
${#”string”}长度,${varname:offset:length}切片(变量名不需要美元符号) 删除: (# 和 ## 的区别就是贪婪与否的区别)
$ phone="555-456-1414"
$ echo ${phone#*-} # 从最后一个-开始删
456-1414
$ echo ${phone##*-} # 从第一个-开始删
1414
替换: ${variable/#pattern/string} 注意, # 左边多了一个 / , 右边多了替换字串
-
以上, 都是从头匹配, 从尾部匹配把
#换成%任意位置匹配则换成/, 所以就成了你们最熟悉的语法:varname/search/replace这个时候再回头看/#,/%, 不过是/语法的修饰符罢了(限定起始方向) -
${varname^^}, ${varname,,}# 转大写, 转小写
数值运算
逗号是求值, 如
echo $((foo = 1+2, 3*4))
输出为12, 但foo的值是3, 依次计算, 输出是逗号后面的
expr命令等同于双括号: expr 3+5 与$((3+5))同义
行操作
Bash内置Readline库, 默认采用Emacs快捷键, 切换:
set -o vi 或 $ set -o emacs
切换目录堆栈
- 不管你CD到哪个了哪个目录, 想回到CD前的目录, 用
cd -就行了 pushd,popd则可以把目录推到堆栈里, 演示:
pushd a, pushd b, pushd c此时目录就到了~/a/b/c- 查看堆栈: dirs
- 然后
cd /abc- 这个时候再查看堆栈, 成了
/abc, ~/b, ~/c
- 这个时候再查看堆栈, 成了
- 然后你再
cd /usr- 堆栈变成了
/usr, ~/b, ~/c
- 堆栈变成了
现在你知道 了, CD永远只是把更改顶栈, 大多数情况下, 你可以用pushd来替换cd, 这样你就有了后退权了,此时你再popd, 目录会顺利切到~/b, 不管你进行过多少次cd, 在堆栈里~/b位置没变那就永远会被pop出来
如果你查看堆栈, 要从第4个开始后退(0为起始), 那么可以把从3开始(不是4)的记录提到顶层来(然后再popd):
- pushd +3 (加号不可省), 注意, 此时0, 1, 2都还在, 只是挪到了尾巴
- 而popd +3则不是”移动”, 而是删除了, 意思是正向删除第3个, 如果不带+, 则理解为删除3以后的所有堆栈(即从5开始)
注: 为什么要从第4个开始退要把从3开始的移到顶层呢? 因为如下dirs: /1, /2, /3 , 你做popd, 是会回到/2的, 所以你想要变更后退的顺序, 只能多包一层, 把顶层也一起移动(因为pushd, popd+数字改动的只是堆栈, 不是目录, 然后通过修改过的堆栈去跳目录, 换句话说, 你没有办法”跳到”堆栈中的顶层去,(严格来说, 顶层即当前目录)
脚本
#!/usr/bin/env bash的写法是为了避免#!/bin/bash这种写法时bash不在bin目录- source命令可以 用一个点来表示:
. ~/.bash_profile - 读用户的输入:
$ read firstname lastname
$ “you input: $firstname, $lastname” # 如果read后没有给变量名, 则由默认的$REPLY来取出
- 读文件:
while read myline # 每次读一行
do
echo "$myline"
done < $filename # 注意这里特殊的传参方式, 同时, 如果不传入文件路径, 就是一个无限循环了(read)
- 存数据
$ read -a varname. # 这就把用户的多个输入全存到varname这个数组里了
$ read -e -p “please input the path to the file” # -e参数使得用户在输入的时候能用tab补全(包含所有readline库快捷键), 如果没有这个参数输入文本的时候是不能使用快捷键的
$ read -s -p “input password” # 可以隐藏用户的输入, 通常用于密码
-p 显然就是能直接显示输入前的提示了
条件判断
if test $USER = "foo"; then
echo "Hello foo."
[elif commands; then
commands...]
else
echo "You are not foo."
fi
if里面的test命令
test expression,[ expression ],[[ expression ]]是等价的(第三种支持正则) # 空格不能省[ -? file ]查看文件状态有非常多的表达式(参数), 具体参阅https://wangdoc.com/bash/condition- 比如
-e /tmp/foo.txt是检查文件是否存在
- 比如
- 字符串判断,正则判断,数字判断等,具体用法查阅文档
循环
有while, until(while的反条件), for...in, for循环
for [ test ] in list; do … done其中的in list如果省略, 则代表所有脚本参数”$@“:
$ for filename; do echo “$filename”; done
# 等同于
$ for filename in “$@“; do echo “$filename”; done
同理, 如果是用在函数中, 则等于所有函数参数.
case in, 如果希望一个匹配后继续做下一个匹配, 每一个case 的结尾用;;&而不是;;(多了一个&)- for 循环:
for (( I=0; I<5; I+=1 )); do echo $i; done# 用双括号, 变量也无需加$了
select
echo "Which Operating System do you like?"
select os in Ubuntu LinuxMint Windows8 Windows10 WindowsXP
do
case $os in
"Ubuntu"|"LinuxMint")
echo "I also use $os."
;;
"Windows8" | "Windows10" | "WindowsXP")
echo "Why don't you try Linux?"
;;
*)
echo "Invalid entry."
break
;;
esac
done
*?命令除了取出上一个命令的返回值, 也可以取出上一个函数的返回值
数组
以下方式声明数组
names=(hatter [5]=duchess Alice), 指定了0, 5, 6, 其它为空字符串mp3s=( *.mp3 )declare -a ARRAYNAMEread -a ARRAYNAME- 读取的时候:
echo ${array[1]}大括号不可省 @仍然是返回所有元素:echo ${array[@]}- 但是在
for…in中, 要把整个表达式放双引号中:
- 但是在
activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
for act in “${activities[@]}”; do….; done
不然其中有”water”, “skiing”,”white-water”, “water rafting”等都会被拆开(bug吧? 字符串也拆)
- 把
@换成*, 加上双引号, 则会一个个字符返回
拷贝数组最方便的方法:
hobbies=( “${activities[@]}” diving )
# 可见,不要忘了双引号。 顺便演示了为数组添加成员
- 直接赋值给一个数组(即没有指定索引), 则是赋给第0个组员, 同理, 使用数组名也是使用的0号组员
echo ${#array[@或*]}返回数组长度#仍然用以计数, 但是如果传的是具体索引, 则返回的是对应项的字符串长度echo ${!array[@或*]}返回有值的索引 (为空的不返回) # 活用的话遍历数组更高效echo ${array[@]:2:3}切片,2,3分别为position和lengtharr+=(3 4 5)追加unset arr[2]删除- 或:
arr[2]=或arr[2]=‘’三者等效,
- 或:
- 根据上面知识
arr=表示删除第一个成员, 但是unset arr则是清空整个数组了 - 也可以用字符串做索引, 就成了字典了:
declare -A colorsa变成变成大写A即可colors[“red”]=“#ff0000”echo ${colors["red]}
set 和 shopt
set
- 单独一个set会显示所有环境变量和Shell函数
- 以下都可以以
set -xxx的方式写在脚本头或任何位置, 就当一个即时开关使用吧 - 也可以在调用bash脚本前传入比如:
bash -eux script.sh- -u 遇到不存在的变量就报错, 而不是忽略 与 -o nounset 等价
- -x 每一个命令执行前会先打印出来 等同于 -o xtrace, 关闭用set +x (组合起来用就是一个小* 环境)
- -e 有错误就中止 等同于 -o errexit
- -o pipefail 即使在管道中, 有错也中止(-e 在管道中会失效)
- -n: -o noexec 不执行命令只检查语法
- -f: -o noglob 不对通配符进行文件名(路径)扩展 可用+f 关闭
- -v: -o verbose 打印shell接收到的每一行输入 可用+v 关闭
set -euxo pipefail一般这么四个连用
一旦设置了-e参数,会导致函数内的错误不会被trap命令捕获(参考《trap 命令》一章)。-E参数可以纠正这个行为,使得函数也能继承trap命令。所以现在一般这么用:
# 写法一
set -Eeuxo pipefail
# 写法二
set -Eeux
set -o pipefail
shopt
- 即: shell option
- 同
set, 直接shopt也可以列出所有参数,-s,-u分别是是打开, 关闭某个参数shopt 参数名, 可直接查询该参数是否打开关闭, 但是如果是编程, 因为返回是字符串, 所以提供了-q参数(返回0/1, 分别表示打开/关闭) if shopt -q globstar; then …; fi
脚本除错
[[ -d $dir_name ]] && cd $dir_name && echo rm *- ↑↑↑先看目录存不存在, 然后再进入, 然后再打印出来将要删除的文件, 这是最安全的删除方法 否则一旦目录不存在, 不同的写法会有不同的问题
- 如果在执行bash脚本前加入
-x参数, 则每一条命令执行前都会打印出来- 等同于
set -x - 或者写在脚本的shebang行里也行
- 打印的每一条命令前会加上一个标识符作前缀, 默认是
+, 可以用export PS4=‘$LINENO +’这种方式自定义PS4的值(比如现在就加上了行号)
- 等同于
几个环境变量
LINENO这个变量在哪, 打印的就是这一行的行号FUNCNAME返回一个数组, 函数调用的名称堆栈, 最里层(即本函数)的是0BASH_SOURCE返回一个数组, 函数调用的脚本堆栈, 即每层调用的脚本是哪一个, 最里层(即本文件)的是0BASH_LINENO返回一个数组, 函数每一次被调用时在该脚本的行号, 同样也是从最里层开始${BASH_LINENO[$i]}跟${FUNCNAME[$i]}是一一对应关系,表示${FUNCNAME[$i]}在调用它的脚本文件${BASH_SOURCE[$i+1]}里面的行号。
例:
${BASH_SOURCE[1]} = main.sh[0] 是文件本身, 所以要[1]${BASH_LINENO[0]} = 17调用来源的行号 —> 所以调用来源的行号的索引永远比调用来源(文件)的索引要小1${FUNCNAME[0]} = hello# 本方法(或者说”被调用的方法”)。代表在 main.sh的17行调用了hello()方法
#!/bin/bash
source lv2.sh # 引入外部脚本
function lv1method()
{
echo ---------lv1------------
i=0
for v in "${BASH_LINENO[@]}"; do
echo "bash_line_no[$((i++))]: $v"
done
i=0
for v in "${FUNCNAME[@]}"; do
echo "func_name[$((i++))]: $v"
done
i=0
for v in "${BASH_SOURCE[@]}"; do
echo "bash_source[$((i++))]: $v"
done
lv2method # 调用外部脚本的方法
}
以上脚本, 多做几次嵌套, 打印出来看看索引之间的关系
临时文件
直接创建临时文件,尤其在/tmp目录里面,往往会导致安全问题。
- 首先,/tmp目录是所有人可读写的,任何用户都可以往该目录里面写文件。创建的临时文件也是所有人可读的。
- 其次,如果攻击者知道临时文件的文件名,他可以创建符号链接,链接到临时文件,可能导致系统运行异常。攻击者也可能向脚本提供一些恶意数据。因此,临时文件最好使用不可预测、每次都不一样的文件名,防止被利用。
- 最后,临时文件使用完毕,应该删除。但是,脚本意外退出时,往往会忽略清理临时文件。
生成临时文件应该遵循下面的规则:
- 创建前检查文件是否已经存在。
- 确保临时文件已成功创建。
- 临时文件必须有权限的限制。
- 临时文件要使用不可预测的文件名。
- 脚本退出时,要删除临时文件(使用trap命令)。
---> mktemp,生成一个文件名随机,且权限只有本人可以读写的文件
安全的用法:
trap 'rm -f "$TMPFILE"’ EXIT退出时删除临时文件)TMPFILE=$(mktemp) || exit 1用mktemp命令建立临时文件可以只有本人能读, 如果失败就退出- echo "Our temp file is $TMPFILE”
- 参数: -d 创建的是目录, -p 指定目录 -t 指定模板,
- 如
mktemp -t aaa.XXXXXXX能生成/tmp/aaa.yZ1HgZV(与X个数相同) trap是用来响应系统信号的, 如ctrl+c产生中断信号SIGINTtrap -l列出所有信号,
trap 命令接的信号有如下
- HUP:编号1,脚本与所在的终端脱离联系。
- INT:编号2,用户按下 Ctrl + C,意图让脚本中止运行。
- QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
- KILL:编号9,该信号用于杀死进程。
- TERM:编号15,这是kill命令发出的默认信号。
- EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。
如果trap要执行多条命令, 可以封装到函数里, 命令的位置写函数:
trap func_name EXIT
启动环境
登录session依次启动如下脚本:
/etc/profile/etc/profile.d目录下的所有.sh文件~/.bash_profile如果有, 则中止~/.bash_login如果有, 则中止 此为C shell 初始化脚本~/.profileBourne shell 和 Korn shell 初始化脚本- 通过
bash --login参数, 可以强制执行以上脚本
非登录session
/etc/bash.bashrc所有用户都执行~/.bashrc当前用户的
启动参数:
- -n: 不执行脚本, 只检查语法
- -v: 执行语句前先输出
- -x: 执行语句后输出该语句
~/.bash_logout退出时要执行的命令include /etc/inputrc在~/.inputrc里加这一行, 可以在里面自定义快捷键
命令提示符
- 上面提到过
$PS4能修改set -x时打印的每句语句前面的+号 - 命令提示符过默认的符号(根用户是#号)则可以用`PS1`来修改, 怎么改参考https://wangdoc.com/bash/prompt.html
PS2表示的是输入时折行的提示符, 默认为>PS3表示使用select命令时系统输入菜单的提示符
实例
最后,来两个简单demo,第一个,根据规则重命名文件(夹),因为是硬性映射,之前每次都是手动重命名,想到不如把映射规则写到一个脚本里,这样循环执行mv命令就好了:
#!/bin/bash
# 接受目标文件夹位置
path=$1
echo executing path: $path
# list dirs
for entry in "$path"/*; do
case $entry in
"$path"/Chinese)
mv $entry $path/zh-CN
;;
"$path"/English)
mv $entry $path/en-US
;;
"$path"/German)
mv $entry $path/de-DE
;;
"$path"/French)
mv $entry $path/fr-FR
;;
"$path"/Italian)
mv $entry $path/it-IT
;;
"$path"/Norwegian)
mv $entry $path/nb-NO
;;
"$path"/Swedish)
mv $entry $path/sv-SE
;;
"$path"/Dutch)
mv $entry $path/nl-NL
;;
"$path"/Spanish)
mv $entry $path/es-ES
;;
"$path"/Danish)
mv $entry $path/da-DK
;;
*)
echo $entry
;;
esac
done
因为for...in语法能直接遍历文件夹,省去了一个ls命令。
第二个,写了个node脚本,因为传参比较多,打算用一个bash脚本包一下,这样既可以保存脚本,也方便编辑参数
#!/bin/bash
author='walker'
projectname='MyProjName'
passkeys='code,flag,message'
fileuri='/path/to/you/file'
offset=1
debug=true
verbose=false
modulename='taskCommon'
httpclient='MyRESTBaseClient'
models=(
#group1
modelAreaListItem,modelAreaJuniorDOListItem
#group2
modelHospitalSearchResult,modelHospitalSearchResultItem
#group3
modelHospitalDetail
#group4
modelHospitalDeptSearchResult,modelDeptSearchResultItem
)
for model in ${models[@]}; do
types=${types}${model},
done
types=${types}
if [ "$debug" = true ] ; then
enableDebug='--debug'
fi
if [ "$verbose" = true ] ; then
enableVerbose='--verbose'
fi
./app.js ${enableVerbose} ${enableDebug} \
-C ${types} \
-P ${passkeys} \
-f ${fileuri} \
-a ${author} \
-m ${modulename} \
-j ${projectname} \
-B ${httpclient} \
-o ${offset}
- 我保留了
models是为了明示数组是用空格分隔,而不是逗号 types=${types}这一句我忘记了当初为什么要加- 最后可以看到,就是一个简单的调用,其它的都是把脚本里的变量拼上去
再一个示例,自定义hosts地址,如果有匹配则更改,否则新增
#!/bin/bash
# insert/update hosts entry
ip_address="192.168.x.x"
host_name="my.hostname.example.com"
# find existing instances in the host file and save the line numbers
matches_in_hosts="$(grep -n $host_name /etc/hosts | cut -f1 -d:)"
host_entry="${ip_address} ${host_name}"
echo "Please enter your password if requested."
if [ ! -z "$matches_in_hosts" ]
then
echo "Updating existing hosts entry."
# iterate over the line numbers on which matches were found
while read -r line_number; do
# replace the text of each line with the desired host entry
sudo sed -i '' "${line_number}s/.*/${host_entry} /" /etc/hosts
done <<< "$matches_in_hosts"
else
echo "Adding new hosts entry."
echo "$host_entry" | sudo tee -a /etc/hosts > /dev/null
fi