Bash脚本DLC:Bash语法和URL检测脚本实例

对前一篇Bash文章的语法补充

Posted by BlackDn on September 26, 2023

“蓝空天末,孤星遥坠。满街游走,打听幸福。”

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

当然,elifthen是是可以没有的
要注意的点其实也就是头尾分别需要iffi包裹、ifelif之后需要加then、条件语句的中括号两边需要有空格。
其实还可以把then和条件语句写在同一行,但是这样就需要分号来分隔:

if [ condition1 ]; then
  ...
elif [ condition2 ]; then
  ...
else
  ...
fi

case 结构

case有点像其它语言的switch,本身也可以用if替代,不过能让代码更简洁易懂

case variable in
  pattern1)
    ...
    ;;
  pattern2)
    ...
    ;;
  *)
    ...
    ;;
esac

if类似,前后分别需要caseesac包裹,每当和一个模式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啥的就是我们的运算表达式,循环体需要用dodone包裹,使用如下:

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  是数组名,value1value2  等是数组中的值。我们用小括号将数组的值包裹,并且元素之间用空格隔开,不需要逗号或分号啥的。
同样,可以用下标取值,也可以重新赋值:

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.com200;[空]
  • https://www.exam000;[空]curl000表示未响应)
  • gmail.com301;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_coderedirect_url),通过分号;分隔,因此可以通过这个分号将两者结构:

status_code=$(echo "$response" | cut -d ";" -f 1)
redirect_url=$(echo "$response" | cut -d ";" -f 2)

response结果echo出来给cut进一步操作,cut-d参数指定分隔符,这里为分号;然后通过-f获取对应位置的值。

判断是否成功并输出

有了status_coderedirect_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 退出状态未知。表示未知的错误或异常情况

保留退出码相当于大家约定俗成的规定,而非强制性的规则,因此在不同命令或脚本之中可能会有出入,最好还是阅读相关文档来明确其退出码的含义。

在命令行(或脚本中),可以通过$?来查看上一条命令的退出码

参考

  1. Linux 脚本:浅浅入下 Bash 编程
  2. What are exit codes in Linux?
  3. curl 官方文档
  4. CURL -w 参数详解
  5. 感谢 ChatGPT