Linux之Shell编程(一)
1. Shell 介绍
1.1 简介
Shell 是一个用 C 语言编写的程序,它作为用户与操作系统内核之间的桥梁,允许用户通过输入命令来访问和使用操作系统的内核服务。从功能上来说,它类似于 DOS 系统中的command.com
和 Windows 系统中的cmd.exe
。
Shell 具有双重属性:它既是一种命令语言,用户可以通过它输入命令与系统交互;同时也是一种程序设计语言,支持变量、函数、流程控制等编程元素。通常所说的 Shell 编程,指的是编写 Shell 脚本(一系列命令的集合),而不是开发 Shell 解释器本身。
1.2 Shell 解释器
进行 Shell 编程需要两个基本工具:一个用于编写代码的文本编辑器(如 vi、vim、nano 等)和一个能够解释执行脚本的解释器。
Linux 系统中存在多种 Shell 解释器,常见的包括:
- bash(Bourne Again SHell)
- sh(Bourne SHell)
- csh(C SHell)
- ksh(Korn SHell)
可以通过执行cat /etc/shells
命令查看当前系统已安装的所有 Shell 解释器。
在这些解释器中,bash 由于其易用性、功能丰富性以及免费开源的特点,在日常工作中被广泛使用,并且是绝大多数 Linux 发行版的默认 Shell 解释器。
2. 快速入门
2.1 编写 Shell 脚本
编写 Shell 脚本的基本步骤如下:
创建脚本文件:使用文本编辑器(如 vi)新建一个文件,文件名通常以
.sh
为扩展名(这只是一种约定,不影响脚本的实际执行),例如hello.sh
。编写脚本内容:
完整的
hello.sh
脚本内容如下:#!/bin/bash echo "Hello World !"
- 脚本的第一行必须是
#!/bin/bash
(称为 shebang),这一行的作用是告诉操作系统该脚本需要使用 bash 解释器来执行。 - 之后可以编写具体的命令,例如使用
echo
命令输出文本信息:echo "Hello World !"
- 脚本的第一行必须是
赋予执行权限:刚创建的脚本文件默认只有读(r)和写(w)权限,没有执行(x)权限,需要通过
chmod
命令赋予执行权限。- 执行
chmod +x ./hello.sh
命令为脚本添加执行权限。 - 可以通过
ls -l hello.sh
命令查看权限变化:未授权前显示为-rw-r--r--
,授权后显示为-rwxr-xr-x
。
- 执行
2.2 执行 Shell 脚本
执行 Shell 脚本主要有以下三种方式:
方式一:
./hello.sh
- 这种方式是在当前目录下直接执行脚本,
./
表示当前目录。 - 执行前必须确保脚本已经获得执行权限(通过
chmod +x
设置)。
- 这种方式是在当前目录下直接执行脚本,
方式二:
/root/shelldemo/hello.sh
- 这种方式是通过指定脚本的完整路径来执行脚本,无论当前处于哪个目录都可以使用。
- 同样需要提前给脚本赋予执行权限。
方式三:
sh hello.sh
(或sh /root/shelldemo/hello.sh
)- 这种方式是直接调用
sh
解释器,并将脚本文件作为参数传递给解释器。 - 不需要提前给脚本赋予执行权限,因为此时是解释器在执行,而不是脚本本身直接执行。
- 这种方式是直接调用
需要注意的是,直接在命令行输入hello.sh
执行脚本会失败,因为系统会到PATH
环境变量所指定的目录中去查找该脚本,而当前目录通常不在PATH
中,所以系统无法找到该脚本。
3. Shell 程序:变量
3.1 语法格式
变量定义的基本语法格式为变量名=值
,定义时需要遵循以下规则:
- 等号
=
两边不能有空格,例如name="zhangsan"
是正确的,而name = "zhangsan"
或name= "zhangsan"
都是错误的。 - 变量名的首字符必须是字母(a-z 或 A-Z),不能以数字或其他符号开头。
- 变量名中间可以包含字母、数字和下划线(_),但不能包含空格和标点符号。
- 变量名不能使用 bash 的关键字(可以通过
help
命令查看 bash 的关键字列表)。
变量命名通常遵循 "见名知意" 的原则,单个单词一般使用小写字母,多个单词组成的变量名通常用下划线分隔,例如your_name
、student_age
、file_path
等。
3.2 变量使用
使用已定义的变量时,需要在变量名前加上$
符号,具体方式有两种:
$变量名
:例如定义your_name="bigdata.com"
后,执行echo $your_name
可以输出变量的值。${变量名}
:这种方式与第一种效果相同,但可以帮助解释器更清晰地识别变量的边界,避免歧义。例如echo ${your_name}
同样可以输出变量的值;当需要在变量后直接连接其他字符时,这种方式尤为有用,如echo ${your_name}123
会输出bigdata.com123
,而echo $your_name123
则无法正确识别变量。
变量的重新赋值与删除:
普通变量可以被重新赋值,例如先定义
name="bigdata"
,之后可以执行name="hadoop"
将变量值修改为 "hadoop"。使用
readonly
命令可以定义只读变量,语法为readonly 变量名=值
(或先定义变量再执行readonly 变量名
)。只读变量的值一旦设定就不能被修改,例如readonly name="bigdata"
后,再执行name="hadoop"
会报错。使用
unset
命令可以删除变量,语法为unset 变量名
,例如unset name
会删除name
变量。但unset
命令不能删除只读变量,尝试删除会报错。
3.3 变量类型
Shell 中的变量主要分为以下两类:
局部变量
- 局部变量是在脚本或命令中定义的变量,仅在当前 Shell 实例中有效,其他 Shell 进程或由当前 Shell 启动的子进程无法访问这些变量。
- 例如,在当前 Shell 窗口中定义
name="hadoop"
,执行echo $name
可以正常输出 "hadoop";但如果打开一个新的 Shell 窗口(新的 Shell 实例),再执行echo $name
则无法输出任何内容,因为该变量在新的 Shell 实例中不存在。
全局变量(环境变量)
- 全局变量(环境变量)是可以被所有程序(包括当前 Shell 和由它启动的所有子进程)访问的变量。一些系统程序和应用程序需要依赖特定的环境变量才能正常运行。
- 可以使用
set
命令查看当前 Shell 中的所有变量(包括局部变量和环境变量),使用env
或printenv
命令可以只查看环境变量。 - 常见的环境变量有
PATH
(系统查找命令的路径)、HOME
(当前用户的主目录)、USER
(当前登录的用户名)等。
4. 字符串
字符串是 Shell 编程中最常用的数据类型之一,字符串的表示方式有以下几种:
4.1 单引号字符串
单引号字符串的特点:
单引号内的所有字符都会原样输出,变量在单引号字符串中不会被解析,例如:
skill='linux' str='I am good at $skill' echo $str # 输出:I am good at $skill
单引号
字符串中不能出现单独的单引号(即使使用转义符
\
转义也不行),但可以成对出现单引号来实现字符串拼接,例如:str='He said ''Hello'' to me' echo $str # 输出:He said Hello to me
4.2 双引号字符串
双引号字符串相比单引号字符串更加灵活,具有以下特点:
双引号内的变量会被解析并替换为其实际值,例如:
skill='linux' str="I am good at $skill" echo $str # 输出:I am good at linux
双引号内可以使用转义符
\
来转义特殊字符,例如:str="He said \"Hello\" to me" echo $str # 输出:He said "Hello" to me
4.3 获取字符串长度
使用${#字符串变量名}
可以获取字符串的长度,例如:
skill='hadoop'
echo ${#skill} # 输出:6(因为"hadoop"有6个字符)
4.4 提取子字符串
Shell 中可以通过以下方式提取子字符串:
从指定索引开始截取到字符串末尾:
${字符串变量名:起始索引}
(索引从 0 开始),例如:str="I am good at hadoop" echo ${str:2} # 输出:am good at hadoop(从索引2开始截取)
从指定索引开始截取指定长度的子字符串:
${字符串变量名:起始索引:长度}
,例如:str="I am good at hadoop" echo ${str:2:2} # 输出:am(从索引2开始,截取2个字符) echo ${str:5:4} # 输出:good(从索引5开始,截取4个字符)
注意:字符串中的空格也会被算作一个字符。
4.5 查找子字符串
使用expr index "$字符串变量名" 子字符串
可以查找子字符串中任意字符在原字符串中首次出现的位置(位置从 1 开始计算),例如:
str="I am good at hadoop"
echo `expr index "$str" am` # 输出:3(字符'a'在原字符串中首次出现的位置是3)
echo `expr index "$str" do` # 输出:7(字符'o'在原字符串中首次出现的位置是7)
注意:该命令的结果是子字符串中任意字符在原字符串中最早出现的位置,而不是整个子字符串的位置。
5. Shell 程序:参数传递
5.1 参数传递方式
在执行 Shell 脚本时,可以向脚本传递参数,具体方式如下:
传递参数:执行脚本时,在脚本文件名后面加上参数,参数之间用空格分隔,格式为
./脚本名 参数1 参数2 参数3 ...
,例如./hello.sh A 100 "test"
。在脚本内部获取参数:使用
$n
的形式来获取传递的参数,其中n
是一个数字:$0
表示当前脚本的名称(包括路径,取决于执行脚本的方式)。$1
表示第一个参数,$2
表示第二个参数,以此类推,$9
表示第九个参数,${10}
表示第十个参数(当数字大于 9 时,需要用花括号包裹)。
示例:编写hello.sh
脚本,内容如下:
#!/bin/bash
echo "脚本名称:$0"
echo "第一个参数:$1"
echo "第二个参数:$2"
echo "第三个参数:$3"
执行./hello.sh A 100 "test"
,输出结果为:
脚本名称:./hello.sh
第一个参数:A
第二个参数:100
第三个参数:test
5.2 特殊字符
Shell 中有一些特殊的变量,用于处理传递的参数,常用的有:
$#
:表示传递到脚本的参数的个数,例如:# 脚本内容 echo "参数个数:$#" # 执行./test.sh 1 2 3,输出:参数个数:3
$*
:以一个单字符串的形式显示所有传递给脚本的参数,例如:# 脚本内容 echo "所有参数:$*" # 执行./test.sh 1 2 3,输出:所有参数:1 2 3
$$
:表示当前脚本运行的进程 ID 号(PID),例如:# 脚本内容 echo "当前进程ID:$$" # 执行脚本会输出当前脚本的进程ID
$!
:表示最后一个在后台运行的进程的 ID 号,例如:# 脚本内容 sleep 100 & echo "后台进程ID:$!" # 执行脚本会输出sleep进程的ID
$@
:与$*
类似,也用于显示所有传递给脚本的参数,但在使用引号括起来时,$@
会将每个参数作为一个独立的字符串,而$*
会将所有参数作为一个整体字符串,例如:# 脚本内容 echo "使用\$@:$@" # 执行./test.sh 1 2 3,输出:使用$@:1 2 3
$?
:表示最后一个命令的退出状态。通常,0 表示命令执行成功,非 0 表示命令执行失败,例如:# 执行成功的命令 ls echo $? # 输出:0 # 执行失败的命令 ls non_existent_file echo $? # 输出:2(不同命令的错误码可能不同)
5.3 $*和$@的区别
$*
和$@
在大多数情况下表现相似,但在被双引号""
包含时,它们的行为有所不同:
当不被双引号包含时,
$*
和$@
都以$1 $2 ... $n
的形式展开所有参数,例如:# 脚本内容 for arg in $*; doecho "参数:$arg" done echo "---------" for arg in $@; doecho "参数:$arg" done # 执行./test.sh "a b" c,两者输出相同: # 参数:a # 参数:b # 参数:c # --------- # 参数:a # 参数:b # 参数:c
当被双引号包含时,
"$*"
会将所有参数合并为一个字符串,形式为"$1 $2 ... $n"
;而"$@"
会将每个参数都作为一个独立的字符串,形式为"$1" "$2" ... "$n"
,例如:# 脚本内容 for arg in "$*"; doecho "参数:$arg" done echo "---------" for arg in "$@"; doecho "参数:$arg" done # 执行./test.sh "a b" c,输出: # 参数:a b c # --------- # 参数:a b # 参数:c
6. Shell 程序:运算符
Shell 支持多种运算符,主要包括算术运算符、关系运算符、逻辑运算符、字符串运算符和文件测试运算符。
6.1 算术运算符
原生的 bash 不支持直接进行算术运算,需要借助其他方式来实现,常用的有以下几种:
使用
expr
命令expr
是一个用于表达式求值的工具,可以进行基本的算术运算。- 语法格式:
result=
expr 运算式 ``(注意:运算式中的运算符两边必须有空格,且整个表达式需要用反引号` 包含)。 - 支持的算术运算符:
+
(加)、-
(减)、*
(乘,需要转义\*
)、/
(除)、%
(取余)。 a=10 b=3 echo `expr $a + $b` # 输出:13 echo `expr $a - $b` # 输出:7 echo `expr $a \* $b` # 输出:30(乘法需要转义) echo `expr $a / $b` # 输出:3(整数除法) echo `expr $a % $b` # 输出:1
使用
(( ))
- 这种方式可以直接在双括号中进行算术运算,支持自增(
++
)、自减(--
)等操作。 - 示例:
a=5 b=2 ((c = a + b)) echo $c # 输出:7 ((a++)) # a自增1,变为6 echo $a # 输出:6 ((b--)) # b自减1,变为1 echo $b # 输出:1
- 这种方式可以直接在双括号中进行算术运算,支持自增(
使用
$(( ))
- 这种方式可以将算术运算的结果赋值给变量,语法为
变量=$((运算式))
。 - 示例:
a=10 b=4 c=$((a - b)) echo $c # 输出:6 d=$((a * b)) echo $d # 输出:40
- 这种方式可以将算术运算的结果赋值给变量,语法为
使用
$[ ]
- 这种方式与
$(( ))
类似,也可以进行算术运算,语法为变量=$[运算式]
。 - 示例:
a=8 b=3 c=$[a + b] echo $c # 输出:11 d=$[a / b] echo $d # 输出:2
- 这种方式与
6.2 关系运算符
关系运算符主要用于比较两个数字(字符串形式的数字也可以),返回布尔值(true 或 false)。在 Shell 中,true 通常用 0 表示,false 用非 0 表示。常用的关系运算符有:
-eq
:检测两个数是否相等,相等返回 true。a=5; b=5 if [ $a -eq $b ]; then echo "相等"; else echo "不相等"; fi # 输出:相等
-ne
:检测两个数是否不相等,不相等返回 true。a=5; b=6 if [ $a -ne $b ]; then echo "不相等"; else echo "相等"; fi # 输出:不相等
-lt
:检测左边的数是否小于右边的数,是则返回 true。a=3; b=5 if [ $a -lt $b ]; then echo "小于"; else echo "不小于"; fi # 输出:小于
-gt
:检测左边的数是否大于右边的数,是则返回 true。a=7; b=5 if [ $a -gt $b ]; then echo "大于"; else echo "不大于"; fi # 输出:大于
-le
:检测左边的数是否小于或等于右边的数,是则返回 true。a=5; b=5 if [ $a -le $b ]; then echo "小于等于"; else echo "大于"; fi # 输出:小于等于
-ge
:检测左边的数是否大于或等于右边的数,是则返回 true。a=6; b=5 if [ $a -ge $b ]; then echo "大于等于"; else echo "小于"; fi # 输出:大于等于
6.3 逻辑运算符
逻辑运算符用于连接多个条件,进行逻辑运算,常用的有:
-a
:逻辑与,当两个条件都为 true 时,结果为 true。a=10; b=5; c=0 if [ $a -gt $b -a $a -gt $c ]; then echo "两个条件都成立"; else echo "至少一个条件不成立"; fi # 输出:两个条件都成立
-o
:逻辑或,当两个条件中至少有一个为 true 时,结果为 true。a=10; b=100; c=0 if [ $a -gt $b -o $a -gt $c ]; then echo "至少一个条件成立"; else echo "两个条件都不成立"; fi # 输出:至少一个条件成立
&&
:逻辑与,与-a
类似,但使用方式不同,通常用于(( ))
或命令之间。a=10; b=5; c=0 if (( $a > $b && $a > $c )); then echo "两个条件都成立"; else echo "至少一个条件不成立"; fi # 输出:两个条件都成立
||
:逻辑或,与-o
类似,通常用于(( ))
或命令之间。a=10; b=100; c=0 if (( $a > $b || $a > $c )); then echo "至少一个条件成立"; else echo "两个条件都不成立"; fi # 输出:至少一个条件成立
6.4 字符串运算符
字符串运算符用于处理和比较字符串,常用的有:
-n STRING
:检测字符串的长度是否不为零(即字符串非空),如果是则返回 true。str="hello" if [ -n $str ]; then echo "字符串非空"; else echo "字符串为空"; fi # 输出:字符串非空
-z STRING
:检测字符串的长度是否为零(即字符串为空),如果是则返回 true。str="" if [ -z $str ]; then echo "字符串为空"; else echo "字符串非空"; fi # 输出:字符串为空
=
:判断两个字符串是否相等,如果相等则返回 true。str1="hello"; str2="hello" if [ $str1 = $str2 ]; then echo "字符串相等"; else echo "字符串不相等"; fi # 输出:字符串相等
!=
:判断两个字符串是否不相等,如果不相等则返回 true。str1="hello"; str2="world" if [ $str1 != $str2 ]; then echo "字符串不相等"; else echo "字符串相等"; fi # 输出:字符串不相等
6.5 文件测试运算符
文件测试运算符用于检测文件的各种属性,常用的有:
-f 文件名
:检测文件是否存在且是一个普通文件(不是目录)。if [ -f ./test.sh ]; then echo "是普通文件"; else echo "不是普通文件或不存在"; fi
-d 目录名
:检测文件是否存在且是一个目录。if [ -d ./testdir ]; then echo "是目录"; else echo "不是目录或不存在"; fi
-s 文件名
:检测文件是否存在且不为空(文件大小大于 0)。if [ -s ./test.txt ]; then echo "文件存在且不为空"; else echo "文件不存在或为空"; fi
-e 文件名
:检测文件(包括普通文件、目录等)是否存在。if [ -e ./test ]; then echo "文件存在"; else echo "文件不存在"; fi
-r 文件名
:检测文件是否存在且当前用户具有读权限。if [ -r ./test.sh ]; then echo "有读权限"; else echo "没有读权限或文件不存在"; fi
-w 文件名
:检测文件是否存在且当前用户具有写权限。if [ -w ./test.sh ]; then echo "有写权限"; else echo "没有写权限或文件不存在"; fi
-x 文件名
:检测文件是否存在且当前用户具有执行权限。if [ -x ./test.sh ]; then echo "有执行权限"; else echo "没有执行权限或文件不存在"; fi
7. 总结
- shell是什么:命令解释器,连接用户与 Linux 内核,转译命令并反馈结果
- shell能做什么:自动化部署、批量操作(如加用户)、备份数据库、探测负载等,解决重复工作省时间
- 脚本构成:首行
#!/bin/bash
(指定解释器),#
开头是注释 - 脚本步骤:写命令→
chmod +x 脚本名
赋权限→检查→执行 - 脚本执行方式:
./脚本名
(需权限)、sh 脚本名
(无需权限)、source 脚本名
(无需权限) - 变量:
变量名=值
(无空格,字母 / 下划线开头),用$变量名
调用;可修改(普通)、设只读(不可改删)、unset
删普通变量;分全局(全环境用)和局部(当前用) - 字符串:单引号不识别变量,双引号识别;
${#串}
查长度,${串:索引}
提取,expr index
查找(单字符,从 1 算) - 参数:
./脚本 参1 参2
传递,$1
取参 1、$#
参个数、$*
整串输出、$@
列表输出 - 运算:算术用
+、-、\*、/、%
;逻辑用&&
(与)、||
(或)、!
(否)