“蓝空天末,孤星遥坠。满街游走,打听幸福。”
Bash 脚本 DLC:Bash 语法和 URL 检测脚本实例
前言
其实之前写过一篇 bash 脚本的文章:Linux 脚本:浅浅入下 Bash 编程
不过由于那篇文章主要介绍了一下 Bash,然后就开始写脚本。虽然对脚本里涉及的知识有一些讲解,但对Bash 语法本身没有一个统一的介绍,所以在这里补上。
两者可能会有一些知识重复了(比如特殊变量,条件表达式等),以及一些各自特有的点(比如上篇文章提到了Shebang 注释,这篇就没提)
不过问题不大,两篇都看不就好了嘛(手动狗头)。
估计是国庆前最后一篇文章,祝你国庆快乐~
Bash 脚本语法
变量
作为轻量级脚本语言,Bash 当然不需要什么类型声明,也不需要声明变量,直接赋值就好了。不过变量名必须以字母或下划线开头,后面可以跟字母、数字或下划线。我们也可以在脚本中随意地更改变量的值,不过在引用变量的时候需要在前面加上$
name="black"
echo "hello $name"
name="blackdn"
echo "hello $name"
>> hello black
hello blackdn
特殊变量
Bash 保留了一些特殊变量以便在脚本中使用:
变量 | 作用 |
---|---|
$0 |
当前脚本的文件名 |
$1 ~$n |
对应的输入参数,$1 表示第一个参数,$2 表示第二个参数,以此类推 |
$# |
输入参数的个数 |
$@ |
是一个列表,包含所有脚本输入参数 |
$* |
和$@ 类似,不过是一个包含全部参数的字符串,以空格分隔 |
$? |
表示上一个命令的退出码(exit code) |
需要注意的是,如果想表示第 10 个参数,需要加上花括号:${10}
。$10
会被解释为第一个参数 $1
后面跟着字符"0"
,而不是第十个参数。大于 10 的参数也一样嗷。
假设我们写了一个脚本test.sh
,内容如下:
#!/usr/bin/env bash
echo "The script name is $0"
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The number of parameters is $#"
echo "The parameter list is $@"
echo "The parameter list is $*"
echo "The exit status of the last command is $?"
我们去命令行执行它(如果出现permission denied
,则说明这个.sh 脚本文件没有执行权限,使用 chmod 为其添加执行权限即可,具体权限问题可以查看Linux 权限及 chmod 命令
./test.sh para1 para2 para3
>>
The script name is ./test.sh
The first parameter is para1
The second parameter is para2
The number of parameters is 3
The parameter list is para1 para2 para3
The parameter list is para1 para2 para3
The exit status of the last command is 0
最后再区分一下$@
和$*
:
$@
:$@
会将命令行参数视为一个参数列表,每个参数都是独立的字符串。这意味着脚本可以逐个访问参数,而不会将它们合并成一个字符串。通常在循环遍历的时候会用到。$*
:$*
将所有的命令行参数当作一个字符串处理。它会将所有参数合并成一个以空格分隔的字符串,并将其视为一个整体。
举个例子,当我们执行某个脚本:./myscript.sh arg1 arg2 "arg3 with spaces"
在上述命令中我们传入了三个参数,当我们用$@
进行遍历,会循环三次;而$*
则只会循环一次:
for arg in $*; do
echo $arg
done
> arg1 arg2 arg3 with spaces
for arg in $@; do
echo $arg
done
>
arg1
arg2
arg3 with spaces
条件判断
if-elseif-else 结构
直接来看看if-elseif-else
的结构:
if [ condition1 ]
then
...
elif [ condition2 ]
then
...
else
...
fi
当然,elif
和then
是是可以没有的
要注意的点其实也就是头尾分别需要if
和fi
包裹、if
和elif
之后需要加then
、条件语句的中括号两边需要有空格。
其实还可以把then
和条件语句写在同一行,但是这样就需要分号来分隔:
if [ condition1 ]; then
...
elif [ condition2 ]; then
...
else
...
fi
case 结构
case
有点像其它语言的switch
,本身也可以用if
替代,不过能让代码更简洁易懂
case variable in
pattern1)
...
;;
pattern2)
...
;;
*)
...
;;
esac
和if
类似,前后分别需要case
和esac
包裹,每当和一个模式pattern
匹配后就会进入这个模式,执行其中的代码;每个模式条件后要带一个右括号,而每个匹配模式后需要以两个分号;;
就结尾。
*
代表任意模式,常用来作为默认模式进行处理。此外,还可以使用 ?
来匹配单个字符,使用 []
来匹配指定范围内的字符
item=5
case "$item" in
1)
echo "item = 1"
;;
2 | 3)
echo "item = 2 or item = 3"
;;
[4-6])
echo "item beteween 4 - 6"
;;
*)
echo "default (none of above)"
;;
esac
>> item beteween 4 - 6
在看别人写脚本的时候,会在条件判断里使用很多条件表达式(),看起来很高级好用,这里整理一下放在附录 1
字符串
字符串长度
在字符串变量前面加个井号#
就可以输出其长度了:
${#string}
name="blackdn"
echo "${#name}"
>> 7
连接字符串
我们知道字符串通常用引号包裹,然后引用变量的时候为了易读性也会加个引号,这两者的引号可以嵌套,而且一个引号中可以包裹多个变量,从而简单地连接两个字符串
name="blackdn"
appearance="handsome"
echo "hello, "$appearance $name""
>> hello, handsome blackdn
截取字符串
可以用以下语法截取字符串:
${string:position:length}
string
是需要截取的字符串,position
是起始位置,length
是截取的长度。
name="blackdn"
greeting="hello, "$name""
echo "${greeting:1:8}"
>> ello, bl
替换字符串
可以用以下语法替换字符串:
${string/old/new}
string
是需要替换的字符串,old
是需要被替换的字符串,new
是替换后的字符串(可以为空)。
name="blackdn"
greeting="hello, "$name""
echo "${greeting/hello/goodbye}"
>> goodbye, blackdn
离谱的是不管字符串是不是变量,都不用加引号=。=
加了引号反而还会把引号一起换进去,真离谱,我直接搞混了变量和字符串值
不过上述方法只会讲匹配到的第一个old
替换为new
。如果想让所有的old
都被替换,需要加两个斜杠:
${string//old/new}
str="sadblackdnsad"
result=${str//sad/}
echo "${result}"
>> blackdn
循环
for 循环
由于在之前文章的“括号”一栏中提到,双括号(())
之中可以进行运算且内部的字符会自动视为变量而无需$
,于是for-i
循环的模版如下:
for (( expression1; expression2; expression3 ))
do
...
done
所谓expression1
啥的就是我们的运算表达式,循环体需要用do
和done
包裹,使用如下:
n=3
for ((i = 0; i < n; i++))
do
echo "${i}"
done
>
0
1
2
不过似乎在 bash 中使用更多的还是for-each
循环:
for variable in values
do
...
done
variable
是自定义名称的循环变量,values
是需要循环遍历的值列表。
for i in 1 2 3 4
do
echo "number: $i"
done
>
number: 1
number: 2
number: 3
number: 4
我们还可以用一些命令或特殊语法来创造一个序列进行for-each
循环:
for item in $(seq 1 3) # seq命令生成1-3序列
do
echo "${item}"
done
for item in {1..3} # 花括号生成1-3序列
do
echo "${item}"
done
>
1
2
3
while 循环
模版如下:
while condition
do
...
done
其中 condition
是需要判断的条件,由于常涉及比较逻辑,因此常用[]
或[[]]
包裹:
i=1
while [ $i -le 3 ] # i小于等于3
do
echo "$i"
i=$((i+1))
done
>
1
2
3
-le
是 bash 的整数比较符之一,表示小于等于,具体的放在附录 2了,这里就先略过
until 循环
until 循环和while 循环很类似,不过他们的执行逻辑却相反:while 循环当条件成立时执行循环体,但until 循环则是当条件成立时结束循环
until condition
do
...
done
i=1
until [ $i -gt 3 ] # i大于3
do
echo "$i"
i=$((i + 1))
done
>
1
2
3
不论是while 循环还是until 循环,记得在循环体中改变比较的值来避免死循环
函数
Bash 允许我们声明一个函数将代码封装,以便多次使用,使得整个代码更加清晰易读,更加模块化。
有几种方法可以声明一个函数:
function function_name {
...
}
# or
function_name() {
...
}
# or
function greet () {
...
}
当我们定义好一个函数后,后续直接用函数名就可以调用函数了,不需要加括号啥的
function greet () {
echo "Hello World!"
}
greet
>
Hello World!
而函数的参数也不需要在括号中声明,只需要在函数体中使用特殊变量,然后调用的时候加上需要传入的参数就行了。我们的函数会根据参数的顺序将其放在相应的位置:
function greet () {
echo "Hello $1!"
}
greet "blackdn"
>
Hello blackdn!
所以其实我们定义的函数更像是一个命令,调用函数就像是在命令行使用这个命令。
此外,我们还可以用return
来为函数设置返回值,返回值可以是整数、字符串等
sum() {
result=$(($1 + $2))
return $result
}
sum 2 4
echo "result is $?"
> 6
在上面的特殊变量一栏中,我们知道$?
表示上一条命令的退出码。其实每条命令都会有一个退出码,默认是0
(表示执行成功)或者1
(表示执行失败)。我们用return
就相当于重新指定了一下这个退出码。
更多和退出码相关的内容放在附录 3
数组
在 Bash 脚本中,可以使用数组来存储多个值进行遍历和操作。和 Python 类似,Bash 允许将不同类型的数组存入同一个数组中,包括整数、字符串等。
array_name=(value1 value2 ... valuen)
array_name
是数组名,value1
、value2
等是数组中的值。我们用小括号将数组的值包裹,并且元素之间用空格隔开,不需要逗号或分号啥的。
同样,可以用下标取值,也可以重新赋值:
animals=(dog cat bird)
echo "${animals[0]}"
> dog
animals[2]=fish
echo "${animals[2]}"
> fish
animals[3]=sheep
echo "${animals[3]}"
> sheep
此外,结合特殊变量的使用方法,我们可以很快获取数组的全部元素和长度:
animals=(dog cat bird)
echo "${animals[@]}"
> dog cat fish
echo "${#animals[@]}"
> 3
animals[3]=sheep
echo "${#animals[@]}"
> 4
我们可以将我们获取的全部元素(${animals[@]}
)看作数组本体(你看他输出的时候也是用空格分开的嘛),所以可以用它来实现数组的遍历:
animals=(dog cat bird)
for animal in "${animals[@]}"; do
echo "${animal}"
done
>
dog
cat
bird
在 Bash 脚本中,可以使用以下语法来对数组进行切片:
${array_name[@]:start_index:length}
其中 start_index
是切片的起始下标,length
是切片的长度。
animals=(dog cat bird)
echo "${animals[@]:1:2}"
> cat bird
实例:URL 检测脚本
需求描述
我们需要一个脚本,用于检测指定 URL 是否可用。
如果可用,则返回状态码200
;
否则检测是否有重定向,如果有则输出重定向目标;
如果没有重定向,则说明 URL 访问失败,输出错误的状态码。
开始实现
核心功能:访问 URL
我们这回先从核心功能开始入手:访问指定 URL
当然首选curl
啦,不过在默认情况下curl
默认会返回页面的 HTML(成功情况),或者返回错误信息(失败情况),这并不符合上述需求,所以先来为其指定一些参数:
- 因为访问错误的情况不需要输出错误信息,所以需要
-s
参数来表示:-s
或--silent
表示静默模式,不输出任何中间状态的信息或错误信息 - 但是
-s
参数无法阻止curl
在成功时输出页面的 HTML 代码,因此我们需要将输出重定向到黑洞文件/dev/null
中,读取他不会有任何输出,写入他不会有任何存储。输出为文件的参数为-o
或--output
,因此要加上-o /dev/null
- 最后我们需要拿到状态码或重定向的目标,因此需要用到
-w
或--write-out
参数,这个参数能够按照我们的格式化形式输出对应的信息,包括但不限于返回码http_code
,HTML 代码http_content
,请求总时间time_total
,重定向地址redirect_url
等,我们要用的自然是返回码http_code
和重定向地址redirect_url
(如果没有重定向则该值为空)。因此要加上-w "%{http_code};%{redirect_url}"
最后我们的curl
命令是这样的:
curl -s -o /dev/null -w "%{http_code};%{redirect_url}" MY_URL
对于这个命令我们可以更改MY_URL
来测试,比如:
https://www.example.com
:200;[空]
https://www.exam
:000;[空]
(curl
的000
表示未响应)gmail.com
:301;https://mail.google.com/mail/u/0/
接受 URL 变量并访问
我们的脚本只接收一个参数,即要访问的 URL,我们为其设置一个变量,并且把上面的核心curl
命令放进来:
declare -r URL="${1:?invalid params.}"
response=$(curl -s -o /dev/null -w "%{http_code};%{redirect_url}" "$URL")
declare -r
表示我们的URL
变量是只读的(readonly),下面吧这个 URL 交给curl
去访问。得益于-s
和-o
我们不会有其他任何输出,将结果交给response
解构结果并
拿到response
之后,根据我们的格式化,我们知道其只有两个字段(http_code
和redirect_url
),通过分号;
分隔,因此可以通过这个分号将两者结构:
status_code=$(echo "$response" | cut -d ";" -f 1)
redirect_url=$(echo "$response" | cut -d ";" -f 2)
将response
结果echo
出来给cut
进一步操作,cut
的-d
参数指定分隔符,这里为分号;然后通过-f
获取对应位置的值。
判断是否成功并输出
有了status_code
和redirect_url
判断就很简单了:
- 如果
status_code = 200
,那么说明可用,直接输出 URL 可用的信息 - 然后我们判断
redirect_url
参数是否有值,有值说明发生了重定向,所以用-n
判断其是否存在,输出重定向的信息。之所以不用status_code
判断是因为3xx
的状态码都可以表示重定向,总不能一个个写吧。 - 如果
status_code
不为200
,且redirect_url
为空,那么说明访问失败,输出 URL 不可用的信息
if [ "$status_code" -eq 200 ]; then
echo "URL: $URL 可用"
else
if [ -n "$redirect_url" ]; then
echo "URL: $URL 重定向至 $redirect_url"
else
echo "URL: $URL 不可用,返回状态码:$status_code"
fi
fi
运行脚本
最后我们的脚本就是这样:
#!/usr/bin/env bash
declare -r URL="${1:?invalid params.}"
response=$(curl -s -o /dev/null -w "%{http_code};%{redirect_url}" "$URL")
status_code=$(echo "$response" | cut -d ";" -f 1)
redirect_url=$(echo "$response" | cut -d ";" -f 2)
if [ "$status_code" -eq 200 ]; then
echo "URL: $URL 可用"
else
if [ -n "$redirect_url" ]; then
echo "URL: $URL 重定向至 $redirect_url"
else
echo "URL: $URL 不可用,返回状态码:$status_code"
fi
fi
测试一下咧:(脚本名为test.sh
)
./test.sh https://www.example.com
> URL: https://www.example.com 可用
./test.sh https://www.exam
> URL: https://www.exam 不可用,返回状态码:000
./test.sh gmail.com
> URL: gmail.com 重定向至 https://mail.google.com/mail/u/0/
需求增加:多个 URL
上面我们只能跟一个参数,访问一个 URL,如果我想访问多个要怎么处理呢?
首先判断一下,当没有变量的时候报个错并退出脚本:
if [ $# -eq 0 ]; then
echo "invalid params."
exit 0
fi
然后我们也不需要额外的变量接收参数了,直接遍历全部参数就好,方便起见我们循环的时候单个变量仍为URL
:
for URL in "$@"; do
...
done
然后把之前的代码复制进来就好了,整体如下:
#!/usr/bin/env bash
if [ $# -eq 0 ] then
echo "invalid params."
exit 0
fi
for URL in "$@"; do
response=$(curl -s -o /dev/null -w "%{http_code};%{redirect_url}" "$URL")
status_code=$(echo "$response" | cut -d ";" -f 1)
redirect_url=$(echo "$response" | cut -d ";" -f 2)
if [ "$status_code" -eq 200 ]; then
echo "URL: $URL 可用"
else
if [ -n "$redirect_url" ]; then
echo "URL: $URL 重定向至 $redirect_url"
else
echo "URL: $URL 不可用,返回状态码:$status_code"
fi
fi
done
最后测试一下:
./test.sh https://www.example.com https://www.exam gmail.com
>
URL: https://www.example.com 可用
URL: https://www.exam 不可用,返回状态码:000
URL: gmail.com 重定向至 https://mail.google.com/mail/u/0/
嘎嘎好用
附录 1:条件表达式
表达式 | 意义 |
---|---|
-f file |
文件是否存在 |
-d /xx |
目录是否存在 |
-s file |
文件是否存在且非空 |
-r file |
文件是否存在且可读 |
-w file |
文件是否存在且可写 |
-x file |
文件是否存在且可执行 |
-z $str |
str 字符串长度是否为 0 |
-n $str |
str 字符串长度是否不为 0 |
附录 2:整数比较符号
比较符 | 作用 | 表达式 |
---|---|---|
-eq |
equal,相等 | == |
-ne |
not equal,不相等 | != |
-gt |
greater than,大于 | > |
-lt |
less than,小于 | < |
-ge |
greater equal,大于等于 | >= |
-le |
less equal,小于等于 | <= |
需要注意的是,比较符需要在单中括号[]
中使用,而表达式则需要在双中括号中[[]]
使用
和其他语言有所出入的一点是,上述比较结果为0
则代表True
,比较成立;为1
代表False
,比较失败。
附录 3:退出码
退出码(Exit Code) ,也称返回码(Return Code),用于表示一个命令或脚本的执行结果,范围从0~255
。一般情况下0
表示成功,非零值则表示错误或异常。不过具体的含义还是取决于命令或程序的作者。
虽然原则上我们可以在脚本中随意指定退出码,但是有一些退出码是被广泛接受并保留的,即保留退出码(Preserved Exit Code) ,通常用于表示特定的状态或错误情况:
退出码 | 意义 |
---|---|
0 | 成功执行 |
1 | 执行失败,没有特定的含义 |
2 | 参数错误,命令后的参数不正确或无效 |
126 | 命令不可执行。命令(或脚本)存在但因权限不足等原因无法执行 |
127 | 命令未找到。命令(或脚本)不存在 |
128 | 无效的退出参数。命令接收到了无效的退出信号而被终止 |
130 | 因为Ctrl+C 而中断程序执行 |
255 | 退出状态未知。表示未知的错误或异常情况 |
保留退出码相当于大家约定俗成的规定,而非强制性的规则,因此在不同命令或脚本之中可能会有出入,最好还是阅读相关文档来明确其退出码的含义。
在命令行(或脚本中),可以通过$?
来查看上一条命令的退出码