Shell(1)|Shell、Python、Lua|概述|调用|语法|变量

本文面向 Linux 自动化入门读者,介绍 Shell、Python 和 Lua 的定位与调用方式,并重点讲解 Bash 脚本的执行、基础语法和变量。示例默认在 Linux Bash 环境中运行。

文章目录

一、什么是 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 的定位

三者都能完成自动化任务,但各自擅长的场景不同。

对比项ShellPythonLua
主要优势组合系统命令通用编程与丰富生态轻量、易嵌入
常见用途运维脚本、构建、部署数据处理、工具、服务游戏、配置、宿主程序扩展
文本处理借助命令工具很方便标准库和第三方库丰富基础能力简洁
大型项目维护成本较高更适合取决于宿主和项目结构
外部命令调用天然擅长需要进程接口依赖标准库或宿主能力

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。
  • 对路径变量使用双引号,并在适合时使用 -- 结束选项。

二十八、实操检查清单

  1. 查看当前登录 Shell 和 Bash 版本。
  2. 编写并执行第一个带 Shebang 的脚本。
  3. 分别使用 Bash 调用 Python 和 Lua 文件。
  4. 比较直接执行、bash script.sh 和 source script.sh。
  5. 练习普通变量、环境变量和位置参数。
  6. 对比单引号、双引号和不加引号的行为。
  7. 使用数组保存包含空格的多个参数。
  8. 编写函数并通过退出状态表示成功或失败。
  9. 完成综合脚本运行器。
  10. 使用 bash -n 和 ShellCheck 检查脚本。

总结

Shell 的优势是连接操作系统命令和程序,Python 更适合复杂通用逻辑,Lua 则常作为轻量嵌入式语言。三者可以通过进程、参数、环境变量、标准输入输出和文件相互协作。

Bash 变量看似简单,但引号、展开、环境继承和参数边界决定了脚本是否正确可靠。编写 Shell 脚本时,应默认给变量展开加双引号,检查命令退出状态,并在逻辑复杂度增长时及时拆分函数或改用更适合的语言。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐