Shell详解&Shell编程
今天看了火哥的嵌入式Linux教程,收获很多,对Shell有了新的认识,做笔记!
一、Shell的核心概念
当你打开黑窗口,在/下输入rm -rf *(别真试啊)的时候,那么恭喜你,你已经在使用Shell了。
so,什么是 Shell呢?
1.Shell 的本质是命令行解释器
- 理解
Shell之前我们可以先了解两个概念: 解释型语言 和 编译型语言-
编译型语言:写好的代码需要先通过 “编译器” 翻译成操作系统能直接理解的机器码(二进制文件),之后才能运行。比如
C、C++就是常见的编程型语言,当键入int a = 10但是忘记加分号,或者更加夸张的把入口函数打成了int mian时,无法通过编译更别谈运行了。虽然要求严格,但是却有更好的执行效率。 -
解释型语言:写好的代码不需要提前编译,而是由 “解释器”逐行读取、逐行解析并执行。比如我们常见的
python和Shell脚本就是解释型语言,在写代码过程中即使后面错误,但是前面正确的部分仍然可以执行。这类语言相对来说更加简单便捷,但执行效率不如上者。
-
Shell就是 执行一条条命令的解释器,那么Shell脚本就是执行一堆命令咯。
2.Shell 有两种工作模式
- 交互式模式:典型的你问我答模式,你给一条命令,我就执行一条,日常终端就是这种模式
- 批处理模式:用户写入一系列的命令,解释器一次性执行,可以实现一些自动化任务。
3.Shell解释器有哪些?
其实Shell解释器有非常多的种类,比如最常见的,Windows下的 PowerShell 还有 Linux 最常见的bash,如果可以,键入
cat /etc/shells

这些都是解释器咯。并且通过echo $SHELL就可以查看你当前所用的解释器了。

其实,本质上不论哪种Shell,可以理解为一个解释器程序,而命令就是你传给它的参数,它会帮你执行这个命令。
4.内置命令和外部命令
-
内置命令:内置命令是
Shell解释器自身包含的命令,不需要依赖外部文件,执行时由 Shell 直接处理,不创建新的进程。例如cd、echo、export等等。可以 -
外部命令:外部命令是独立于
Shell的可执行程序,以文件形式存储在磁盘中(通常位于/bin、/usr/bin、/usr/local/bin等目录),执行时需要 Shell 创建新进程来运行。例如ls、cp、grep、python等。 -
通过
type可以区分内置命令和外部命令。

-
当一个命令为外部命令时,
Shell会到对应的路径去查找对应的程序,找到了才执行,此处可以通过echo $PATH来查看Shell能查找的目录
同时也可以通过添加路径的方式让Shell发现你。
#例如我编写了一个hello.c,用于打印Hello,world!
#编译后的可执行文件在/home/lsy/workspace下
gcc hello.c -o hello
#直接执行hello
hello #报错:command not found
#添加路径
export PATH=$PATH:/home/lsy/workspace
#打印查看
echo $PATH
#直接执行hello
hello

二、Shell 编程
1.第一个Shell脚本
- 学习一门语言,我想没有比输出
Hello,world!更让人兴奋的事情了,创建一个hello.sh(Shell脚本后缀为.sh)
#!/bin/bash
#首行,告诉系统使用bash解释器
echo "Hello,world!"
接下来,执行它!
#改变文件权限,可执行
chmod 777 hello.sh
#执行
./hello.sh

2.Shell变量
-
定义变量,命名规则:
- 只能包含 字母、数字、下划线(a-z、A-Z、0-9、_)
- 不能以 数字开头(如 1name 是错误的,name1 正确)。
- 区分大小写(Name 和 name 是两个不同的变量)。
- 避免使用 Shell 关键字(如 if、for 等)。
-
变量赋值
- 基本赋值
#变量名=值,等号左右无空格,值如果包含空格要用引号包裹 name=Jack message='Hello World' text="go to bed" age=18- 变量的引用:使用变量时,需在变量名前加
$符号,或用${变量名} 明确边界(避免歧义)。
echo $name
echo ${name}
echo "My name is $name"
echo 'My name i $name' #单引号打印原字符

- 将命令的结果赋值给变量
# var=`command`
# var=$(command)
var1='pwd'
var2=$(ls)
echo $var1
echo $var2
运行结果:

- 变量分类
- 局部变量:作用范围当前
Shell会话,不影响子Shell或者其他进程。例如,在当前Shell,我定义一个变量a并echo a
在这里插入图片描述
当我输入bash打开子Shell时,无法得到10

exit退出 - 环境变量:作用范围全局有效,可被当前 Shell 及其启动的所有子 Shell(如脚本中调用的其他命令 / 脚本)继承。可以用
export命令将局部变量升级为环境变量

此时进入子Shell:

- 特殊变量
- 局部变量:作用范围当前
| 变量符号 | 具体含义 |
|---|---|
| $0 | 当前脚本的文件名(若执行路径为 ./test.sh,则 $0 输出 ./test.sh) |
| $n(n≥1) | 传递给脚本/函数的第 n 个参数(如 $1 是第1个参数,$3 是第3个参数) |
| $# | 传递给脚本/函数的 总参数个数(若传3个参数,$# 输出 3) |
| $* | 所有参数的集合(不保留参数原格式,将所有参数合并为一个字符串) |
| $@ | 所有参数的集合(保留参数原格式,每个参数独立;双引号包裹时与 $* 差异明显) |
| $? | 上一个命令的 退出状态码(0 表示执行成功,非0表示执行失败) |
| $$ | 当前 Shell 进程的 PID(脚本运行时,即脚本所在进程的 ID) |
我们看一个例子:
#!/bin/bash
echo "===== 特殊参数演示 ====="
echo "1. 脚本文件名(\$0):$0"
echo "2. 第1个参数(\$1):$1"
echo "3. 第2个参数(\$2):$2"
echo "4. 第3个参数(\$3):$3"
echo "5. 参数总个数(\$#):$#"
echo "6. 所有参数(\$*):$*"
echo "7. 所有参数(\$@):$@"
echo "8. 上一条命令退出状态(\$?):$?" # 这里打印的是“echo $?”本身的退出状态(成功为0)
echo "9. 当前进程ID(\$\$):$$"
运行结果

$@、$*差异后续描述。
- 变量拼接
name="Jack is a"
#注意,变量名并不会被自动识别,用空格隔开或者加{}
info="$nameboy"#错误
info="${name}boy"
echo $info
info="$name boy"
echo $info

- 变量运算
方式 1:$(( 表达式 )):支持+(加)、-(减)、*(乘)、/(除,整数除法)、%(取余),表达式内变量可省略$。
a=10
b=3
# 加法
echo $((a + b))
# 减法
echo $((a - b))
# 乘法(无需转义*)
echo $((a * b))
# 除法(向下取整)
echo $((a / b))
# 取余
echo $((a % b))
# 表达式内可直接写数值
echo $((5 + 6 * 2))
结果:

方式 2:expr 命令
a=10
b=3
echo $(expr $a + $b) # 输出:13(+两边必须有空格)
echo $(expr $a \* $b) # 输出:30(*必须转义)
echo $(expr $a / $b) # 输出:3
结果:

方式 3:let 命令
a=10
b=3
let c=a+b
echo $c
let a+=2
echo $a
结果:

3.常见命令
| 命令 | 作用说明 | 核心语法/常用选项 | 示例 |
|---|---|---|---|
echo | 输出文本、变量或命令结果 | echo [选项] 内容- -n:输出后不换行;- -e:解析转义字符(如\n换行、\t制表符) | echo "Hello $name"(输出变量)echo -e "Line1\nLine2"(换行输出) |
exit | 退出当前Shell/脚本,返回状态码 | exit [状态码](0表示成功,非0表示错误,范围0-255) | exit 0(正常退出)exit 1(异常退出,标识错误) |
cd | 切换当前工作目录 | cd [目录路径]特殊路径: ~(家目录)、..(上级目录)、-(上一次目录) | cd /home(切换到绝对路径)cd ..(切换到上级目录) |
pwd | 显示当前工作目录的绝对路径 | pwd | pwd(输出:/home/user/projects) |
read | 读取用户输入并存储到变量 | read [选项] 变量名- -p:显示提示文本;- -s:隐藏输入(如密码);- -n 数字:限制输入字符数 | read -p "输入姓名:" name(带提示读取)read -s -p "密码:" pwd(隐藏输入) |
export | 将局部变量升级为环境变量(子进程可继承) | export 变量名=值(直接定义并导出)export 变量名(先定义局部变量再导出) | export PATH=$PATH:~/bin(扩展命令路径)age=20; export age |
unset | 删除已定义的变量(局部或环境变量) | unset 变量名 | name="Tom"; unset name(删除name变量) |
type | 判断命令类型(内置/外部/别名) | type 命令名 | type cd(输出:cd is a shell builtin)type ls(输出路径) |
test/[ ] | 条件判断(文件/数值/字符串状态) | test 条件 或 [ 条件 ]([ ]前后必须有空格)常见条件: - 文件: -f 路径(是否为普通文件)、-d 路径(是否为目录);- 数值: a -eq b(a等于b)、a -gt b(a大于b);- 字符串: "str1" == "str2"(相等)、-z "str"(是否为空) | [ -f "file.txt" ](判断文件是否存在)[ $a -gt $b ](判断a是否大于b) |
4.逻辑与和或
- 与和或解析
command1 && command2
#对于逻辑与:
#如果command1不成立 ,那么command2不会执行
#如果command1成立,那么command2会执行
command1 || command2
#对于逻辑或
#如果command1成立,那么command2就不会执行
#如果command1不成立,那么command2会执行
- 条件判断的两种方式
#使用test
test condition
#使用[]但是前后必须有空格
[ condition ]
#示例
a=1
b=2
#方式一
test $a -eq $b
#方式二
[ $a -eq $b ]
- Shell 条件判断常用选项表
一、文件测试(检查文件/目录状态)
| 选项 | 含义 | 示例([ 选项 路径 ]) |
|---|---|---|
-e | 路径是否存在(文件/目录均可) | [ -e "/home/file.txt" ] |
-f | 路径是否为普通文件(非目录/链接) | [ -f "data.log" ] |
-d | 路径是否为目录 | [ -d "/var/log" ] |
-r | 路径是否可读(当前用户权限) | [ -r "config.ini" ] |
-w | 路径是否可写(当前用户权限) | [ -w "output.txt" ] |
-x | 路径是否可执行(当前用户权限) | [ -x "./script.sh" ] |
-s | 文件是否非空(大小 > 0) | [ -s "result.txt" ] |
-L | 路径是否为符号链接 | [ -L "link_to_file" ] |
-nt | 文件A是否比文件B新(修改时间) | [ "fileA.txt" -nt "fileB.txt" ] |
-ot | 文件A是否比文件B旧(修改时间) | [ "fileA.txt" -ot "fileB.txt" ] |
二、数值比较(仅用于整数,变量需为数值)
| 选项 | 含义 | 示例([ 数值1 选项 数值2 ]) |
|---|---|---|
-eq | 等于(equal) | [ $a -eq $b ](判断变量a是否等于b) |
-ne | 不等于(not equal) | [ $count -ne 0 ](判断count是否不等于0) |
-gt | 大于(greater than) | [ $age -gt 18 ](判断age是否大于18) |
-lt | 小于(less than) | [ $num -lt 100 ](判断num是否小于100) |
-ge | 大于等于(>=) | [ $score -ge 60 ](判断score是否≥60) |
-le | 小于等于(<=) | [ $size -le 1024 ](判断size是否≤1024) |
三、字符串比较(变量或文本)
| 选项/符号 | 含义 | 示例([ 字符串1 选项 字符串2 ]) |
|---|---|---|
==/= | 字符串相等(==是=的扩展,推荐==) | [ "$name" == "Alice" ] |
!= | 字符串不相等 | [ "$status" != "success" ] |
-n | 字符串长度非空(not zero) | [ -n "$input" ](判断input是否有内容) |
-z | 字符串长度为空(zero) | [ -z "$empty_var" ](判断变量是否为空) |
> | 字符串按字典序大于(需转义\>) | [ "apple" \> "banana" ](结果为假) |
< | 字符串按字典序小于(需转义\<) | [ "apple" \< "banana" ](结果为真) |
5.管道操作
|表示管道,它的作用是将左侧命令的****标准输出(stdout)直接作为右侧命令的****标准输入(stdin),实现 “数据流水线” 式的处理
#查看当前路径下所有.sh文件
ls | grep ".sh"
#统计hello.c行数
cat hello.c | wc -l
#找到当前的python进程
ps aux | grep "python"
看看结果:

- 管道操作可以连用,但是尽量避免过度串联
6.if-else
- 模板
if condition1
then
statement1
elif condition2
then
statement2
else
statement3
fi
- 二选一
file="data.txt"
if [ -f "$file" ]; then # -f判断是否为普通文件
echo "$file 存在"
else
echo "$file 不存在"
fi
运行结果:

- 多选一
read -p "请输入score:" score
if [ $score -ge 90 ]; then # -ge:大于等于
echo "优秀"
elif [ $score -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
运行结果:

7.case in 语句
- 基本模板
case 变量名 in
模式1)
# 变量匹配模式1时执行的命令
命令1
命令2
;; # 分支结束符(两个分号)
模式2)
# 变量匹配模式2时执行的命令
命令3
;;
模式3|模式4) # 多个模式用|分隔(逻辑或)
# 变量匹配模式3或模式4时执行的命令
命令4
;;
*) # 通配符*表示“所有未匹配的情况”(默认分支)
# 未匹配任何模式时执行的命令
命令5
;;
esac # case的反写,标识整个语句结束
- 举个栗子
#!/bin/bash
read -p "请输入一个字符:" char
case $char in
[a-z]) # 匹配小写字母
echo "你输入了小写字母:$char"
;;
[A-Z]) # 匹配大写字母
echo "你输入了大写字母:$char"
;;
[0-9]) # 匹配数字
echo "你输入了数字:$char"
;;
*) # 其他字符(如符号、多字符)
echo "你输入了特殊字符或字符串:$char"
;;
esac
结果:

- case vs if-elif-else
- 用case更合适的场景:
1.变量的可能取值是固定的离散值(如选项 1/2/3、状态码 0/1/2)
2.需要模式匹配(如文件名后缀、字符串前缀、字符范围)
3.分支数量较多(超过 3 个时,case比if-elif-else更易读) - 用if-elif-else更合适的场景:
需要数值比较(如> < >=)或复杂条件组合(如a>10 && b<20)。
- 用case更合适的场景:
8. for in 循环
在 Shell 脚本中,for in循环是遍历列表元素的常用结构,核心功能是 “依次取出列表中的每个值,赋值给变量,然后执行循环体命令”。
- 基本模板
for 变量名 in 元素列表; do
# 循环体:对当前变量执行的命令
命令1
命令2
done
- 举例一
# 遍历水果列表
for fruit in apple banana "cherry tomato" date; do
echo "当前水果:$fruit"
done
# 遍历数字1到5,方式一
for num in 1 2 3 4 5; do
echo $num
done
# 遍历数字1到5,方式二
for num in {1..5}; do # 用{1..5}表示1到5的连续数字,添
echo $num
done
运行结果

- 使用通配符
# 遍历当前目录的.sh文件,仅打印文件名
for sh_file in *.sh; do
# 只处理真实存在的普通文件(排除无.sh文件时的"*.sh"字符串)
[ -f "$sh_file" ] && echo "$sh_file"
done
结果:

$*和$@区别
| 变量 | 不加双引号时 | 加双引号时 |
|---|---|---|
$* | 将所有参数以空格分隔合并为一个列表 | 所有参数被合并成单个字符串(用 IFS 变量的第一个字符分隔,默认是空格) |
$@ | 与 $* 行为一致(按空格分割为列表) | 保留每个参数的独立性,每个参数作为单独的元素(即使参数包含空格,也会被当作一个整体) |
让我们打印出来看看:
#!/bin/bash
echo "===== 用 \$* 遍历(不加双引号) ====="
for arg in $*; do
echo "参数:$arg"
done
echo -e "\n===== 用 \$@ 遍历(不加双引号) ====="
for arg in $@; do
echo "参数:$arg"
done
echo -e "\n===== 用 \"\$*\" 遍历(加双引号) ====="
for arg in "$*"; do
echo "参数:$arg"
done
echo -e "\n===== 用 \"\$@\" 遍历(加双引号) ====="
for arg in "$@"; do
echo "参数:$arg"
done
运行结果:

9.While循环
while循环是基于条件判断的循环结构—— 只要指定的条件为 “真”(命令退出状态码为 0),就会重复执行循环体中的命令。它适合处理 “需要满足某个条件才停止” 的场景(如等待用户输入正确值、读取文件直到末尾、无限循环等),比for in循环更灵活。
- 基本模板
while 条件判断; do
# 循环体:条件为真时执行的命令
命令1
命令2
# (可选)修改条件相关的变量,避免无限循环
done
- 简单示例
input="" # 初始化输入变量
while [ "$input" != "yes" ]; do # 条件:输入不是"yes"时循环
read -p "请输入yes继续:" input
done
结果示例;

while和for in
| 循环类型 | 适用场景 | 控制方式 |
|---|---|---|
while | 基于条件判断的循环(如 “满足 X 则停止”) | 依赖条件表达式的真假,需手动更新条件变量 |
for in | 遍历已知列表(如固定值、文件列表、参数) | 自动遍历列表元素,无需手动控制次数 |
10.函数
在 Shell 脚本中,函数用于将一组命令封装起来,实现代码复用和模块化。它可以接收参数、执行操作并返回结果,让脚本结构更清晰,尤其适合复杂脚本中重复逻辑的处理。
- 定义方式,两种等价
# 方式1:function关键字 + 函数名 + { 命令 }
function 函数名 {
# 函数体:要执行的命令
命令1
命令2
}
# 方式2:函数名() + { 命令 }
函数名() {
# 函数体
命令1
命令2
}
- 调用方式,无需括号,和其他语言不同
# 定义函数
say_hello() {
echo "Hello, world!"
}
# 调用函数
say_hello
- 函数参数
Shell 函数通过位置参数接收外部传入的值,与脚本的位置参数规则一致。($1 $2 ....)
例如:
#!/bin/bash
# 定义函数:计算两个数的和
add() {
# 接收参数($1是第一个数,$2是第二个数)
local a=$1 # local声明局部变量(仅函数内有效)
local b=$2
echo "$((a + b))"
}
# 调用函数并传递参数
result=$(add 3 5) # 用$(...)捕获函数输出的结果
echo "3 + 5 = $result"
结果:

-
函数返回值,返回值有两种
- 退出状态码:用return 数字返回退出状态码(0-255,0 表示成功,非 0 表示错误),类似exit但不终止脚本;
外部通过$?(上一条命令的退出状态)获取。 - 计算结果:若需返回具体数值(如计算结果),需通过echo输出,外部用$(函数名)捕获。
- 注意:return只能返回 0-255 的整数,无法返回字符串或大数值,因此复杂结果必须用echo输出。
- 退出状态码:用return 数字返回退出状态码(0-255,0 表示成功,非 0 表示错误),类似exit但不终止脚本;
-
局部变量与全局变量
- 全局变量:在函数外定义的变量,默认在函数内也可访问和修改;
- 局部变量:在函数内用local声明的变量,仅在函数内部有效,不影响外部变量(推荐优先使用,避免污染全局)。
三、总结
其实Shell并不复杂,如果学习过C语言,那么这个语法应该很熟悉了,如果学习过python应该也能找到相似之处。
17万+

被折叠的 条评论
为什么被折叠?



