Shell 脚本语言入门
Shell 脚本语言是与Linux系统沟通的桥梁。

Shell 脚本语言入门


一、概念认识

命令行是架设在用户和内核之间的一座桥梁。在Linux下,这个命令行程序叫做 Shell。

  • Shell 是一种脚本语言,我们编写完源码后不用编译,直接运行源码即可。
  • Shell 本身支持的命令并不多,但是它可以调用其他的程序,每个程序就是一个命令,这使得 Shell 命令的数量可以无限扩展。
  • Shell 脚本很适合处理纯文本类型的数据,而 Linux 中几乎所有的配置文件、日志文件(如 NFS、Rsync、Httpd、Nginx、MySQL 等),以及绝大多数的启动文件都是纯文本类型的文件。
  • Shell 必学内容,Linux 正则表达式以及三剑客 grep、awk、sed 等命令。

Shell是一个程序,一般都是放在/bin或者/user/bin目录下,当前 Linux 系统可用的 Shell 都记录在/etc/shells文件中。
不同的组织机构开发了不同的 Shell,它们各有所长,有的占用资源少,有的支持高级编程功能,有的兼容性好,有的重视用户体验。常见的 Shell 有 sh、bash、csh、tcsh、ash、zsh 等。

  • sh 是 UNIX 上的标准 shell。
  • bash 是 Linux 的默认 shell。bash 由 GNU 组织开发,保持了对 sh shell 的兼容性。
  • ash 一个简单的轻量级的 Shell,占用资源少,适合运行于低内存环境,与 bash shell 完全兼容。

(以下内容均基于 bash 编写)

查看当前 Linux 的默认 Shell,$环境变量:

$ echo $SHELL

此外 $PS1$PS2 是提示符的当前格式。
对于普通用户,Base shell 默认的提示符是美元符号 $;对于超级用户(root 用户),Bash Shell 默认的提示符是井号 #

在图形桌面环境(例如 GNOME、KDE、Unity 等),原生 Shell 入口被隐藏了。进入 Shell 的方法是让 Linux 系统退出图形界面模式,进入 Linux 控制台(Console)。 现代 Linux 系统在图形界面启动时会自动创建几个虚拟控制台(Virtual Console),其中一个供图形桌面程序使用,其他的保留原生控制台的样子。虚拟控制台其实就是 Linux 系统内存中运行的虚拟终端(Virtual Terminal)。

二、基础

(一)脚本文件

“脚本”是把Shell集中放在一个文件中,按顺序逐句执行。运行效果和一句句输入一样。

  • 脚本的扩展名不影响脚本执行,没有都可,但一般用.sh
  • 脚本文件需要可执行权限,即需要 chmod +x ./test.sh
  • 脚本内容的第一行必须是#!约定,告诉系统这个脚本需要什么解释器来执行。
#!/bin/bash

单个#是注释,到行尾将被忽略。另,可以把大段要注释的代码用一对花括号 { } 括起来,定义成一个函数,不去调用它,就达到注释效果。

Shell可以将外部脚本的内容合并到当前脚本:

. filename
# 或者
source filename
  • 两种方式的效果相同。注意:点号.和文件名中间有一个空格
  • 被包含脚本不需要有执行权限。

(二)变量

脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。

在 Bash shell 中,全部变量的值都是字符串
无论给变量赋值时有没有使用引号,值都会以字符串的形式存储。

Shell 使用 \ 转义,特殊字符。

变量类型:
(1)局部变量:在脚本命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问。
(2)环境变量所有的程序,都能访问环境变量。有些程序需要环境变量来保证其正常运行。shell脚本中可以定义环境变量。
(3)shell变量:由shell程序设置的特殊变量。特殊变量不遵循命名规则。

  1. 定义变量
variable=value    # 不用引号时,value中不能包含任何空白符(例如空格、Tab缩进等)
variable='value'  # 单引号、双引号有区别。 
variable="value"

注意,赋值号的周围不能有空格!例如ABC = 123 不对。 这和大部分编程语言都不一样。
但 Shell 变量的命名规范和大部分编程语言一样:变量名由数字、字母、下划线组成;必须以字母或者下划线开头;不能使用关键字(通过 help 命令查看保留关键字)。

  1. 变量的值

改变变量值时,和新定义变量形式一样。

  • 以单引号' '包围变量的值时,纯字符串,单引号里面是什么就输出什么,即使内容中有转义、变量和命令(命令需要反引)也会把它们原样输出。
  • 以双引号" "包围变量的值时,会解析里面的变量和命令(命令需要反引)。
  • 单引号中不能嵌套单引号;双引号中可以随便嵌套。

建议: 数字不加引号;字符串都建议加上双引号;除非必须用纯字符串,否则不用单引号。

Shell 支持将命令的执行结果赋值给变量,两种方式:

variable=$(command)     # 推荐
variable=`command`      # 巨坑!强烈不建议!反引号极容易与单引号混淆。

例如:

$ log=$(cat log.txt)
$ echo $log
  1. 调用变量值

使用一个定义过的变量,只要在变量名前面加美元符号 $ 即可;变量名外面可以加花括号{ },推荐加。

有时必须加花括号,是为了让解释器识别变量的边界。例如:$ echo "I am good at ${skill}Script

  1. 声明只读 定义变量之后,可以使用 readonly 命令将变量声明为只读(尝试更改只读变量,报错)。例如:
#!/bin/bash
myUrl="http://see.xidian.edu.cn/cpp/shell/"
readonly myUrl
  1. 删除变量
    使用 unset 命令可以删除变量。调用删除变量的值没有任何输出。unset命令不能删除只读变量。

  2. 变量替换(重点

变量可以根据状态(是否为空、是否定义等)来改变值。

变量替换形式:

# 变量本来的值。
${var}
# 如var被定义且不空,返回 "abc",但不改变var原值;否则返回var值。
${var:+"abc"}
# 如var为空或已被删除(unset),返回 "abc",但不改变var的原值;否则返回var值。
${var:-"abc"}
# 如var为空或已被删除(unset),返回 "abc",并改变var值为 "abc";否则返回var值。
${var:="abc"}
# 如var被定义,返回var的值;如果为空或已被删除(unset),将消息 "message" 送到标准错误输出,脚本停止运行。
${var:?"message"}
  1. 特殊变量(重点
  • $0 当前脚本的文件名。即从终端输入的第一个word。输入的第二word作为 $1

  • $n 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2

  • $# 传递给脚本或函数的参数个数。

  • $* 传递给脚本或函数的所有参数。

  • $@ 传递给脚本或函数的所有参数。被双引号(" “)包含时,与 $* 稍有不同,下面将会讲到。

  • $? 上个命令的退出状态或函数的返回值。一般情况下,成功 0 ,失败 1 ,但没有硬性规定。

  • $$ 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。
    注意 $*$@ 的区别:

    • $*$@ 不被双引号包裹时,都将各参数分开视之,即”$1" “$2” … “$n”。
    • 被双引号包裹时,"$@" 将各个参数分开,即 "$1" "$2" … "$n" ;而 "$*" 将所有的参数作为一个整体,即 "$1 $2 … $n"
  • ${#str} 获取字符串str的长度。例如:str="abcdefg"$ echo ${#str} 输出7

  • ${str:1:4} 提取子字符串。例如:$ echo ${str:1:4} 输出bcde

(三)运算

原生bash不支持数学运算,但是可以通过其他命令来实现,例如 awkexpr

expr 是一款表达式计算工具:加+、减-、乘*(注意必须 \* )、除/、取余%

var=$(expr 1 + 1)
var2=$(expr 2 \* 2)
echo $var $var2

注意: 表达式和运算符之间必须要有空格,必须写成 2 + 2,而2+2不对,这与熟悉的大多数编程语言不一样。

(四)数组

bash支持一维数组,不支持多维数组。
下标由0开始编号。
获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于等于0。

  1. 定义数组
  • 用小括号 () 定义一个数组,数组元素用空格分开。例如:rray_name=(value0 value1 value2 value3)
  • 也可以用中括号 [] 单独定义数组的各个分量:
array_name[0]=value0
array_name[1]=value1
array_name[5]=value2  # 可以不使用连续的下标。
  1. 读取数组元素
  • 单个:valuen=${array_name[2]}
  • 全部:使用 @* 可以获取数组中的所有元素,例如:${array_name[*]}${array_name[@]}
  1. 获取数组长度 获取数组长度的方法与获取字符串长度的方法相同。例如:
length=${#array_name[@]}   # 取得数组元素的个数
length=${#array_name[*]}   # 取得数组元素的个数
lengthn=${#array_name[2]}  # 取得数组单个元素的长度

三、控制

(一)关系判断if

注意: 必须方括号!且必须全部有空格隔开!例如 [$a-le$b] 或者 [$a -le $b] 都是错误的。

1. 变量检测

# shell变量值都是字符串:
[ $a ]       # 检测字符串不为空。
[ -z $a ]    # 检测字符串长度为0。
[ -n $a ]    # 检测字符串长度不为0。
[ $a = $b ]  # 检测两个字符串相同。
[ $a != $b ] # 检测两个字符串不同。
# 可以将shell的变量值视作数字:
[ $a -eq $b ]   # 检测两个数字相等。
# -ne 不等于;-gt 大于;-lt 小于;-ge 大于等于;-le 小于等于。

2. 文件检测

假设 file="/var/www/tutorialspoint/unix/test.sh"

[ -r $file ]  # 检测文件可读。-w 可写;-x 可执行。
[ -s $file ]  # 检测文件不空(文件大小大于0)。
[ -e $file ]  # 检测文件(包括目录)是否存在。
[ -f $file ]  # 检测文件是普通文件(既不是目录,也不是设备文件)。
              # -d 目录;-b 块设备文件;-c 字符设备文件。
[ -g $file ]  # 检测文件设置了 SGID 位。-u 设置了 SUID 位;-k 设置了粘着位(Sticky Bit)。
[ -p $file ]  # 检测文件是具名管道。

3. 逻辑组合

  • 或:-o ,例如 [ $a -lt 20 -o $b -gt 100 ]
  • 与:-a ,例如 [ $a -lt 20 -a $b -gt 100 ]
  • -a优先级高,或-o优先级低。(shell 没有“非”)

IF语句:

#!/bin/sh
a=10
b=20

if [ $a -ne $b ]
then
    echo "a is not equal to b"
else
    echo "a is equal to b"
fi

IF可以写成一行,以命令的方式来运行:

if test $[2*3] -eq $[1+5]; then echo 'The two numbers are equal!'; fi;
# 注意用分号;连接。 test 命令用于检查某个条件是否成立,与方括号 [ ] 类似。

复杂IF语句:

if [ expression 1 ]
then
   Statement(s) to be executed if expression 1 is true
elif [ expression 2 ]
then
   Statement(s) to be executed if expression 2 is true
else
   Statement(s) to be executed if no expression is true
fi

CASE语句:

#!/bin/bash
option="${1}"

case ${option} in
   -f) FILE="${2}"
      echo "File name is $FILE"
      ;;
   -d) DIR="${2}"
      echo "Dir name is $DIR"
      ;;
   *) 
      echo "`basename ${0}`:usage: [-f file] | [-d directory]"
      exit 1 # Command to come out of the program with status 1
      ;;
esac
  • 右括号,值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行,直至 ;;
  • ;; 与其他语言中的 break 类似,不再继续其他模式。
  • 如果无一匹配模式,使用星号 *) 捕获该值,再执行后面的命令。

(二)循环

1. FOR循环

for 变量 in 列表
do
    command1
    command2
done
  • 列表是一组值(数字、字符串等)组成的序列。每个值通过空格分隔。
  • 每循环一次,就将列表中的下一个值赋给变量。 例如:
#!/bin/bash
# 显示主目录下以 .bash 开头的全部文件:
for FILE in $HOME/.bash*
do
   echo $FILE
done

结果输出:

/root/.bash_history
/root/.bash_logout
/root/.bash_profile
/root/.bashrc

2. WHILE循环

while command
do
   Statement(s) to be executed if command is true
done

例如:

COUNTER=0
while [ $COUNTER -lt 5 ]
do
    COUNTER='expr $COUNTER + 1'
    echo $COUNTER
done

3. UNTIL循环

until command
do
   Statement(s) to be executed until command is true
done

4. 循环干预

  • break 命令跳出循环。在嵌套循环中,后面跟一个整数,表示跳出第几层循环。例如:break 2表示跳出第几层循环。
  • continue 命令仅跳出当前循环。同样,continue 3后面也可跟一个数字,表示跳出第几层循环。

(三)函数

1. 先定义函数

function function_name () {
    list of commands
    return value
}
  • 关键字 function 可以省略。
  • 函数返回 return 也可省略,此时最后一条命令的运行结果将作为返回值。
  • shell函数返回值只能是整数,一般表示成功与否,0 表示成功,其他值表示失败。

2. 再调用:

function_name
  • 调用函数只需要给出函数名,不需要加括号。
  • 函数返回值在调用该函数之后如果还想取,可以通过 $? 来获得。

3. 可删除:

unset .f function_name
  • 删除函数要加 .f 选项。

4. 传递参数:

  • 在函数体内部可直接通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数。
  • 建议 ${n} 。如果n>=10,必须加{},否则$10视为$10

四、输出输入

Linux命令默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是终端,即显示器。

一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:

  • 标准输入文件(stdin):文件描述符为 0 ,默认从stdin读取数据。
  • 标准输出文件(stdout):文件描述符为 1 ,默认向stdout输出数据。
  • 标准错误文件(stderr):文件描述符为 2 ,会向stderr流中写入错误信息。

(一)输出重定向

Linux默认标准输出文件(stdout),文件描述符为 1
Linux输出不仅可以是显示器,还可以很容易的转移向到文件。

  1. 命令输出重定向:command > file
  • 显示器上不会看到任何输出。
  • > 输出重定向会覆盖文件内容。
  • 如果不希望文件内容被覆盖,可以使用 >> 追加到文件末尾。
  1. stderr 重定向到 file : command 2 > file 或追加到文件末尾command 2 >> file
  • 注意:2表示标准错误文件(stderr)。
  1. stdout 和 stderr 合并后重定向到 file :command > file 2>&1
  • n >& m 将输出文件 m 和 n 合并。
  • n <& m 将输入文件 m 和 n 合并。
  1. /dev/null
    /dev/null 是一个特殊的文件,写入到它的内容都会被丢弃,command > /dev/null起到“禁止输出”的效果。 如果尝试从该文件读取内容,那么什么也读不到。
  • 具体应用:
    > /dev/null 不写默认为1(stdout)。即标准输出抛弃;异常才会打印。
    2 > /dev/null 2代表(stderr),只有命令返回错误信息才会抛弃,正常信息打印出来。
    > /dev/null 2>error.log stdout标准输出抛弃;错误信息写入error.log。
    > /dev/null 2>&1 标准输出和错误,都抛弃。2>&1 代表redirect the error stream into the output stream。注意如不写 &1 就成为文件名。

(二)输入重定向

可以从文件获取输入:command < file

  • 本来需要从键盘获取输入的命令会转移从文件读取内容。
  1. 如果希望对 stdin 和 stdout 都重定向:command < file1 > file2
  2. << tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入。
    如下:
command << tag
    document
tag
  • 将两个tag之间的document内容作为输入传递给command
  • 结尾的tag一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。

(三)输出的指令

1. echo

echo "aaa"

  • 在 stdout 输出。
  • -n 不在结尾自动换行。
  • -e 若字符串中出现转义格式符,则转义。默认 echo "Value of a is $a \n" 将输出 Value of a is $a \n ;加 -e 后,输出 Value of a is $a
    echo $var1 $var2 不论命令中用什么间隔,输出后,不同变量间仅用一个空格隔开。

2. printf

printf format-string arguments...

  • 不加括号!
  • format-string 为格式控制字符串,可以没有引号,但最好加上,单双引号都行,效果一样。
  • arguments 为参数列表,用空格分隔,不用逗号。
  • arguments 没有或不够,%s为空。
  • 参数多于格式控制符(%)时,format-string 重复调用,例如:printf "%s %s %s\n" a b c d e f g h i j,输出:
a b c
d e f
g h i
j

注意: printf 不像 echo 那样会自动换行,必须显式添加换行符 \n 。如:printf "Hello, Shell\n"

(四)输入的指令

read ABC 从 stdin 获取输入,赋值给 ABC 变量(不用之前声明)。

五、正则表达式

Linux正则表达式一般是以 “行” 为单位处理。

\        # 转义字符。    例如:',' 是一个特殊字符,在正则表达式中有特殊含义。必须要先转义 '\,'

^word    # 以word开头的行
word$    # 以word结束的行
         # 例子:搜寻以 # 开头的脚本注释行:grep –n '^#' regular.txt

.        # 匹配任意1个字符,必须1个
         # 例子:grep –n 'e.e' regular.txt 可匹配eee,eae,eve,但是不匹配ee。
?        # 匹配0个或1个在其之前的那个普通字符
         # 例子:grep –nE 'go?d' regular.txt 可匹配 gd,god
*        # 前面的字符重复0个到多个
         # 例子:grep –n 'go*gle' regular.txt 可匹配 gle,gogle,google,gooogle 等等
+        # 匹配1个或多个在其之前的那个普通字符,重复前面字符1到多次
         # 例子:grep –nE 'go+d' regular.txt 可匹配 god,good,goood 等等

()       # 表示一个字符的集合。    例如:(abcdefg) 匹配的是 abcdefg
         # ()用在expr中,仅匹配括号内的字符串。例如 /\d+(\w+)/ 用在 123abc 将捕获到 abc
|        # 表示“或”,匹配一组可选的字符,或(or)的方式匹配多个字符串
         # 例子:grep –nE 'god|good' regular.txt 可匹配 god或者good         
         # 例子:grep –nE 'g(oo|la)' regular.txt 可匹配 good或者glad

[list]   # 匹配一系列字符中的1个
         # 例子:grep –n 'g[lf]' regular.txt 可匹配 gl,gf
[n1-n2]  # 匹配一个字符范围中的1个字符    例如 [a-zA-Z]
         # 例子:grep –n '[0-9]' regular.txt 可匹配单个数字字符
[^list]  # 匹配字符集以外的字符
         # 例子:grep –n '[^o]' regular.txt` 匹配非o字符

\<abc    # 匹配单词是abc开头的
         # 例子:grep –n '\<g' regular.txt 匹配以g开头的单词
abc\>    # 匹配单词是abc结尾的
         # 例子:grep –n 'tion\>' regular.txt 匹配以tion结尾的单词

\{n1\}   # 前面的字符重复n1个,包括前面的
         # 例子:grep –n 'go\{2\}gle' regular.txt 匹配 google
\{n1,\}  # 前面的字符至少重复n1个
         # 例子:grep –n 'go\{2\}gle' regular.txt 匹配 google,gooogle
\{n1,n2\}  # 前面的字符重复n1,n2次
           # 例子:grep –n 'go\{2,3\}gle' regular.txt 匹配 google,gooogle

例子

#!/bin/sh

# 通过pppoe-wan提取拨号返回的IPv4和IPv6地址
ADD4=""
TMP=$(ifconfig pppoe-wan | grep 'inet addr:[1-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[1-9][0-9]*' | sed 's/:/ /g')
if [ ${#TMP} -gt 20 ]
then 
	HEAD=$(echo $TMP | awk '{print $1$2}')
	if [ ${#HEAD} -eq 8 ]
	then 
		if [ ${HEAD} = 'inetaddr' ]
		then
			ADD4=$(echo $TMP | awk '{print $3}')
		fi
	fi
fi
if [ ${#ADD4} -lt 8 ]; then exit 1; fi

TMP=""
TMP=$(ifconfig pppoe-wan | grep 'inet6 addr: 2...\:.*\:.*\:.*\:.*\:.*\/.*Global' | sed 's#\/# #g')
if [ ${#TMP} -gt 35 ] 
then
	HEAD=""
	HEAD=$(echo $TMP | awk '{print $1$2}')
	if [ ${#HEAD} -eq 10 ]
	then
		if [ ${HEAD} = 'inet6addr:' ]
		then
			ADD6=$(echo $TMP | awk '{print $3}')
			if [ ${#ADD6} -gt 20 ] 
			then
				echo $ADD4
				echo $ADD6
				exit 0
			fi
		fi
	fi
fi

echo $ADD4
exit 0

最后修改于 2024-02-24