8

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

 9 months ago
source link: https://blackdn.github.io/2023/09/26/Bash-Grammar-2023/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

特殊变量在前置文章的Shell中的变量及声明中也提到过,这里再总结一下

变量 作用
$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表示成功,非零值则表示错误或异常。不过具体的含义还是取决于命令或程序的作者。
虽然原则上我们可以在脚本中随意指定退出码,但是有一些退出码是被广泛接受并保留的,通常用于表示特定的状态或错误情况:

退出码 意义
0 成功执行
1 执行失败,没有特定的含义
2 参数错误,命令后的参数不正确或无效
126 命令不可执行。命令(或脚本)存在但因权限不足等原因无法执行
127 命令未找到。命令(或脚本)不存在
128 无效的退出参数。命令接收到了无效的退出信号而被终止
130 因为Ctrl+C而中断程序执行
255 退出状态未知。表示未知的错误或异常情况

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

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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK