Tutorial-bash

本文详细介绍了Bash脚本的基础知识,包括常用的命令、变量操作、条件判断和循环结构。讲解了如`readonly`、`let`、`expr`等命令,以及`if`、`case`、`while`、`for`等循环语句。此外,还探讨了Bash中的正则表达式、函数和数组的使用,以及如何处理文本文件。文章旨在帮助读者掌握Bash脚本编写的基本技巧。
摘要由CSDN通过智能技术生成
#############
#  参考资料  #
#############
# 《阮一峰 Bash 脚本教程》
# https://www.bookstack.cn/read/bash-tutorial/README.md


#############
# 0.常用操作 #
#############
#0. 常用命令
readonly
let
expr
history
pushd
popd
dirs
shif
getopts
mktemp  #为安全创建临时文件而设计,它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险
trap  #用来在 Bash 脚本中响应系统信号
uname -rm  #返回当前机器的信息,-r:内核的版本 -m:CPU 架构
service --status-all  #查看当前正在运行的服务,+表示正在运行,-表示已经停止,?表示service命令不了解相关信息

#1. 快捷键
Ctrl + L  #清除屏幕并将当前行移到页面顶部。
Ctrl + C  #中止当前正在执行的命令。
Shift + PageUp  #向上滚动。
Shift + PageDown  #向下滚动。
Ctrl + U  #从光标位置删除到行首。
Ctrl + K  #从光标位置删除到行尾。
Ctrl + D  #关闭 Shell 会话。
↑,↓  #浏览已执行命令的历史记录。

#2. 脚本除错
#2.1 编写 Shell 脚本的时候,一定要考虑到命令失败的情况,否则很容易出错。
#先判断目录$dir_name是否存在,然后才执行其他操作
[[ -d $dir_name ]] && cd $dir_name && rm *
[[ -d $dir_name ]] && cd $dir_name && echo rm *  #echo rm *不会删除文件,只会打印出来要删除的文件
#2.2 bash的-x参数可以在执行每一行命令之前,打印该命令
bash -x script.sh
#! /bin/bash -x
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
  echo "Number is equal to 1."
else
  echo "Number is not equal to 1."
fi
export PS4='$LINENO + '
#2.3 环境变量
$LINENO  #变量LINENO返回它在脚本里面的行号
$FUNCNAME  #变量FUNCNAME返回一个数组,内容是当前的函数调用堆栈
$BASH_SOURCE  #变量BASH_SOURCE返回一个数组,内容是当前的脚本调用堆栈
$BASH_LINENO  #变量BASH_LINENO返回一个数组,内容是每一轮调用对应的行号

#3. set命令
#set命令用来修改子 Shell 环境的运行参数,即定制环境
set  #显示所有的环境变量和 Shell 函数
set -u == set -o nounset #一般执行脚本时,如果遇到不存在的变量,Bash 默认忽略它。脚本在头部加上set -u,遇到不存在的变量bash就会报错,并停止执行
set -x == set -o xtrace #用来在运行结果之前,先输出执行的那一行命令。命令会先打印出来,行首以+表示。这对于调试复杂的脚本是很有用的。
set +x  #关闭命令输出
#只对特定的代码段打开命令输出
#!/bin/bash
number=1
set -x
if [ $number = "1" ]; then
  echo "Number equals 1"
else
  echo "Number does not equal 1"
fi
set +x
#Bash 的错误处理
#如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。
command || exit 1  #只要command有非零返回值,脚本就会停止执行
#如果停止执行之前需要完成多个操作,就要采用下面三种写法
# 写法一
command || { echo "command failed"; exit 1; }
# 写法二
if ! command; then echo "command failed"; exit 1; fi
# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
set -e == set -o errexit #据返回值来判断,一个命令是否运行失败。脚本只要发生错误,就终止执行。
#可以暂时关闭set -e,当命令执行结束后,再重新打开set -e。
set +e
command1
command2
set -e
#命令即使执行失败,脚本也不会终止执行
command || true  
#只要一个子命令失败,整个管道命令就失败,脚本就会终止执行
set -o pipefail
set -n  #等同于set -o noexec,不运行命令,只检查语法是否正确。
set -f  #等同于set -o noglob,表示不对通配符进行文件名扩展。
set -v  #等同于set -o verbose,表示打印 Shell 接收到的每一行输入。
#终极命令,建议放在所有 Bash 脚本的头部
set -euxo pipefail
bash -euxo pipefail script.sh
shopt -suq  #用来调整 Shell 的参数 -s:用来打开某个参数 -u:用来关闭某个参数 -q:作用也是查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态($?)表示查询结果。如果状态为0,表示该参数打开;如果为1,表示该参数关闭。
#主要用于脚本,供if条件结构使用
if shopt -q globstar; then
  ...
if

#4. bash命令
#4.1 登录 Session
#登录 Session 是用户登录系统以后,系统为用户开启的原始 Session,通常需要用户输入用户名和密码进行登录。
#登录 Session 一般进行整个系统环境的初始化,启动的初始化脚本依次如下。
/etc/profile  #所有用户的全局配置脚本。
/etc/profile.d  #目录里面所有.sh文件
~/.bash_profile  #用户的个人配置脚本。如果该脚本存在,则执行完就不再往下执行。
~/.bash_login  #如果~/.bash_profile没找到,则尝试执行这个脚本(C shell 的初始化脚本)。如果该脚本存在,则执行完就不再往下执行。
~/.profile  #如果~/.bash_profile和~/.bash_login都没找到,则尝试读取这个脚本(Bourne shell 和 Korn shell 的初始化脚本)。
bash --login  #会强制执行登录 Session 会执行的脚本
bash --noprofile  #会跳过上面这些 Profile 脚本
#4.2 非登录 Session
#非登录 Session 是用户进入系统以后,手动新建的 Session,这时不会进行环境初始化。
#非登录 Session 的初始化脚本依次如下。
#由于每次执行 Bash 脚本,都会新建一个非登录 Session,所以~/.bashrc也是每次执行脚本都会执行的
/etc/bash.bashrc  #对全体用户有效。
~/.bashrc  #仅对当前用户有效。
bash --norc  #可以禁止在非登录 Session 执行~/.bashrc脚本
bash --rcfile testr  #指定另一个脚本代替.bashrc
~/.bash_logout  #脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。
#4.3 启动选项
bash -n scriptname  #不运行脚本,只检查是否有语法错误
bash -v scriptname  #输出每一行语句运行结果前,会先输出该行语句
bash -x scriptname  #每一个命令处理完以后,先输出该命令,再进行下一个命令的处
#4.4 键盘绑定
/etc/inputrc  #全局的键盘绑定文件
.inputrc  #在主目录创建自己的键盘绑定文件,要在其中加入include /etc/inputrc,保证全局绑定不会被遗漏

#5. 命令提示符
#5.1 环境变量 PS1
echo $PS1
export PS1="\A \h \$ "
\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 会话中的命令数。
\$  #普通用户显示为$字符,根用户显示为#字符。
\[  #非打印字符序列的开始标志。
\]  #非打印字符序列的结束标志。
#5.2 颜色
#设置前景颜色
\033[0;30m  #黑色
\033[1;30m  #深灰色
\033[0;31m  #红色
\033[1;31m  #浅红色
\033[0;32m  #绿色
\033[1;32m  #浅绿色
\033[0;33m  #棕色
\033[1;33m  #黄色
\033[0;34m  #蓝色
\033[1;34m  #浅蓝色
\033[0;35m  #粉红
\033[1;35m  #浅粉色
\033[0;36m  #青色
\033[1;36m  #浅青色
\033[0;37m  #浅灰色
\033[1;37m  #白色
export PS1='\[\033[0;31m\]<\u@\h \W>\$'  #将提示符设为红色
export PS1='\[\033[0;31m\]<\u@\h \W>\$\[\033[00m\]'  #可以在结尾添加另一个特殊代码\[\033[00m\],表示将其后的文本恢复到默认颜色
export PS1='\[\033[0;41m\]<\u@\h \W>\$\[\033[0m\] '  #带有红色背景的提示符
#设置背景颜色
\033[0;40m  #蓝色
\033[1;44m  #黑色
\033[0;41m  #红色
\033[1;45m  #粉红
\033[0;42m  #绿色
\033[1;46m  #青色
\033[0;43m  #棕色
\033[1;47m  #浅灰色
#5.3 环境变量 PS2,PS3,PS4
PS2  #环境变量PS2是命令行折行输入时系统的提示符,默认为>
PS3  #环境变量PS3是使用select命令时,系统输入菜单的提示符
PS4  #环境变量PS4默认为+

#6. 命名管道
process1 > named_pipe + process2 < named_pipe = process1 | process2
mkfifo pipe1  #使用 mkfifo 命令能够创建命令管道
ls -l pipe1
ls -l > pipe1  #按下 Enter 按键之后,命令将会挂起,这是因为在管道的另一端没有任何接受数据
cat < pipe1  #一旦我们绑定一个进程到管道的另一端,该进程开始从管道中读取输入的时候,这种情况会消失

#7. 重定向
#重定向指的是将命令行输出写入指定位置。
cmd1 | cmd2  #Pipe; take standard output of cmd1 as standard input to cmd2.
> file  #Direct standard output to file.
< file  #Take standard input from file.
>> file  #Direct standard output to file; append to file if it already exists.
>| file  #Force standard output to file even if noclobber is set.
n>| file  #Force output to file from file descriptor n even if noclobber is set.
<> file  #Use file as both standard input and standard output.
n<> file  #Use file as both input and output for file descriptor n.
#<< label  #Here-document; see text.
n > file  #Direct file descriptor n to file.
n < file  #Take file descriptor n from file.
n >> file  #Direct file descriptor n to file; append to file if it already exists.
n>&  #Duplicate standard output to file descriptor n.
n<&  #Duplicate standard input from file descriptor n.
n>&m  #File descriptor n is made to be a copy of the output file descriptor.
n<&m  #File descriptor n is made to be a copy of the input file descriptor.
&>file  #Directs standard output and standard error to file.
<&-  #Close the standard input.
>&-  #Close the standard output.
n>&-  #Close the output from file descriptor n.
n<&-  #Close the input from file descriptor n.
n>&word  #If n is not specified, the standard output (file descriptor 1) is used. If the digits in word do not specify a file descriptor open for output, a redirection error occurs. As a special case, if n is omitted, and word does not expand to one or more digits, the standard output and standard error are redirected as described previously.
n<&word  #If word expands to one or more digits, the file descriptor denoted by n is made to be a copy of that file descriptor. If the digits in word do not specify a file descriptor open for input, a redirection error occurs. If word evaluates to -, file descriptor n is closed. If n is not specified, the standard input (file descriptor 0) is used.
n>&digit-  #Moves the file descriptor digit to file descriptor n, or the standard output (file descriptor 1) if n is not specified.
n<&digit-  #Moves the file descriptor digit to file descriptor n, or the standard input (file descriptor 0) if n is not specified. digit is closed after being duplicated to n.
ls -l /usr/bin > ls-output.txt  #>用来将标准输出重定向到指定文件。如果重定向后的指定文件已经存在,就会被覆盖,不会有任何提示。
> ls-output.txt  #如果命令没有任何输出,那么重定向之后,得到的是一个长度为0的文件。因此,>具有创建新文件或改写现存文件、将其改为长度0的作用。
ls -l /usr/bin >> ls-output.txt  #>>用来将标准输出重定向追加到指定文件
ls -l /bin/usr 2> ls-error.txt  #2>用来将标准错误重定向到指定文件
ls -l /bin/usr > ls-output.txt 2>&1 or ls -l /bin/usr &> ls-output.txt  #标准输出和标准错误,可以重定向到同一个文件
ls -l /bin/usr &>> ls-output.txt  #追加到同一个文件
ls -l /bin/usr 2> /dev/null  #如果不希望输出错误信息,可以将它重定向到一个特殊文件/dev/null
ls -l /usr/bin | less  #|用于将一个命令的标准输出,重定向到另一个命令的标准输入
#标准错误重定向
invalid_input () {
    echo "Invalid input '$REPLY'" >&2
    exit 1
}
read -p "Enter a single item > "
[[ -z $REPLY ]] && invalid_input
ls /usr/bin | tee ls.txt | grep zip  #tee命令用于同时将标准输出重定向到文件,以及另一个命令的标准输入
echo $(ls) or ls -l $(which cp)  #命令替换(command substitution)指的是将一个命令的输出,替换进入另一个命令。$(command)表示命令替换,另一种写法是使用反引号。
#basename命令清除 一个路径名的开头部分,只留下一个文件的基本名称
#!/bin/bash
# file_info: simple file information program
PROGNAME=$(basename $0)
if [[ -e $1 ]]; then
    echo -e "\nFile Type:"
    file $1
    echo -e "\nFile Status:"
    stat $1
else
    echo "$PROGNAME: usage: $PROGNAME file" >&2
    exit 1
fi

#8. 命令的连续执行
# 第一个命令执行完,执行第二个命令
command1; command2
# 只有第一个命令成功执行完(退出码0),才会执行第二个命令
command1 && command2
# 只有第一个命令执行失败(退出码非0),才会执行第二个命令
command1 || command2
#组命令
{ command1; command2; [command3; ...] }
#子 shell
(command1; command2; [command3;...])
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr

#9. 反引号、$()和${}
#9.1 反引号与$()用于命令替换
#反引号和$()的作用相同,用于命令替换(command substitution),即完成引用的命令的执行,将其结果替换出来
echo `date '--date=1 hour ago' +%Y-%m-%d-%H`
#或者
echo $(date '--date=1 hour ago' +%Y-%m-%d-%H)
#在多层次的复合替换中,里层的反引号需要转义处理(\`) ,而$()则比较直观
command1 `command2 \`command3\``
#或者换成$()
command1 $(command2 $(command3))
#反引号中对于反斜杠有特殊的处理,使用反协议对Shell特殊字符进行转义时需要两个反斜杠,而$()中只需要使用一个反斜杠
var1=`echo \$HOME`    #使用一个反斜杠无法完成对$符的转义
var2=`echo \\$HOME`
var3=$(echo \$HOME)
#反引号是一个老的用法,$()是新的用法,无论是在学习还是实际工作中,建议使用$()

#9.2 ${}用于变量替换
#1. 直接变量替换
echo ${A}B  #一般情况下,$var与${var}并没有区别,但是用${ }会比较精确的界定变量名称的范围
#2. 特殊变量替换
#${} 除了直接替换变量内容,还有一些用于字符串变量的特殊功能
file="/dir1/dir2/dir3/my.file.txt"
#2.1 ${:}与${::}用于字符串提取
${var:n}  #若n为正数,n从0开始,表示在变量var中提取第n个字符到末尾的所有字符。若n为负数,提取字符串最后面n的绝对值个字符,使用时在冒号后面加空格或一个算术表达式或整个num加上括号,如${var: -2}、${var:1−3}或 ${var:(-2)}均表示提取最后两个字符。
${file:1}  #提取第1个字符及其后面的所有字符:dir1//dir2/dir3/my.file.txt
${file: -3}  #提取最后3个字符,注意冒号后面添加一个空格:txt
${file:1-4}  #提取最后3个字符,冒号后面不需要添加空格:txt
${file:(-3)}  #提取最后3个字符,冒号后面不需要添加空格:txt
${var:n1:n2}  #${var:n1:n2}用于提取从下标n1开始后面n2个字符,其中下标n1与n2从0开始
${file:0:5}:提取最左边的5个字符:/dir1
${file:5:5}:提取从第5个字符开始右边的连续5个字符:/dir2
#2.2 ${/}与${//}用于字符串模式匹配替换
#${var/pattern/pattern}表示将var字符串的第一个匹配的pattern替换为另一个pattern。不改变原变量。
${file/dir/path}  #将第一个dir替换为path:/path1/dir2/dir3/my.file.txt
${file//dir/path}  #将全部dir替换为path:/path1/path2/path3/my.file.txt
#2.3 ${#}、${##}、${%}与${%%}用于字符串模式匹配截断
#可以过滤掉符合指定规则的字符串,不改变原变量
##是去掉左边(在鉴盘上#在$之左边)
#%是去掉右边(在鉴盘上%在$之右边)
#一个符号是最小匹配,两个符号是最大匹配。
${file#*/}  #拿掉第一个 / 及其左边的字符串:dir1/dir2/dir3/my.file.txt
${file##*/}  #拿掉最后一个 / 及其左边的字符串:my.file.txt
${file#*.}  #拿掉第一个 . 及其左边的字符串:file.txt
${file##*.}  #拿掉最后一个 . 及其左边的字符串:txt
${file%/*}  #拿掉最后一个 / 及其右边的字符串:/dir1/dir2/dir3
${file%%/*}  #拿掉第一个 / 及其右边的字符串:(空值)
${file%.*}  #拿掉最后一个 . 及其右边的字符串:/dir1/dir2/dir3/my.file
${file%%.*}  #拿掉第一个 . 及其右边的字符串:/dir1/dir2/dir3/my

#10. 特殊字符
#1、;分号
ifdown eth0;ifup eth0  #连续运行命令
#2、| 管道
#正则表达式中表示或者
echo "ooooee" |egrep '(oo|ee)'{2}  #表示匹配 oooo 或者 eeee 的字符
#前面命令的标准输出作为后面命令的标准输入
ifconfig|grep eth0  #表示ifconfig查出来的信息然后过滤出eth0的这一行
#3、&
#将命令放到后台执行
mysqld_safe --user=mysql &  #将MySQL放到后台启动
#表示标准输出和标准错误输出
ifconfig &>/dev/null  #将ifconfig执行得到的结果输出到/dev/null里面
#4、&&
#前面命令返回值为0才执行后面的命令
ls && echo "ok"
#5、||
#前面命令返回值为非0才执行后面的命令
lls || echo "ok"
#6、# 井号
# # 表示注释
# $# 表示位置参数的个数
echo $#
# ${#变量名} 表示变量的长度
echo ${#a}
# ${#变量名[@]} 表示数组的个数
echo ${#a[@]}
#7、!惊叹号
#将命令或者条件表达式的返回值取反
if ! [ 1<2 ]; then echo 'ok'; else echo 'no'; fi
#执行历史命令
#vi或者ftp中执行外部shell命令
#间接应用变量
#8、$ 美元符号
#取变量的值
#正则表达式表示行尾
#9、> 大于号
#输出重定向
echo '123' >test.txt  #表示将123 输入到文件test.txt中 条件测试中的大于号
#11、< 小于号
#输入重定向
#条件测试中的小于号
#= 等号
#变量赋值 
a=10  #设置变量a=10
#条件测试中的等号 
[ a=b ]  #判断变量a是否等于b
#数值比较 == 
(( a==20 ))  #判断变量a是否等于20
#12、+ 加号
#算术运算中的加号
1+3
#正则表达式中1个或多个前面的字符 
ab+c  #表示匹配ab和c之间有1个或者多个 字符
#13、>>
#输出重定向追加
echo "123" >> test.txt  #将123追加到文件test.txt中
#14、<<
#here document
# passwd <<end
> 123
> 123
> end
#15、- 减号
#算术运算中的减号 
10-2
#命令的选项 
ls -l
#上一次工作目录 
cd -
#通配符和正则表达式中表示范围 
[a-z]
#表示输出流或输入流
#将前面的输出 ,通过管道交给后面的命令,前面的压缩,后面的解压
#16、'' 单引号
#解决变量赋值空格的问题
#阻止shell替换
#17、"" 双引号
#解决变量赋值空格的问题
#阻止shell部分字符替换,对$、!等无效
#18、`` 反引号 相当于 $()
#命令行替换
#19、% 百分号
echo $((100%10))  #就是100除以10的余数为0
#vi中替换操作中表示所有行(末行模式下,替换所有前面加 %)  
在末行模式下输入 :% s/D/d  #表示将文本中的所有的D替换为d
#20、() 单圆括号
#子shell中执行命令,会继承父shell的变量
#括起数组元素
#21、(()) 双圆括号
#算术运算 
echo $((10/2))  #结果就是5
#整数比较测试  
(( 10>2 ))  #判断10是否大于2
#22、[] 单方括号
#通配符和正则中表示匹配括号中的任意一个字符
[abc]  #表示匹配abc中的任意一个字符
#条件测试表达式
[ -f /etc/passwd ]  #测试是不是文件
#数组中下标括号
echo ${a[0]}  #表示取数组中下标为0的值
#23、[[]] 双方括号
#字符串比较测试
[[a=b]]  #用来字符串的比较
#24、. 英文句点号
#正则中表示任意1个字符
a...b  #表示 匹配 a和b之间夹三个字符的字符串
#当前shell执行脚本命令
./test.sh  #执行当前路径下的shell脚本test.sh
#表示当前目录
cd ./bgk  #进入当前目录下的bgk目录下
#25、{} 大括号
#通配符扩展 
abc{1,2,3}
#正则表达式中表示范围
a{3}  #匹配3个 a
#for i in {1...10} 循环指定范围
#匿名函数
{ cmd1;cmd2;cmd3;} &> /dev/null
#{ } 里面的命令,是在当前shell执行  注意: { } 第一条命令前面要有空格,后面的命令要有分号
#括起变量名 ${abc}a
#26、/ 正斜杠
#算术运算中的除法
echo $((10/2))  #结果就是5
#根目录或路径分割符
cd /usr/local/  #表示路径
#27、^
#在通配符中表示取反
[^abc]  #表示匹配除了abc外的任意一个字符
#在正则表达式中表示以什么开头


#############
# 1.基本语法 #
#############
#1. 基本命令
echo $SHELL
cat /etc/shells
pwd
bash --version
echo -en <txt>  #-n:可以取消末尾的回车符,使得下一个提示符紧跟在输出内容的后面 -e:会解释引号(双引号和单引号)里面的特殊字符(比如换行符\n)
type -at <cmd>  #用来判断命令的来源 -a:查看一个命令的所有定义 -t:可以返回一个命令的类型
cd -  #可以返回前一次的目录
source .bashrc  #用于执行一个脚本,通常用于重新加载一个配置文件
source ./lib.sh  #在脚本内部加载外部库
alias  #可以显示所有别名
alias NAME=DEFINITION  #NAME是别名的名称,DEFINITION是别名对应的原始命令。注意,等号两侧不能有空格,否则会报错。
unalias lt  #解除别名
awk  #AWK 是一种处理文本文件的语言,是一个强大的文本分析工具。

#2. 命令输入格式 
#2.1 将长命令用反斜杠(\)分成多行短命令
echo foo bar
echo foo \
bar
#2.2 分号(;)是命令的结束符,使得一行可以放置多个命令
clear;ls
#2.3 命令的组合符&&和||,允许更好地控制多个命令之间的继发关系
Command1 && Command2  #如果Command1命令运行成功,则继续运行Command2命令
Command1 || Command2  #如果Command1命令运行失败,则继续运行Command2命令


##############
# 2.模式扩展 #
#############
#Bash 接收到命令以后,发现里面有通配符,会进行通配符扩展,然后再执行命令
#文件名扩展在没有可匹配的文件时,会原样输出
#1. 波浪线扩展
#波浪线~会自动扩展成当前用户的主目录
echo ~

#2. ? 字符扩展
#?字符代表文件路径里面的任意单个字符,不包括空字符
ls ?.txt
ls ??.txt

#3. * 字符扩展
#*字符代表文件路径里面的任意数量的字符,包括零个字符
#*只匹配当前目录,不会匹配子目录
#*不会匹配隐藏文件
ls *.txt
ls *

#4.1 方括号扩展
#[aeiou]可以匹配五个元音字母中的任意一个
ls [ab].txt
ls ?[!a]? == ls ?[^a]?
#4.2 [start-end] 扩展
#方括号扩展有一个简写形式[start-end],表示匹配一个连续的范围,[a-c]等同于[abc],[0-9]匹配[0123456789]
ls [a-c].txt
ls report[0-9].txt
#[!a-zA-Z]表示匹配非英文字母的字符
echo report[!1–3].txt

#5.1 大括号扩展
#大括号扩展{...}表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔
echo d{a,e,i,u,o}g
#5.2 {start..end} 扩展
#大括号扩展有一个简写形式{start..end},表示扩展成一个连续序列,比如{a..z}可以扩展成26个小写英文字母
echo {a..c}
echo {c..a}
mkdir {2007..2009}-{01..12}
rm -rf mkdir {2007..2009}-{01..12}
echo {001..5}
#(start..end..step),用来指定扩展的步长
echo {0..8..2}
echo {a..c}{1..3}

#6. 变量扩展
#Bash 将美元符号$开头的词元视为变量,将其扩展成变量值
echo $SHELL
echo ${SHELL}
#${!string*}或${!string@}返回所有匹配给定字符串string的变量名
echo ${!S*}

#7. 子命令扩展
#$(...)可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值
echo $(date) == echo `date`

#8. 算术扩展
#$((...))可以扩展成整数运算的结果

echo $((2 + 2))
#9. 字符类
#[[: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 [[:upper:]]*  #输出所有大写字母开头的文件名
echo [![:digit:]]*  #输出所有不以数字开头的文件名


###############
# 3.引号与转义 #
###############
#1. 转义
echo \$date #如果想要原样输出特殊字符,就必须在前面加上反斜杠,使其变成普通字符。这就叫做“转义”(escape)
echo \\  #反斜杠本身也是特殊字符,如果想要原样输出反斜杠,就需要对它自身转义,连续使用两个反斜线(\\)
#反斜杠除了用于转义,还可以表示一些不可打印的字符
\a:响铃
\b:退格
\n:换行
\r:回车
\t:制表符
echo -e "a\tb"  #如果想要在命令行使用这些不可打印的字符,可以把它们放在引号里面,然后使用echo命令的-e参数
#由于反斜杠可以对换行符转义,使得 Bash 认为换行符是一个普通字符,从而可以将一行命令写成多行。
#如果一条命令过长,就可以在行尾使用反斜杠,将其改写成多行。这是常见的多行命令的写法
mv \
/path/to/foo \
/path/to/bar
# 等同于
mv /path/to/foo /path/to/bar

#2. 单引号
#单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符,比如星号(*)、美元符号($)、反斜杠(\)等
#单引号使得 Bash 扩展、变量引用、算术运算和子命令,都失效了。如果不使用单引号,它们都会被 Bash 自动扩展。
$ echo '*'
*
$ echo '$USER'
$USER
$ echo '$((2+2))'
$((2+2))
$ echo '$(echo foo)'
$(echo foo)
  #在双引号之中使用单引号
$ echo "it's"  #在双引号之中使用单引号

#3. 双引号
#双引号比单引号宽松,可以保留大部分特殊字符的本来含义,但是三个字符除外:美元符号($)、反引号(` )和反斜杠(\)。也就是说,这三个字符在双引号之中,会被 Bash 自动扩展。
#美元符号和反引号在双引号中,都保持特殊含义。美元符号用来引用变量,反引号则是执行子命令
$ echo "$SHELL"
/bin/bash
$ echo "`date`"
Mon Jan 27 13:33:18 CST 2020
#保存原始命令的输出格式
# 单行输出
$ echo $(cal)
一月 2020 日 一 二 三 四 五 六 1 2 3 ... 31
# 原始格式输出
$ echo "$(cal)"
      一月 2020
日 一 二 三 四 五 六
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

##########
# 4.变量 #
##########
#1. 查看变量
#环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用
env == printenv  #可以显示所有环境变量
printenv PATH == echo $PATH  #查看单个环境变量的值
#自定义变量是用户在当前 Shell 里面自己定义的变量,必须先定义后使用,而且仅在当前 Shell 可用
set  #可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数

#2. 创建变量
#Bash 没有数据类型的概念,所有的变量值都是字符串
variable=value  #变量声明的语法,等号左边是变量名,右边是变量 注意,等号两边不能有空格
myvar="hello world"  #如果变量的值包含空格,则必须将值放在引号中
a=z                     # 变量 a 赋值为字符串 z
b="a string"            # 变量值包含空格,就必须放在引号里面
c="a string and $b"     # 变量值可以引用其他变量的值
d="\t\ta string\n"      # 变量值可以使用转义字符
e=$(ls -l foo.txt)      # 变量值可以是命令的执行结果
f=$((5 * 7))            # 变量值可以是数学运算的结果

#3. 读取变量
#读取变量的时候,直接在变量名前加上$就可以了
echo $foo
echo ${a}_file  #可以用于变量名与其他字符连用的情况
echo ${!myvar}  #如果变量的值本身也是变量,可以使用${!varname}的语法,读取最终的值

#4. 输出变量
#为了把变量传递给子 Shell,需要使用export命令 这样输出的变量,对于子 Shell 来说就是环境变量
export NAME=value  #用来向子 Shell 输出变量
#子 Shell 如果修改继承的变量,不会影响父 Shell

#5. 特殊变量
$?  #$?为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果是非零,上一个命令执行失败。
$$  #$$为当前 Shell 的进程 ID,这个特殊变量可以用来命名临时文件
$_  #$_为上一个命令的最后一个参数
$!  #$!为最近一个后台执行的异步命令的进程 ID
$0  #$0为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)
$-  #$-为当前 Shell 的启动参数
$@ $#  #$@和$#表示脚本的参数数量


###############
# 5.字符串操作 #
###############


##############
# 6.算数运算 #
#############


############
# 7.行操作 #
############
#操作历史
echo $HISTFILE
/home/me/.bash_history
history  #能显示操作历史,即.bash_history文件的内容
export HISTTIMEFORMAT='%F %T  '  #通过定制环境变量HISTTIMEFORMAT,可以显示每个操作的时间
history | grep /usr/bin  #返回.bash_history文件里面,那些包含/usr/bin的命令
history -c  #清除操作历史


#############
# 8.目录堆栈 #
#############


#############
# 9.脚本入门 #
#############
#1. Shebang 行
# 脚本(script)就是包含一系列命令的一个文本文件。Shell 读取这个文件,依次执行里面的所有命令,就好像这些命令直接输入到命令行一样
# 脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。
# “#!”后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh或/bin/bash
#!/bin/sh
#!/bin/bash
# 如果 Bash 解释器不放在目录/bin,脚本就无法执行了
#!/usr/bin/env bash  #/usr/bin/env bash的意思就是,返回bash可执行文件的位置,前提是bash的路径是在$PATH里面
./script.sh  #有 Shebang 行的时候,可以直接调用执行
bash ./script.sh  #如果没有 Shebang 行,就只能手动将脚本传给解释器来执行

#2. 执行权限和路径
#如果将脚本放在环境变量$PATH指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件
chmod +x script.sh  #给所有用户执行权限
chmod +rx script.sh == chmod 755 script.sh  #给所有用户读权限和执行权限
chmod u+rx script.sh  #只给脚本拥有者读权限和执行权限
export PATH=$PATH:~/bin  #建议在主目录新建一个~/bin子目录,专门存放可执行脚本,然后把~/bin加入$PATH
env -i /bin/sh  #新建一个不带任何环境变量的 Shell

#3. 脚本参数
script.sh word1 word2 word
$0  #脚本文件名,即script.sh。
$1~$9  #对应脚本的第一个参数到第九个参数。
$#  #参数的总数。
$@  #全部的参数,参数之间使用空格分隔。
$*  #全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
echo "全部参数:" $@
echo "命令行参数数量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
for i in "$@"; do
  echo $i
done

#4. read命令
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
#read可以接受用户输入的多个值
#如果read命令之后没有定义变量名,那么环境变量REPLY会包含所有的输入
#read命令除了读取键盘输入,可以用来读取文件


##############
# 10.条件判断 #
##############
#0. 补充
#当${var}为空或未设置时,语句被解释为 if [ == "0" ],出现语法错误。
#加上x后,当${var}为空或未设置时,解释为if [ “x" == "x" ] ,依然正确
#if [ “x${var}" == “x” ]  判断${var}是否为空,添加x是为了防止出现语法错误

#1. if结构
#if是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令
if commands; then
  commands
[elif commands; then
  commands...]
[else
  commands]
fi
#if关键字后面是主要的判断条件,elif用来添加在主条件不成立时的其他判断条件,else则是所有条件都不成立时要执行的部分
if test $USER = "foo"; then
  echo "Hello foo."
else
  echo "You are not foo."
fi
#if和then写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号
if true
then
  echo 'hello world'
fi
if false
then
  echo 'it is false' # 本行不会执行
fi
#除了多行的写法,if结构也可以写成单行
if true; then echo 'hello world'; fi
#if关键字后面也可以是一条命令,该条命令执行成功(返回值0),就意味着判断条件成立
if echo 'hi'; then echo 'hello world'; fi
#if后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回0,就会执行then的部分。
if false; true; then echo 'hello world'; fi
#elif部分可以有多个
#!/bin/bash
echo -n "输入一个1到3之间的数字(包含两端)> "
read character
if [ "$character" = "1" ]; then
    echo 1
elif [ "$character" = "2" ]; then
    echo 2
elif [ "$character" = "3" ]; then
    echo 3
else
    echo 输入不符合要求
fi

#2. test 命令
# 上面的expression是一个表达式。这个表达式为真,test命令执行成功(返回值为0);表达式为伪,test命令执行失败(返回值为1)。注意,第二种和第三种写法,[和]与内部的表达式之间必须有空格。
# 写法一
if test -e /tmp/foo.txt ; then
  echo "Found foo.txt"
fi
# 写法二
if [ -e /tmp/foo.txt ] ; then
  echo "Found foo.txt"
fi
# 写法三
if [[ -e /tmp/foo.txt ]] ; then
  echo "Found foo.txt"
fi

#3. 判断表达式
#3.1 文件判断
[ -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。
#上面代码中,$FILE要放在双引号之中。这样可以防止$FILE为空,因为这时[ -e ]会判断为真。而放在双引号之中,返回的就总是一个空字符串,[ -e "" ]会判断为伪。
#!/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
#3.2 字符串判断
[ 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之前,则判断为真。
#注意,test命令内部的>和<,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。
#!/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
#3.3 整数判断
[ 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。
#!/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
#3.4 正则判断
#[[ expression ]]这种判断形式,支持正则表达式
[[ 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  #符号!
#!/bin/bash
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  if [[ $INT -ge $MIN_VAL && $INT -le $MAX_VAL ]]; then
    echo "$INT is within $MIN_VAL to $MAX_VAL."
  else
    echo "$INT is out of range."
  fi
else
  echo "INT is not an integer." >&2
  exit 1
fi
#使用否定操作符!时,最好用圆括号确定转义的范围
#test命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释
if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
    echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
    echo "$INT is in range."
fi
#3.6 算术判断
#Bash 还提供了((...))作为算术条件,进行算术运算的判断
#如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心 ((1))表示判断成立,((0))表示判断不成立
if ((3 > 2)); then
  echo "true"
fi
$ if ((1)); then echo "It is true."; fi
It is true.
$ if ((0)); then echo "It is true."; else echo "it is false."; fi
It is false.
#!/bin/bash
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
  if ((INT == 0)); then
    echo "INT is zero."
  else
    if ((INT < 0)); then
      echo "INT is negative."
    else
      echo "INT is positive."
    fi
    if (( ((INT % 2)) == 0)); then
      echo "INT is even."
    else
      echo "INT is odd."
    fi
  fi
else
  echo "INT is not an integer." >&2
  exit 1
fi
#3.7 普通命令的逻辑运算
mkdir temp && cd temp  #创建一个名为temp的目录,执行成功后,才会执行第二个命令,进入这个目录
[ -d temp ] || mkdir temp  #会测试目录temp是否存在,如果不存在,就会执行第二个命令,创建这个目录。这种写法非常有助于在脚本中处理错误。
if [ condition ] && [ condition ]; then
  command
fi
#! /bin/bash
filename=$1
word1=$2
word2=$3
if grep $word1 $filename && grep $word2 $filename
then
  echo "$word1 and $word2 are both in $filename."
fi
#下面的示例演示如何将一个&&判断表达式,改写成对应的if结构。
[[ -d "$dir_name" ]] && cd "$dir_name" && rm *
# 等同于
if [[ ! -d "$dir_name" ]]; then
  echo "No such directory: '$dir_name'" >&2
  exit 1
fi
if ! cd "$dir_name"; then
  echo "Cannot cd to '$dir_name'" >&2
  exit 1
fi
if ! rm *; then
  echo "File deletion failed. Check results" >&2
  exit 1
fi

#4. case 结构
#case结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif的if结构等价,但是语义更好。
case expression in
  pattern )
    commands ;;
  pattern )
    commands ;;
  ...
esac
#!/bin/bash
echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
  1 ) echo 1
    ;;
  2 ) echo 2
    ;;
  3 ) echo 3
    ;;
  * ) echo 输入不符合要求
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
#case的匹配模式可以使用各种通配符
a)  #匹配a。
a|b) #匹配a或b。
[[:alpha:]]) #匹配单个字母。
???) #匹配3个字符的单词。
*.txt) #匹配.txt结尾。
*) #匹配任意输入,通过作为case结构的最后一个模式。
#!/bin/bash
echo -n "输入一个字母或数字 > "
read character
case $character in
  [[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
                              ;;
  [0-9] )                     echo "输入了数字 $character"
                              ;;
  * )                         echo "输入不符合要求"
esac
#Bash 4.0之前,case结构只能匹配一个条件,然后就会退出case结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&终止每个条件块。
#可以看到条件语句结尾添加了;;&以后,在匹配一个条件之后,并没有退出case结构,而是继续判断下一个条件。
#!/bin/bash
# test.sh
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


############
# 11.循环 #
###########
#1. while 循环
#while循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。
while condition; do
  commands
done
#!/bin/bash
number=0
while [ "$number" -lt 10 ]; do
  echo "Number = $number"
  number=$((number + 1))
done
#关键字do可以跟while不在同一行,这时两者之间不需要使用分号分隔。
while true
do
  echo 'Hi, while looping ...';
done
#while循环写成一行,也是可以的。
while true; do echo 'Hi, while looping ...'; done
#while的条件部分也可以是执行一个命令。
while echo 'ECHO'; do echo 'Hi, while looping ...'; done
#while的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果。
while true; false; do echo 'Hi, looping ...'; done

#2. until 循环
#一般来说,until用得比较少,完全可以统一都使用while。
#until循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
until condition; do
  commands
done
until condition
do
  commands
done
#until的部分一直为false,导致命令无限运行,必须按下 Ctrl + c 终止。
until false; do echo 'Hi, until looping ...'; done
#只要变量number小于10,就会不断加1,直到number大于等于10,就退出循环。
#!/bin/bash
number=0
until [ "$number" -ge 10 ]; do
  echo "Number = $number"
  number=$((number + 1))
done
#until的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试。
#只要cp $1 $2这个命令执行不成功,就5秒钟后再尝试一次,直到成功为止。
until cp $1 $2; do
  echo 'Attempt to copy failed. waiting...'
  sleep 5
done
#until循环都可以转为while循环,只要把条件设为否定即可。上面这个例子可以改写如下。
while ! cp $1 $2; do
  echo 'Attempt to copy failed. waiting...'
  sleep 5
done

#3. for…in 循环
#for...in循环用于遍历列表的每一项。
for variable in list
do
  commands
done
#关键词do可以跟for写在同一行,两者使用分号分隔。
for variable in list; do
  commands
done
#!/bin/bash
for i in word1 word2 word3; do
  echo $i
done
#列表可以由通配符产生。
for i in *.png; do
  ls -l $i
done
#列表也可以通过子命令产生。
#cat ~/.bash_profile命令会输出~/.bash_profile文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。
#!/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
#in list的部分可以省略,这时list默认等于脚本的所有参数$@。
for filename; do
  echo "$filename"
done
# 等同于
for filename in "$@" ; do
  echo "$filename"
done

#4. for 循环
#for循环还支持 C 语言的循环语法。
#expression1用来初始化循环条件,expression2用来决定循环结束的条件,expression3在每次循环迭代的末尾执行,用于更新值。
#注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$。
for (( expression1; expression2; expression3 )); do
  commands
done
# 等同于
(( expression1 ))
while (( expression2 )); do
  commands
  (( expression3 ))
done
for (( i=0; i<5; i=i+1 )); do
  echo $i
done
#for条件部分的三个语句,都可以省略。
#脚本会反复读取命令行输入,直到用户输入了一个点(.)位为止,才会跳出循环。
for ((;;))
do
  read var
  if [ "$var" = "." ]; then
    break
  fi
done

#5. break,continue
#break命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。
#!/bin/bash
for number in 1 2 3 4 5 6
do
  echo "number is $number"
  if [ "$number" = "3" ]; then
    break
  fi
done
#continue命令立即终止本轮循环,开始执行下一轮循环。
#!/bin/bash
while read -p "What file do you want to test?" filename
do
  if [ ! -e "$filename" ]; then
    echo "The file does not exist."
    continue
  fi
  echo "You entered a valid file.."
done

#6. select 结构
#select结构主要用来生成简单的菜单。它的语法与for...in循环基本一致。
select name
[in list]
do
  commands
done
#Bash 会对select依次进行下面的处理。
#1. select生成一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号。
#2. Bash 提示用户选择一项,输入它的编号。
#3. 用户输入以后,Bash 会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。
#4. 执行命令体commands。
#5. 执行结束后,回到第一步,重复这个过程。
#!/bin/bash
# select.sh
select brand in Samsung Sony iphone symphony Walton
do
  echo "You have chosen $brand"
done
#select可以与case结合,针对不同项,执行不同的命令。
#!/bin/bash
echo "Which Operating System do you like?"
select os in Ubuntu LinuxMint Windows8 Windows7 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


###########
# 12.函数 #
###########
#1. 简介
#函数(function)是可以重复使用的代码片段,有利于代码的复用。它与别名(alias)的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。
#函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。
# 第一种
fn() {
  # codes
}
# 第二种
function fn() {
  # codes
}
#一个简单函数的例子。
hello() {
  echo "Hello $1"
}
#一个多行函数的例子,显示当前日期时间
today() {
  echo -n "Today's date is: "
  date +"%A, %B %-d, %Y"
}
#删除一个函数,可以使用unset命令。
unset -f functionName
#查看当前 Shell 已经定义的所有函数,可以使用declare命令。 declare命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合more或less使用。
declare -f
#declare命令还支持查看单个函数的定义。
declare -f functionName
#declare -F可以输出所有已经定义的函数名,不含函数体。
declare -F

#2. 参数变量
#函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的
$1~$9  #函数的第一个到第9个的参数。
$0  #函数所在的脚本名。
$#  #函数的参数总数。
$@  #函数的全部参数,参数之间使用空格分隔。
$*  #函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
#!/bin/bash
# test.sh
function alice {
  echo "alice: $@"
  echo "$0: $1 $2 $3 $4"
  echo "$# arguments"
}
alice in wonderland
#日志函数
function log_msg {
  echo "[`date '+ %F %T'` ]: $@"
}

#3. return 命令
#return命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。
function func_return_value {
  return 10
}
#return后面不跟参数,只用于返回也是可以的。
function name {
  commands
  return
}

#4. 全局变量和局部变量,local 命令
#Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
#变量$foo是在函数fn内部声明的,函数体外也可以读取。
# 脚本 test.sh
fn () {
  foo=1
  echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
#函数体内不仅可以声明全局变量,还可以修改全局变量。
foo=1
fn () {
  foo=2
}
echo $foov
#函数里面可以用local命令声明局部变量。
#local命令声明的$foo变量,只在函数体内有效,函数体外没有定义。
# 脚本 test.sh
fn () {
  local foo
  foo=1
  echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"


###########
# 13.数组 #
###########


################
# 14.正则表达式 #
################


##################
# 15.处理文本文件 #
##################
#1. awk命令
#1.1 基本用法
# 格式
awk 动作 文件名
# 示例
# $ + 数字表示某个字段
awk '{print $0}' demo.txt  #demo.txt是awk所要处理的文本文件。前面单引号内部有一个大括号,里面就是每一行的处理动作print $0。其中,print是打印命令,$0代表当前行,因此上面命令的执行结果,就是把每一行原样打印出来。
echo 'this is a test' | awk '{print $0}'  #rint $0就是把标准输入this is a test,重新打印了一遍
# awk会根据空格和制表符,将每一行分成若干字段,依次用$1、$2、$3代表第一个字段、第二个字段、第三个字段等等。
echo 'this is a test' | awk '{print $3}'  #$3代表this is a test的第三个字段a
awk -F ':' '{ print $1 }' demo.txt  #文件的字段分隔符是冒号(:),所以要用-F参数指定分隔符为冒号。然后,才能提取到它的第一个字段
#1.2 变量
echo 'this is a test' | awk '{print $NF}'  #变量NF表示当前行有多少个字段,因此$NF就代表最后一个字段
awk -F ':' '{print $1, $(NF-1)}' demo.txt  #$(NF-1)代表倒数第二个字段,print命令里面的逗号,表示输出的时候,两个部分之间使用空格分隔
awk -F ':' '{print NR ") " $1}' demo.txt  #变量NR表示当前处理的是第几行,print命令里面,如果原样输出字符,要放在双引号里面
FILENAME  #当前文件名
FS  #字段分隔符,默认是空格和制表符。
RS  #行分隔符,用于分割每一行,默认是换行符。
OFS  #输出字段的分隔符,用于打印时分隔字段,默认为空格。
ORS  #输出记录的分隔符,用于打印时分隔记录,默认为换行符。
OFMT  #数字输出的格式,默认为%.6g。
#1.3 函数
awk -F ':' '{ print toupper($1) }' demo.txt  #函数toupper()用于将字符转为大写
#tolower()  #字符转为小写。
#length()  #返回字符串长度。
#substr()  #返回子字符串。
#sin()  #正弦。
#cos()  #余弦。
#sqrt()  #平方根。
#rand()  #随机数。
#1.4 条件
awk '条件 动作' 文件名  #awk允许指定输出条件,只输出符合条件的行。输出条件要写在动作的前面
awk -F ':' '/usr/ {print $1}' demo.txt  #print命令前面是一个正则表达式,只输出包含usr的行
awk -F ':' 'NR % 2 == 1 {print $1}' demo.txt  #只输出奇数行
awk -F ':' 'NR >3 {print $1}' demo.txt  #输出第三行以后的行
awk -F ':' '$1 == "root" {print $1}' demo.txt  #输出第一个字段等于指定值的行
awk -F ':' '$1 == "root" || $1 == "bin" {print $1}' demo.txt  #输出第一个字段等于指定值的行
#1.5 if语句
awk -F ':' '{if ($1 > "m") print $1}' demo.txt  #输出第一个字段的第一个字符大于m的行
awk -F ':' '{if ($1 > "m") print $1; else print "---"}' demo.txt  

#2. sed命令
#sed 把每一行都存在临时缓存区中,对这个副本进行编辑,所以不会修改或破坏源文件
sed [option] 'command' <inputfile>
#2.1 正则表达式

#2.2 常用选项
-n  #使用安静模式,在一般情况所有的 STDIN 都会输出到屏幕上,加入-n 后只打印被 sed 特殊处理的行
-e  #多重编辑,且命令顺序会影响结果
-f  #指定一个 sed 脚本文件到命令行执行,
-r  #Sed 使用扩展正则
-i  #直接修改文档读取的内容,不在屏幕上输出 
#2.3 操作命令
#sed 操作命令告诉 sed 如何处理由地址指定的各输入行。如果没有指定地址, sed 就会处理输入的所有的行。
a\  #在当前行后添加一行或多行
c\  #用新文本修改(替换)当前行中的文本
d  #删除行
i\  #在当前行之前插入文本
h  #把模式空间里的内容复制到暂存缓存区
H  #把模式空间里的内容追加到暂存缓存区
g  #取出暂存缓冲区里的内容,将其复制到模式空间,覆盖该处原有内容
G  #取出暂存缓冲区里的内容,将其复制到模式空间,追加在原有内容后面
l  #列出非打印字符
p  #打印行
n  #读入下一输入行,并从下一条命令而不是第一条命令开始处理
q  #结束或退出 sed
r  #从文件中读取输入行
! #对所选行意外的所有行应用命令
s  #用一个字符串替换另一个

#2.4 替换标志
g  #在行内进行全局替换
p  #打印行
w  #将行写入文件
x  #交换暂存缓冲区与模式空间的内容
y  #将字符转换为另一字符(不能对正则表达式使用 y 命令)

#3. 向文件中添加内容
#3.1 vi编辑法
#打开终端,输入vi test.txt 回车,按a或i进入编辑模式,输入 I am a boy,然后按esc键退出编辑模式,输入:wq保存并退出

#3.2 echo命令法
#打开终端,输入
echo ‘I am a boy’ >> ./test.txt  #追加单行文本法

#3.3 cat命令法
#注:结尾的EOF要顶格,成对出现,可以其它字符代替
cat >> ./test.txt <<EOF
I am a boy
EOF

#3.4 cat编辑法
cat >> ./test.txt  #回车后开始编辑输入内容
I am a boy.
#按cntl+d组合键结束编辑

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值