Learn Bash实践
本教程根据 Bash脚本教程, 经过自身实践, 而得. 作为笔记性质存在.
https://wangdoc.com/bash/
2. 基本语法
-
echo -n(取消换行) -e(解释转义字符)
-
快捷键:
- ctrl + L : clear
- ctrl + U: 从光标位置删除到行首
- ctrl + PgUp: 切换上一个Bash
3. 模式扩展
Shell 先扩展命令, 后执行.
- **~ **: 用户目录
- ? : 任意单个字符
- * : 任意个任意字符
- **[…] **: (方括号扩展) 匹配方括号内的 任 一个 字符
- [!..] 和 [^…] : 非方括号内的字符
举例: [!a-zA-Z] : 非 a-z, A-Z 的字符
- {…} : (大括号扩展) 扩展为大括号内的所有值
大括号扩展不是文件扩展, 会强行扩展为所有. 一般在创建文件时使用.
- {start…end} : 扩展为连续序列
{start…end…step} : 指定步长
- $xxx : 变量扩展 ${变量名}
${!string*}
或${!string@}
返回所有匹配给定字符串string
的变量名。
- $(…) : 子命令扩展, 返回 子命令的执行结果
老式语法: echo `date`
Tue Jan 28 00:01:13 CST 2020
-
$((…)) : 算数扩展 echo $((2 + 2))
-
[[:class:]] : 字符类 (文件匹配)
[[: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 [[:lower:]]* 匹配小写字母开头的所有文件
[![:digit:]]
(或[^[:digit:]]
) 匹配所有非数字。
- 量词语法
量词是正则表达式中的一种元字符,用于指定匹配模式的重复次数。
?(pattern-list)
:模式匹配零次或一次。*(pattern-list)
:模式匹配零次或多次。+(pattern-list)
:模式匹配一次或多次。@(pattern-list)
:只匹配一次模式。!(pattern-list)
:匹配给定模式以外的任何内容。
举例:
$ ls abc?(.)txt abctxt abc.txt $ ls abc+(.txt) abc.txt abc.txt.txt
shopt
命令可以调整 Bash 的行为。它有好几个参数跟通配符扩展有关。
参考: https://wangdoc.com/bash/expansion
4. 引号和转义
- **\ **: 用于转义
- **‘xxx’ 单引号 **: 特殊字符(*, $, \) 在单引号内都变成普通字符
- **“xxx” 双引号 **: 部分特殊字符($, `, \) 会被自动扩展. ( 变量, 子命令扩展, 转义)
- Here 文档 : 处理多行文本的概念.
<<[DELIMITER] Your text here [DELIMITER]
- Here 字符串: 处理 多行字符串
$ md5sum <<< 'ddd'
# 等同于
$ echo 'ddd' | md5sum
5. 变量
1. 环境变量
Bash 环境自带的变量, env
可以展示
2. 创建变量
如果变量的值包含空格,则必须将值放在引号中。(Bash所有变量都是字符串)
myvar="hello world"
e=$(ls -l foo.txt) # 变量值可以是命令的执行结果
f=$((5 * 7)) # 变量值可以是数学运算的结果
3. 读取变量
$变量名
变量名也可以使用花括号${}
包围。这种写法可以用于变量名与其他字符连用的情况。
$ echo $a_file
$ echo ${a}_file
foo_file
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值。
$ myvar=USER
$ echo ${!myvar}
ruanyf
4. 删除变量
unset NAME
删除一个变量,也可以将这个变量设成空字符串。
$ foo=''
$ foo=
5. 输出变量
为了把变量传递给子 Shell,需要使用export
命令, 对于子 Shell 来说就是环境变量。
export NAME=value
6. 特殊变量
- $? : 上一个命令的退出码,用来判断上一个命令是否执行成功。
0
表示成功. - $$ : 当前Shell 的进程ID.
- $_ : 上一个命令的最后一个参数.
- $! : 最近一个后台执行的异步命令的进程ID.
- $0 : 当前Shell 名称.
- $- : 当前Shell 的启动参数.
- $@ : 脚本参数数量.
- $# : 脚本参数值.
7. 变量默认值
- 返回一个默认值
${varname:-word}
如果变量varname存在且不为空,则返回它的值,否则返回word。它的目的是返回一个默认值,比如${count:-0}表示变量count不存在时返回0。
- 设置变量默认值
${varname:=word}
如果变量varname
存在且不为空,则返回它的值,否则将它设为word
,并且返回word
。它的目的是设置变量的默认值,比如${count:=0}
表示变量count
不存在时返回0
,且将count
设为0
。
- 检测变量是否存在
${varname:+word}
如果变量名存在且不为空,则返回word
,否则返回空值。它的目的是测试变量是否存在,比如${count:+1}
表示变量count
存在时返回1
(表示true
),否则返回空值。
- 变量存在则返回变量值 , 变量不存在 , 返回message
${varname:?message}
如果变量varname
存在且不为空,则返回它的值,否则打印出varname: message
,并中断脚本的执行。如果省略了message
,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,比如${count:?"undefined!"}
表示变量count
未定义时就中断执行,抛出错误,返回给定的报错信息undefined!
。
- 脚本参数
如果用在脚本中,变量名的部分可以用数字1
到9
,表示脚本的参数。
filename=${1:?"filename missing."}
上面代码出现在脚本中,1
表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
8. declare 命令
declare
命令用于声明变量及其属性。
declare [options] variable=value
declare
命令的主要参数(OPTION)如下。
-a
:声明数组变量。-f
:输出所有函数定义。-F
:输出所有函数名。-i
:声明整数变量。-i
参数声明整数变量以后,可以直接进行数学运算。-l
:声明变量为小写字母。可以自动把变量值转成小写字母。-p
:查看变量信息。-r
:声明只读变量。无法改变变量值,也不能unset
变量。-u
:声明变量为大写字母。可以自动把变量值转成大写字母。-x
:该变量输出为环境变量。
declare -i num=5 # 声明一个整数
declare -a arr=(1 2 3) # 声明一个数组
declare -r readonly_var="This is read-only" # 声明一个只读变量
declare -x export_var="Exported variable" # 将变量标记为环境变量
9. readonly 命令
readonly
命令等同于declare -r
,用来声明只读变量,不能改变变量值,也不能unset
变量。
10. let 命令
let
是一个用于执行算术运算和赋值的命令。它可以用来进行整数运算,而且它是可选的,因为在许多情况下,Bash 变量也可以直接进行算术运算而无需 let
。
基本语法如下:
let expression
其中,expression
是一个包含算术表达式的字符串。let
将计算该表达式并将结果赋给变量。以下是一些示例:
# 使用 let 进行整数运算
let "sum = 5 + 3"
echo $sum # 输出: 8
# 变量的自增
let "counter++"
echo $counter # 输出: 1
# 使用变量进行计算
num1=10
num2=5
let "result = num1 * num2"
echo $result # 输出: 50
let
也可以用于在条件语句中进行算术比较:
# 使用 let 进行算术比较
num1=10
num2=20
if let "num1 < num2"; then
echo "num1 is less than num2"
fi
虽然 let
在某些情况下很有用,但在 Bash 中,通常也可以直接使用 $(( ))
或 (( ))
来执行算术运算,而无需使用 let
。例如:
# 使用 $(( )) 进行算术运算
result=$((num1 + num2))
echo $result # 输出: 30
或者
# 使用 (( )) 进行算术运算
((result = num1 * num2))
echo $result # 输出: 50
这两种方式都是执行算术运算的常见形式,而 let
是其中之一。
6. 字符串操作
1. 获取字符串长度
${#varname}
2. 提取子字符串
${varname:offset:length}
# varname只能是变量
# offset可以为负, 从后往前数,但是还是从前往后截取.
# length可省略, 截取到末尾
foo="This string is long."
echo ${foo: -5:-2}
lon
3. 搜索和替换
1. 字符串头部的模式匹配
如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。
# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}
# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}
myPath=/home/cam/book/long.file.name
echo ${myPath#/*/}
cam/book/long.file.name
echo ${myPath##/*/}
long.file.name
替换头部部分:
# 模式必须出现在字符串的开头
${variable/#pattern/string}
# 示例
foo=JPG.JPG
echo ${foo/#JPG/jpg}
jpg.JPG
2. 尾部模式匹配
如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable%pattern}
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable%%pattern}
将尾部匹配的部分,替换成其他内容
# 模式必须出现在字符串的结尾
${variable/%pattern/string}
# 示例
$ foo=JPG.JPG
$ echo ${foo/%JPG/jpg}
JPG.jpg
3. 任意位置模式匹配
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}
4. 大小写
# 转为大写
${varname^^}
# 转为小写
${varname,,}
7. 算术运算
((...))
# 引用 算术运算结果:
$((...))
- 进制
- 赋值运算
- 算数运算
- 位运算
- 逻辑运算
8. 操作历史
- ctrl + r : 搜索命令历史
- !! 上一条指令
# 报错,没有执行权限
$ yum update
$ sudo !!
sudo yum update
- !$ 和 !*
!$
代表上一个命令的最后一个参数,它的另一种写法是$_
。
!*
代表上一个命令的所有参数,即除了命令以外的所有部分。
9. 行操作
- alt + f : 移动到当前单词的词尾.
- alt +b : 移动到当前单词的词首.
10. 目录堆栈
- **dirs **: 查看目录堆栈
- **pushd dirname **: 压入目录栈, 并cd到该目录
- popd : 弹出 目录栈, 并cd 到 上一个目录.
11. 脚本入门
1. Shebang 行
#!/bin/bash
2. 执行权限和路径
chmod 755 script.sh
5. 脚本参数
$0
:脚本文件名,即script.sh
。$1
~$9
:对应脚本的第一个参数到第九个参数。$#
:参数的总数。$@
:全部的参数,参数之间使用空格分隔。$*
:全部的参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
用户可以输入任意数量的参数,利用for
循环,可以读取每一个参数。
#!/bin/bash
for i in "$@"; do
echo $i
done
6. shift命令
shift
命令移除一个参数, 使得后面的参数向前一位
#!/bin/bash
echo "一共输入了 $# 个参数"
while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done
7. getopts
getopts
取出 脚本所有的带有前置连词线(-
)的参数。
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))" # 移除带前置线的参数,保证脚本可以处理主参数
8. 配置项参数终止符
--
: 将其后的参数作为实体参数解释, 而不是配置项。
# 情况1: 文件名中是--开头
cat -- --file
# 情况2: myPath中,可能- 或 -- 开头
myPath="-l"
$ ls -- $myPath
ls: 无法访问'-l': 没有那个文件或目录
# 情况3: 要搜索的内容是 - 或 -- 开头
grep -- "--hello" example.txt
9. exit命令
exit 0 : 成功退出.
10. 命令执行结果
$?
是上一条命令的执行结果( 0 表示成功)
11. source 命令
.
(省略写法): 在当前bash 执行一个脚本.
12. alias 别名
alias NAME=DEFINITION
12. read 命令
1. read 用法
read
命令 读入数据.
read [-options] [variable...]
# read可以接受用户输入的多个值。
read FN LN
echo "Hi! $LN, $FN !"
# 如果read命令之后没有定义变量名,那么环境变量REPLY会包含所有的输入。
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
read 读取 文件内容
#!/bin/bash
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
2. 参数
- -t 设置超时秒数
read -t 3 response
- -p 指定用户输入提示.
read -p " please enter xxx >"
- -a 将输入赋值给数组.
read -a people
,echo ${people[2]}
- -n 只读取 n 个 字符.
read -n 3 letter
- -e 允许用户使用readline 快捷键.
不常用:
- -d 定义输入结束符(默认是换行符)
- -r raw模式,不把反斜杠转义
- -u fd : 使用文件描述符 fd 作为输入.
3. IFS 变量
read
命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS
(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
#!/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
上面例子中,IFS
设为冒号,然后用来分解/etc/passwd
文件的一行。IFS
的赋值命令和read
命令写在一行,这样的话,IFS
的改变仅对后面的命令生效.
<<<
是 Here 字符串,用于将变量值转为标准输入,因为read
命令只能解析标准输入。
如果IFS
设为空字符串,就等同于将整行读入一个变量。
#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line
do
echo "$line"
done < "$input"
13. 条件判断
1. if 结构
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
2. test命令
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]] # 支持正则判断
3. 判断表达式
3.1 文件判断
#!/bin/bash
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi
[ -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
。
3.2 字符串判断
#!/bin/bash
ANSWER=maybe
if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
[ 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
之前,则判断为真。
3.3 整数判断
#!/bin/bash
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
[ 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
。
3.4 正则判断
[[ string1 =~ regex ]]
regex
是一个正则表示式,=~
是正则比较运算符。
#!/bin/bash
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "INT is an integer."
exit 0
else
echo "INT is not an integer." >&2
exit 1
fi
3.5 test的逻辑运算
AND
运算:符号&&
,也可使用参数-a
。OR
运算:符号||
,也可使用参数-o
。NOT
运算:符号!
。
if !([ $INT -ge $MIN_VAL ] && [ $INT -le $MAX_VAL ]); then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
圆括号 ( )
在这里可以用来创建一个子shell,从而限定否定的范围。
3.6 算术运算
((...))
进行算术运算的判断。只要是算术表达式,都能用于((...))
语法.
if (( ((INT % 2)) == 0)); then
echo "INT is even."
else
echo "INT is odd."
fi
如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。
4. 普通命令的逻辑运算
# 对于&&操作符,先执行command1,执行成功后,才会执行command2。
$ command1 && command2
# 对于||操作符,先执行command1,执行失败后,才会执行command2。
$ command1 || command2
示例:
mkdir temp && cd temp
[ -d temp ] || mkdir temp
if grep $word1 $filename && grep $word2 $filename
then
echo "$word1 and $word2 are both in $filename."
fi
[[ -d "$dir_name" ]] && cd "$dir_name" && rm *
5. case 结构
case expression in
pattern ) # pattern 可以使用 模式扩展
commands ;;
pattern )
commands ;;
...
esac
示例:
#!/bin/bash
OS=$(uname -s)
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之前,case
结构只能匹配一个条件,然后就会退出case
结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&
终止每个条件块。
read -n 1 -p "Type a character > "
echo
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac
14. 循环
1. while 循环
while condition; do
commands
done
示例:
#!/bin/bash
number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
2. for…in 循环
for...in
循环用于遍历列表的每一项。
for variable in list; do
commands
done
# in list 省略时,默认是脚本所有参数$@.
示例:
#!/bin/bash
count=0
for i in $(cat ~/.bash_profile); do
count=$((count + 1))
echo "Word $count ($i) contains $(echo -n $i | wc -c) characters"
done
上面例子中,cat ~/.bash_profile
命令会输出~/.bash_profile
文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。
3. for 循环
for (( expression1; expression2; expression3 )); do
commands
done
4. break, continue
5. select 结构
select name [in list]
do
commands
done
Bash 会对select
依次进行下面的处理。
select
生成一个菜单,内容是列表list
的每一项,并且每一项前面还有一个数字编号。- Bash 提示用户选择一项,输入它的编号。
- 用户输入以后,Bash 会将该项的内容存在变量
name
,该项的编号存入环境变量REPLY
。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。 - 执行命令体
commands
。 - 执行结束后,回到第一步,重复这个过程。
示例;
#!/bin/bash
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
15. 函数
1. 函数定义
函数与脚本的区别是; 1. 函数在当前Shell执行,2. 脚本 在 子Bash 执行.
[function] fn() { # fn 是函数名
# codes
}
函数调用方法和脚本一致.
2. 参数变量
函数参数 和 脚本参数 一致.
3. return 命令
返回值必须是数字.通过$? 拿出.
4. 全局变量和局部变量
bash内, 不论是否在函数内声明的变量, 默认是全局变量.
# 声明局部变量需要使用 local 关键字
fn () {
local var_name
}
16. 数组
1. 创建数组
ARRAY[INDEX]=value
ARRAY=(
value1
value2
value3
)
array=([2]=c [0]=a [1]=b)
mp3s=( *.mp3 )
declare -a ARRAYNAME
# read -a命令则是将用户的命令行输入,存入一个数组。
read -a dice
2. 读取数组
echo ${array[i]}
# 读取所有成员
echo ${foo[@]}
# 不加双引号, @ 和 * 一样, 加双引号, @ 分开处理每个成员, * 将所有成员当成一个字符串.
for i in "${names[@]}"; do # 双引号保护字符串、变量和命令,
echo $i
done
# 拷贝一个数组的最好方法:
hobbies=( "${activities[@]}" )
# 赋值,引用时: 默认位置是array[0]
declare -a foo
foo=A
echo ${foo[0]}
A
3. 数组长度
${#array[*]}
${#array[@]}
# 返回字符串长度:
echo ${#a[100]}
4. 返回数组有成员的序号
${!array[@]}
${!array[*]}
5. 提取数组成员
${array[@]:position:length}
6. 追加数组成员
foo+=(d e f)
7. 删除数组
unset foo[2]
# 数组成员变成空值, 但是成员依然存在
foo[1]=''
# 清空数组
unset ArrayName
8. 关联数组(键值)
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
echo ${colors["blue"]}
17. set命令, shopt命令
1. set
1.1 常用命令
修改子Shell的运行参数
# 如果遇到不存在的变量,则报错(默认是忽略)。
set -u
# 执行命令前,先输出执行的哪条命令
set -x
# 关闭命令输出:
set +x
# 发生错误,则终止执行
set -e
# 发生错误继续执行
set +e
# 推荐写法
command || true
command1 && command2
# 当使用 | 管道 组合命令时, 只要最后一个命令成功,则认为该命令成功. set -e 失效.
# 当前管道命令(视为一个命令)还会执行完, 但是后续命令不执行.
set -o pipefail
# 纠正错误不会被trap捕获
set -E
示例:
#!/bin/bash
set -Eeuo pipefail
trap "echo ERR trap fired!" ERR
myfunc()
{
# 'foo' 是一个不存在的命令
foo
}
myfunc
$ bash test.sh
test.sh:行9: foo:未找到命令
ERR trap fired!
1.2 其他命令
set
命令还有一些其他参数。
set -n
:等同于set -o noexec
,不运行命令,只检查语法是否正确。set -f
:等同于set -o noglob
,表示不对通配符进行文件名扩展。set -v
:等同于set -o verbose
,表示打印 Shell 接收到的每一行输入。set -o noclobber
:防止使用重定向运算符>
覆盖已经存在的文件。
上面的-f
和-v
参数,可以分别使用set +f
、set +v
关闭。
1.3 set 命令总结
# 写法一
set -Eeuxo pipefail
# 写法二
set -Eeux
set -o pipefail
2. 错误处理
如果遇到运行失败的命令(返回值非0
),Bash 默认会继续执行后面的命令。
# 建议写法:
command || exit 1
command1 && command2
3. shopt 命令
shopt
命令用来调整 Shell 的参数,跟set
命令的作用很类似。set
是从 Ksh 继承的,属于 POSIX 规范的一部分,而shopt
是 Bash 特有的。
# 查看参数状态
shopt
shopt globstar
# 打开某个参数
shopt -s optionNameHere
# 关闭某个参数
shopt -u optionNameHere
# 查询某个参数是否打开
shopt -q globstar
# 通过命令的执行状态($?)表示查询结果。如果状态为0,表示该参数打开;如果为1,表示该参数关闭。
示例:
if (shopt -q globstar); then
...
fi
18. 脚本除错
1. 常见错误
建议写法:
# 只打印要删除的内容,不删除
[[ -d $dir_name ]] && cd $dir_name && echo rm *
2. bash -x 参数
输出的命令之前的+
号,是由系统变量PS4
决定,可以修改这个变量。
$ export PS4='$LINENO + '
$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.
3. 环境变量
3.1 LINENO
变量LINENO
返回它在脚本里面的行号。
3.2 FUNCNAME
变量FUNCNAME
返回一个数组,内容是当前的函数调用堆栈。
3.3 BASH_SOURCE
变量BASH_SOURCE
返回一个数组,内容是当前的脚本调用堆栈。
3.4 BASH_LINENO
变量BASH_LINENO
返回一个数组,内容是每一轮调用对应的行号。
19. mktemp命令, trap命令
1. 临时文件的安全问题
对于临时文件,使用mktemp
命令是最安全的做法。
生成临时文件应该遵循下面的规则。
- 创建前检查文件是否已经存在。
- 确保临时文件已成功创建。
- 临时文件必须有权限的限制。
- 临时文件要使用不可预测的文件名。
- 脚本退出时,要删除临时文件(使用
trap
命令)。
2. mktemp 命令
2.1 mktemp命令用法
mktemp
命令就是为安全创建临时文件而设计的。
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
2.2 命令参数
# -d 参数可以创建一个临时目录。
$ mktemp -d
/tmp/tmp.Wcau5UjmN6
# -p 参数可以指定临时文件所在的目录。默认是使用$TMPDIR环境变量指定的目录(/tmp).
$ mktemp -p /home/ruanyf/
/home/ruanyf/tmp.FOKEtvs2H3
# -t参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X字符.
$ mktemp -t mytemp.XXXXXXX
/tmp/mytemp.yZ1HgZV
2. trap命令
trap
命令用来在 Bash 脚本中响应系统信号。
trap [动作] [信号1] [信号2] ...
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
echo 'find'
fi
如果trap
需要触发多条命令,可以封装一个 Bash 函数。
function egress {
command1
command2
command3
}
trap egress EXIT