#1 Shell(1)|Shell、Python、Lua|概述|调用|语法|变量
Shell(1)|Shell、Python、Lua|概述|调用|语法|变量
本文面向 Linux 自动化入门读者,介绍 Shell、Python 和 Lua 的定位与调用方式,并重点讲解 Bash 脚本的执行、基础语法和变量。示例默认在 Linux Bash 环境中运行。
文章目录
- Shell(1)|Shell、Python、Lua|概述|调用|语法|变量
- 一、什么是 Shell
- 二、Shell、Python 和 Lua 的定位
- 三、如何选择脚本语言
- 四、Shell 调用 Python 和 Lua
- 五、Python 和 Lua 调用 Shell 命令
- 六、第一个 Bash 脚本
- 七、Shell 脚本的执行方式
- 八、Shell 基础语法
- 九、注释与代码风格
- 十、变量的定义与读取
- 十一、引号与变量展开
- 十二、环境变量
- 十三、常见环境变量
- 十四、位置参数
- 十五、特殊变量
- 十六、变量默认值与必填检查
- 十七、只读变量与局部变量
- 十八、数组变量
- 十九、命令替换
- 二十、条件判断
- 二十一、循环
- 二十二、函数
- 二十三、一个综合脚本
- 二十四、严格模式的正确理解
- 二十五、语法与静态检查
- 二十六、常见错误
- 二十七、安全注意事项
- 二十八、实操检查清单
- 总结
一、什么是 Shell
Shell 是用户与操作系统交互的一层程序。它既可以读取用户输入并执行命令,也可以解释脚本文件中的流程控制、变量和函数。
用户或脚本
↓
Shell 命令解释器
↓
系统调用、程序和文件
↓
Linux 内核
常见 Shell 包括 Bash、Zsh、Dash 和 Fish。它们有相似的基本用途,但语法和扩展特性并不完全相同。
本文使用 Bash。编写脚本时应明确目标解释器,不要默认所有 /bin/sh 都等同于 Bash。
查看当前登录 Shell:
echo "$SHELL"
查看当前 Bash 版本:
bash --version
查看系统中可用的 Shell:
cat /etc/shells
二、Shell、Python 和 Lua 的定位
三者都能完成自动化任务,但各自擅长的场景不同。
| 对比项 | Shell | Python | Lua |
|---|---|---|---|
| 主要优势 | 组合系统命令 | 通用编程与丰富生态 | 轻量、易嵌入 |
| 常见用途 | 运维脚本、构建、部署 | 数据处理、工具、服务 | 游戏、配置、宿主程序扩展 |
| 文本处理 | 借助命令工具很方便 | 标准库和第三方库丰富 | 基础能力简洁 |
| 大型项目 | 维护成本较高 | 更适合 | 取决于宿主和项目结构 |
| 外部命令调用 | 天然擅长 | 需要进程接口 | 依赖标准库或宿主能力 |
2.1 Shell 适合什么
Shell 擅长编排现有命令,例如创建目录、查找文件、处理日志、启动程序、调用编译器和连接管道。
find . -name '*.log' -type f
如果任务主要是在多个系统命令之间传递文件和文本,Shell 往往最直接。
2.2 Python 适合什么
Python 适合复杂数据结构、网络请求、文件格式解析、业务逻辑和跨平台工具。
from pathlib import Path
for path in Path(".").glob("*.log"):
print(path)
当 Shell 脚本开始出现复杂嵌套、数据结构、异常处理和大量字符串拼接时,可以考虑改用 Python。
2.3 Lua 适合什么
Lua 语言小巧,常嵌入 Nginx、Redis、游戏引擎和其他宿主程序中,让用户在不重新编译宿主程序的情况下扩展逻辑。
local user = "alice"
print("hello, " .. user)
Lua 的价值往往不是单独替代 Shell 或 Python,而是作为嵌入式脚本语言运行在特定系统内部。
三、如何选择脚本语言
可以先根据任务重心判断:
- 主要组合 Linux 命令:优先考虑 Shell。
- 需要复杂数据处理或第三方库:优先考虑 Python。
- 需要嵌入已有程序或平台指定 Lua:使用 Lua。
- 需要长期维护的大型自动化工具:通常 Python 比 Shell 更清晰。
- 只需几十行部署或启动逻辑:Shell 通常已经足够。
语言选择不是性能排名。可维护性、运行环境、团队经验和依赖管理同样重要。
四、Shell 调用 Python 和 Lua
4.1 调用 Python 文件
创建 hello.py:
import sys
name = sys.argv[1] if len(sys.argv) > 1 else "guest"
print(f"hello from python, {name}")
在 Shell 中调用:
python3 hello.py alice
Shell 会启动一个 Python 进程,并把 hello.py 和 alice 作为参数传递给解释器。
4.2 调用 Lua 文件
创建 hello.lua:
local name = arg[1] or "guest"
print("hello from lua, " .. name)
在 Shell 中调用:
lua hello.lua alice
系统需要提前安装 Lua 解释器。不同发行版的软件包名称和默认版本可能不同。
4.3 获取子程序输出
Shell 可以使用命令替换保存程序输出:
python_message=$(python3 hello.py alice)
lua_message=$(lua hello.lua bob)
printf '%s\n' "$python_message"
printf '%s\n' "$lua_message"
命令替换会捕获标准输出,并移除末尾连续的换行符。大量输出不适合全部放进变量,可以改用文件或管道。
4.4 检查退出状态
程序使用退出状态告诉调用方是否成功。通常 0 表示成功,非 0 表示失败。
if python3 hello.py alice; then
echo 'python 程序执行成功'
else
echo 'python 程序执行失败' >&2
fi
Shell 中的条件判断主要依据退出状态,而不是命令输出的文字内容。
五、Python 和 Lua 调用 Shell 命令
5.1 Python 调用外部命令
Python 推荐使用 subprocess:
import subprocess
result = subprocess.run(
["uname", "-s"],
check=True,
text=True,
capture_output=True,
)
print(result.stdout.strip())
参数列表能够避免很多引号和注入问题。只有确实需要 Shell 语法时才使用 shell=True,并且不能把不可信输入直接拼进命令字符串。
5.2 Lua 调用外部命令
local success, exit_type, code = os.execute("uname -s")
print(success, exit_type, code)
os.execute 会通过系统环境执行命令,返回值细节可能与 Lua 版本有关。不可信输入不能直接拼接到命令中。
如果需要读取命令输出,可以使用:
local handle = io.popen("uname -s", "r")
local output = handle:read("*a")
handle:close()
print(output)
六、第一个 Bash 脚本
创建 hello.sh:
#!/usr/bin/env bash
name=${1:-guest}
printf 'hello, %s\n' "$name"
第一行称为 Shebang,用于告诉操作系统应使用哪个解释器执行文件。
添加执行权限并运行:
chmod +x hello.sh
./hello.sh alice
也可以直接调用解释器:
bash hello.sh alice
两种方式的区别在于:./hello.sh 使用 Shebang 指定的解释器,而 bash hello.sh 明确使用当前调用的 Bash。
七、Shell 脚本的执行方式
7.1 启动子 Shell
bash script.sh
脚本在新的 Bash 进程中运行。脚本内部修改普通变量或当前目录,不会直接改变父 Shell 的环境。
7.2 直接执行文件
./script.sh
文件需要执行权限,系统根据 Shebang 选择解释器。它同样通常运行在独立进程中。
7.3 在当前 Shell 中执行
source script.sh
简写形式:
. script.sh
source 不会启动独立脚本进程,而是在当前 Shell 环境中执行。因此,变量、函数和目录变化会保留在当前会话。
这也是为什么修改 Shell 配置后经常执行:
source ~/.bashrc
不要随意 source 不可信脚本,因为它可以直接修改当前环境并执行任意命令。
八、Shell 基础语法
8.1 命令与参数
cp source.txt backup.txt
cp 是命令,后面两个单词是参数。Shell 先进行变量展开、命令替换和通配符展开,再执行命令。
8.2 命令分隔
一行通常写一个命令:
mkdir -p output
cp report.txt output/
也可以用分号分隔,但可读性通常不如多行:
mkdir -p output; cp report.txt output/
8.3 条件执行
前一条命令成功才执行下一条:
mkdir -p build && cd build
前一条命令失败才执行下一条:
test -f config.ini || echo 'config.ini 不存在'
&& 和 || 判断的是命令退出状态。复杂流程使用 if 通常更清晰。
8.4 管道
管道把前一条命令的标准输出连接到后一条命令的标准输入:
printf '%s\n' apple banana apple | sort | uniq -c
管道适合流式处理文本。每个命令只负责一个步骤,组合后完成更复杂任务。
8.5 重定向
覆盖写入文件:
echo 'first line' > output.txt
追加写入文件:
echo 'second line' >> output.txt
把错误输出单独保存:
some_command 2> error.log
同时保存标准输出和错误输出:
some_command > all.log 2>&1
Bash 也支持简写:
some_command &> all.log
重定向顺序会影响结果。2>&1 表示让标准错误指向“此刻标准输出所指向的位置”。
九、注释与代码风格
单行注释使用 #:
# 创建输出目录
mkdir -p output
Shebang 虽然也以 # 开头,但它在文件第一行具有特殊含义。
Shell 没有统一的多行注释语法。可以连续使用多行 #,这比借用 Here Document 模拟注释更清晰。
建议:
- 使用统一缩进,例如 4 个空格。
- 变量名表达含义,不使用难懂缩写。
- 路径和变量展开通常加双引号。
- 脚本保持短小,复杂逻辑拆成函数或改用其他语言。
十、变量的定义与读取
10.1 定义变量
username='alice'
count=3
等号两侧不能有空格。下面不是变量赋值,而是尝试执行名为 username 的命令:
username = 'alice'
10.2 读取变量
echo "$username"
printf 'count = %s\n' "$count"
推荐使用 ${变量名} 明确边界:
filename='report'
echo "${filename}_2026.txt"
如果写成 $filename_2026,Shell 会把它理解为名为 filename_2026 的变量。
10.3 Shell 变量通常是字符串
Shell 不像 C++ 那样为普通变量声明整数、浮点数和布尔类型。内容通常以字符串形式保存,特定上下文会按数字解释。
left=10
right=20
sum=$((left + right))
echo "$sum"
$((...)) 是算术展开。Bash 的整数运算不提供普通浮点计算,涉及小数时可以使用 awk、bc 或其他语言。
十一、引号与变量展开
引号是 Shell 最容易出错的部分之一。
11.1 单引号
单引号中的内容通常按字面值处理:
name='alice'
echo '$name'
输出:
$name
11.2 双引号
双引号允许变量展开和命令替换,同时把展开结果保留为一个参数:
name='alice smith'
echo "hello, $name"
11.3 不加引号的风险
filename='my report.txt'
rm $filename
Shell 可能把展开结果拆成 my 和 report.txt 两个参数。正确写法是:
rm -- "$filename"
-- 表示后续内容按普通参数处理,可以避免以 - 开头的文件名被误认为选项。
一般原则是:除非明确需要单词拆分或通配符展开,否则变量展开应使用双引号。
十二、环境变量
Shell 变量默认只存在于当前 Shell 中。使用 export 后,后续启动的子进程可以继承它。
project_mode='development'
export project_mode
python3 -c 'import os; print(os.getenv("project_mode"))'
也可以一步完成:
export project_mode='development'
只为一条命令设置环境变量:
project_mode='test' python3 app.py
子进程不能直接修改父 Shell 的环境。子程序即使修改同名变量,父 Shell 中原值也不会因此改变。
十三、常见环境变量
printf 'home: %s\n' "$HOME"
printf 'user: %s\n' "$USER"
printf 'shell: %s\n' "$SHELL"
printf 'path: %s\n' "$PATH"
printf 'working directory: %s\n' "$PWD"
| 变量 | 常见含义 |
|---|---|
HOME | 当前用户主目录 |
USER | 当前用户名 |
SHELL | 登录 Shell 路径 |
PATH | 命令搜索路径列表 |
PWD | 当前工作目录 |
LANG | 语言和区域设置 |
修改 PATH 时通常保留原值:
export PATH="$HOME/bin:$PATH"
不要把当前目录 . 随意放到 PATH 最前面,这可能导致同名恶意程序覆盖系统命令。
十四、位置参数
脚本通过位置参数读取命令行输入:
#!/usr/bin/env bash
printf 'script: %s\n' "$0"
printf 'first: %s\n' "${1:-}"
printf 'second: %s\n' "${2:-}"
printf 'argument count: %s\n' "$#"
| 写法 | 含义 |
|---|---|
$0 | 脚本名称 |
$1 到 $9 | 对应位置参数 |
${10} | 第十个及以后参数需要花括号 |
$# | 参数数量 |
"$@" | 保持边界展开全部参数 |
"$*" | 合并为一个字符串展开 |
遍历所有参数:
for argument in "$@"; do
printf 'argument: %s\n' "$argument"
done
转发参数时通常使用 "$@",它能够保留每个参数原本的边界。
十五、特殊变量
15.1 上一条命令的退出状态
ls /path/that/does/not/exist
status=$?
echo "$status"
$? 只保存最近一条前台命令的退出状态,因此需要使用时应立即读取。
15.2 当前 Shell 的进程号
echo "$$"
$$ 常用于构造临时名称,但安全临时文件应优先使用 mktemp,避免名称预测和竞争问题。
15.3 最近后台任务的进程号
sleep 10 &
background_pid=$!
echo "$background_pid"
wait "$background_pid"
$! 保存最近启动的后台任务进程号,wait 等待它结束并返回其退出状态。
十六、变量默认值与必填检查
如果变量未设置或为空,使用默认值:
name=${1:-guest}
如果变量未设置或为空,直接报错退出:
: "${config_file:?必须设置 config_file}"
常见参数展开:
| 写法 | 作用 |
|---|---|
${value:-default} | 空或未设置时使用默认值 |
${value:=default} | 空或未设置时赋予默认值 |
${value:+alternate} | 已设置且非空时使用替代值 |
${value:?message} | 空或未设置时报错 |
这里的冒号表示同时把“未设置”和“空字符串”视为需要处理。如果去掉冒号,行为会有所不同。
十七、只读变量与局部变量
只读变量不能再次赋值或取消:
readonly application_name='demo'
函数中的局部变量使用 local:
greet() {
local name=${1:-guest}
printf 'hello, %s\n' "$name"
}
greet alice
如果函数中直接赋值而不使用 local,可能意外修改外层同名变量。
十八、数组变量
Bash 支持索引数组:
languages=(shell python lua)
echo "${languages[0]}"
echo "${languages[1]}"
echo "${languages[2]}"
遍历数组:
for language in "${languages[@]}"; do
printf '%s\n' "$language"
done
获取数组长度:
echo "${#languages[@]}"
Bash 还支持关联数组:
declare -A ports=(
[http]=80
[https]=443
[ssh]=22
)
echo "${ports[https]}"
数组是 Bash 扩展,不应假设所有 POSIX sh 都支持。
十九、命令替换
现代写法使用 $(...):
current_date=$(date '+%Y-%m-%d')
echo "$current_date"
命令替换可以嵌套,可读性也比旧式反引号更好:
file_count=$(find . -type f | wc -l)
printf 'file count: %s\n' "$file_count"
如果文件名可能包含换行等特殊字符,不能只依赖面向行的文本处理,需要使用支持空字符分隔的接口或其他语言。
二十、条件判断
20.1 if 语句
if [[ -f config.ini ]]; then
echo '配置文件存在'
else
echo '配置文件不存在'
fi
[[ ... ]] 是 Bash 条件表达式,功能比传统 [ ... ] 更完整。若脚本要求严格兼容 POSIX sh,应使用标准语法并避免 Bash 扩展。
20.2 字符串判断
if [[ -n $username ]]; then
echo '用户名非空'
fi
if [[ $environment == 'production' ]]; then
echo '生产环境'
fi
20.3 数值判断
if (( count > 10 )); then
echo 'count 大于 10'
fi
(( ... )) 是 Bash 算术上下文,更适合整数比较与计算。
二十一、循环
21.1 for 循环
for language in shell python lua; do
printf 'learning %s\n' "$language"
done
21.2 while 循环
count=1
while (( count <= 3 )); do
echo "$count"
((count += 1))
done
读取文件时避免反斜杠被解释,并保留最后一行没有换行符的情况:
while IFS= read -r line || [[ -n $line ]]; do
printf '%s\n' "$line"
done < input.txt
二十二、函数
add() {
local left=$1
local right=$2
printf '%s\n' "$((left + right))"
}
result=$(add 10 20)
echo "$result"
Shell 函数只能通过退出状态返回 0 到 255 范围的状态码。需要返回数据时,通常写到标准输出,由调用方通过命令替换捕获。
函数中的普通 return 用于返回状态:
is_file() {
[[ -f $1 ]]
}
if is_file config.ini; then
echo '文件存在'
fi
二十三、一个综合脚本
下面的脚本根据文件扩展名调用 Python 或 Lua,也可以直接执行 Shell 脚本。
创建 script_runner.sh:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
usage() {
printf 'usage: %s <script> [args...]\n' "$0" >&2
}
if (( $# == 0 )); then
usage
exit 64
fi
script_path=$1
shift
if [[ ! -f $script_path ]]; then
printf 'script not found: %s\n' "$script_path" >&2
exit 66
fi
case $script_path in
*.sh)
bash "$script_path" "$@"
;;
*.py)
python3 "$script_path" "$@"
;;
*.lua)
lua "$script_path" "$@"
;;
*)
printf 'unsupported script type: %s\n' "$script_path" >&2
exit 65
;;
esac
运行:
chmod +x script_runner.sh
./script_runner.sh hello.py alice
./script_runner.sh hello.lua bob
./script_runner.sh hello.sh carol
脚本使用 shift 移除第一个参数,剩余参数通过 "$@" 原样传递给目标程序。
二十四、严格模式的正确理解
很多脚本会使用:
set -o errexit
set -o nounset
set -o pipefail
errexit在部分未处理的失败命令后退出。nounset读取未设置变量时报错。pipefail让管道中任意命令失败都可能导致管道失败。
它们能发现许多错误,但不是自动安全保证。errexit 在条件语句、管道和命令替换等上下文中有复杂规则。重要命令仍应进行明确错误处理。
二十五、语法与静态检查
只检查 Bash 语法,不执行脚本:
bash -n script_runner.sh
如果系统安装了 ShellCheck,可以进行更深入检查:
shellcheck script_runner.sh
ShellCheck 能发现未加引号的变量、可疑重定向和不可移植语法等问题,但检查结果仍需结合脚本目标判断。
二十六、常见错误
26.1 赋值时加入空格
错误:
name = alice
正确:
name='alice'
26.2 变量展开没有双引号
错误:
cp $source $destination
推荐:
cp -- "$source" "$destination"
26.3 使用 sh 执行 Bash 脚本
如果脚本使用数组、[[ ... ]] 或 source 等 Bash 特性,就不应通过以下方式执行:
sh script.sh
应使用:
bash script.sh
或者添加 Bash Shebang 后直接执行。
26.4 把不可信内容拼入命令
eval "rm -rf $user_input"
这种写法可能执行用户注入的额外命令。应避免 eval,使用数组保存命令和参数,并对输入进行明确验证。
26.5 用解析 ls 输出处理文件
文件名可以包含空格、制表符和换行。复杂文件遍历应使用 find ... -print0、Bash 通配符数组或 Python 的文件接口,而不是依赖 ls 的显示文本。
二十七、安全注意事项
- 不使用
eval执行不可信输入。 - 临时文件使用
mktemp创建。 - 删除文件前检查目标路径,避免空变量和错误通配符。
- 密码、令牌和密钥不要直接写入脚本仓库。
- 下载内容不要未经验证就通过管道交给 Shell 执行。
- 使用最小权限运行脚本,不无目的使用
sudo。 - 对路径变量使用双引号,并在适合时使用
--结束选项。
二十八、实操检查清单
- 查看当前登录 Shell 和 Bash 版本。
- 编写并执行第一个带 Shebang 的脚本。
- 分别使用 Bash 调用 Python 和 Lua 文件。
- 比较直接执行、
bash script.sh和source script.sh。 - 练习普通变量、环境变量和位置参数。
- 对比单引号、双引号和不加引号的行为。
- 使用数组保存包含空格的多个参数。
- 编写函数并通过退出状态表示成功或失败。
- 完成综合脚本运行器。
- 使用
bash -n和 ShellCheck 检查脚本。
总结
Shell 的优势是连接操作系统命令和程序,Python 更适合复杂通用逻辑,Lua 则常作为轻量嵌入式语言。三者可以通过进程、参数、环境变量、标准输入输出和文件相互协作。
Bash 变量看似简单,但引号、展开、环境继承和参数边界决定了脚本是否正确可靠。编写 Shell 脚本时,应默认给变量展开加双引号,检查命令退出状态,并在逻辑复杂度增长时及时拆分函数或改用更适合的语言。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)