3. 安装脚本基础:Shell脚本入门、Python脚本入门、参数传递与解析、错误处理机制
说实话,搞自动化安装这么多年,我最大的体会就是:脚本写得好,下班回家早。脚本是自动化的灵魂,不管你用 Ansible 还是 SaltStack,底层跑的还是脚本。这一章,咱们就把脚本基础打扎实。
3.1 Shell脚本入门:别把它想得太复杂
Shell 脚本,说白了就是把一堆命令按顺序写进文件里。我刚开始做运维时,总觉得 Shell 就是「复制粘贴」的升级版。后来才发现,它其实是 Linux 管理员的瑞士军刀。
3.1.1 第一个脚本:Hello World
#!/bin/bash
# 我的第一个安装脚本
echo "Hello, 安装世界!"
echo "当前时间: $(date)"
嗯,这里要注意:#!/bin/bash 这行叫 shebang,它告诉系统用哪个解释器来跑这个脚本。我见过有人写 #!/bin/sh,结果在 Ubuntu 上 sh 指向的是 dash,语法不兼容,脚本直接挂了。所以,我建议统一用 #!/bin/bash。
3.1.2 变量与字符串处理
Shell 的变量用起来很随意,但坑也不少。你看:
#!/bin/bash
# 变量赋值,等号两边不能有空格!
INSTALL_DIR="/opt/myapp"
VERSION="1.0.0"
# 使用变量,加花括号是好习惯
echo "安装目录: ${INSTALL_DIR}"
echo "版本号: ${VERSION}"
# 字符串拼接
FULL_PATH="${INSTALL_DIR}/app-${VERSION}"
echo "完整路径: ${FULL_PATH}"
我曾经在项目里犯过一个低级错误:INSTALL_DIR = "/opt/myapp"(等号两边加了空格),结果 Shell 把 INSTALL_DIR 当成命令执行了,报错半天没反应过来。避坑指南:等号两边绝对不能有空格。
3.1.3 条件判断与循环
安装脚本里最常用的就是判断文件是否存在、目录是否可写。我一般这么写:
#!/bin/bash
# 检查安装目录是否存在
if [ -d "${INSTALL_DIR}" ]; then
echo "目录已存在,跳过创建"
else
echo "创建目录: ${INSTALL_DIR}"
mkdir -p "${INSTALL_DIR}"
fi
# 遍历要安装的文件列表
for file in /tmp/packages/*.rpm; do
echo "正在安装: ${file}"
rpm -ivh "${file}"
done
这里有个小技巧:[ -d ] 这种写法,中括号前后都要有空格。我见过新手写 [-d],结果语法错误。你想想看,Shell 就是这么「矫情」。
3.2 Python脚本入门:更强大的选择
当安装逻辑变得复杂时,Shell 就有点力不从心了。比如你要解析 JSON 配置文件、处理复杂的字符串、或者调用 REST API,这时候 Python 就派上用场了。
3.2.1 为什么选择 Python?
我个人习惯:简单的文件操作用 Shell,复杂的业务逻辑用 Python。Python 的优势在于:
- 跨平台:Windows/Linux/macOS 都能跑
- 丰富的标准库:os、sys、shutil、subprocess 都是安装脚本的好帮手
- 异常处理:比 Shell 的
$?优雅得多
3.2.2 第一个 Python 安装脚本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import shutil
def main():
"""主安装函数"""
install_dir = "/opt/myapp"
# 检查 Python 版本
if sys.version_info < (3, 6):
print("错误:需要 Python 3.6 或更高版本")
sys.exit(1)
# 创建目录
os.makedirs(install_dir, exist_ok=True)
print(f"安装目录已创建: {install_dir}")
if __name__ == "__main__":
main()
注意看 #!/usr/bin/env python3 这行。为什么不直接写 #!/usr/bin/python3?因为不同系统上 Python 的安装路径不一样,用 env 可以自动在 PATH 里找。我在 CentOS 和 Ubuntu 之间切换时,这个写法从来没出过问题。
3.2.3 文件操作实战
安装脚本免不了要复制文件、修改配置。Python 的 shutil 模块是神器:
import shutil
import os
# 复制文件
shutil.copy2("source.conf", "/etc/myapp/config.conf")
# 复制整个目录
shutil.copytree("plugins", "/opt/myapp/plugins", dirs_exist_ok=True)
# 删除目录
shutil.rmtree("/tmp/install_temp", ignore_errors=True)
这里 dirs_exist_ok=True 是 Python 3.8 才有的参数。如果你还在用老版本,就得先判断目录是否存在。嗯,版本兼容性永远是脚本编写的大敌。
3.3 参数传递与解析:让脚本更灵活
写死的脚本没人用。一个好的安装脚本,应该能接受各种参数:安装路径、版本号、是否覆盖等等。
3.3.1 Shell 中的参数处理
Shell 里用 $1、$2 来取参数,但这样太原始了。我推荐用 getopts:
#!/bin/bash
# 使用 getopts 解析参数
usage() {
echo "用法: $0 -d <安装目录> -v <版本号> [-f]"
exit 1
}
while getopts "d:v:f" opt; do
case $opt in
d)
INSTALL_DIR="$OPTARG"
;;
v)
VERSION="$OPTARG"
;;
f)
FORCE=true
;;
*)
usage
;;
esac
done
echo "安装目录: ${INSTALL_DIR}"
echo "版本号: ${VERSION}"
echo "强制安装: ${FORCE:-false}"
我曾经写过一个脚本,参数解析用了 50 行 case 语句,后来发现 getopts 10 行就搞定了。避坑指南:能用工具就别手写。
3.3.2 Python 中的参数解析
Python 的 argparse 模块是标准库里的明星。我几乎所有的安装脚本都用它:
#!/usr/bin/env python3
import argparse
def parse_args():
parser = argparse.ArgumentParser(
description="我的安装脚本",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"-d", "--install-dir",
default="/opt/myapp",
help="安装目录 (默认: /opt/myapp)"
)
parser.add_argument(
"-v", "--version",
required=True,
help="版本号 (必填)"
)
parser.add_argument(
"-f", "--force",
action="store_true",
help="强制安装,覆盖已有文件"
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
print(f"安装目录: {args.install_dir}")
print(f"版本号: {args.version}")
print(f"强制模式: {args.force}")
你看,argparse 自动帮你生成了 -h 帮助信息,还会校验必填参数。说白了,Python 就是比 Shell 省心。
3.4 错误处理机制:别让脚本静默失败
这是最容易被忽视的部分。我见过太多脚本,执行到一半失败了,但没有任何提示,用户还以为安装成功了。结果生产环境一跑,各种问题。
3.4.1 Shell 中的错误处理
Shell 里错误处理的核心是 $?,它保存上一条命令的退出码。0 表示成功,非 0 表示失败。
#!/bin/bash
# 开启错误检测
set -e # 任何命令失败就退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中任何命令失败都算失败
# 手动检查错误
if ! mkdir -p "${INSTALL_DIR}"; then
echo "错误:无法创建目录 ${INSTALL_DIR}" >&2
exit 1
fi
# 或者用 || 处理
cp source.conf "${INSTALL_DIR}/" || {
echo "错误:文件复制失败" >&2
exit 1
}
这里 >&2 是把错误信息输出到 stderr。我刚开始写脚本时总忽略这个,结果错误信息和正常输出混在一起,排查问题特别痛苦。记住:正常输出走 stdout,错误信息走 stderr。
3.4.2 Python 中的异常处理
Python 的 try-except 比 Shell 的 $? 优雅太多了:
import os
import sys
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def install_package(pkg_path):
"""安装单个包,带错误处理"""
try:
if not os.path.exists(pkg_path):
raise FileNotFoundError(f"包文件不存在: {pkg_path}")
# 模拟安装过程
logging.info(f"开始安装: {pkg_path}")
# 这里放实际的安装逻辑
except FileNotFoundError as e:
logging.error(f"文件错误: {e}")
return False
except PermissionError as e:
logging.error(f"权限不足: {e}")
return False
except Exception as e:
logging.error(f"未知错误: {e}")
return False
else:
logging.info(f"安装成功: {pkg_path}")
return True
def main():
"""主函数,统一错误处理"""
try:
packages = ["/tmp/pkg1.rpm", "/tmp/pkg2.rpm"]
for pkg in packages:
if not install_package(pkg):
sys.exit(1)
except KeyboardInterrupt:
logging.warning("用户中断安装")
sys.exit(1)
except Exception as e:
logging.critical(f"安装过程异常: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
你看,Python 的异常处理可以精确到每种错误类型。我在项目中遇到过这样的情况:磁盘空间不足导致安装失败,但 Shell 脚本只返回了「复制失败」,排查了半天才发现是磁盘满了。而 Python 的 OSError 会直接告诉你「No space left on device」。
3.4.3 错误处理的最佳实践
总结一下我多年积累的经验:
| 场景 | Shell 做法 | Python 做法 |
|---|---|---|
| 命令执行失败 | set -e 或 || exit 1 |
subprocess.check_call() |
| 文件不存在 | [ -f file ] || exit 1 |
try: open() except FileNotFoundError |
| 权限不足 | [ -w dir ] || exit 1 |
try: os.mkdir() except PermissionError |
| 用户中断 | trap 'exit 1' INT |
except KeyboardInterrupt |
我的个人建议:对于超过 50 行的安装脚本,直接用 Python。Shell 适合做「胶水」,把几个命令串起来。一旦涉及逻辑判断、错误恢复、日志记录,Python 的优势就体现出来了。
避坑指南:我曾经写过一个 200 行的 Shell 安装脚本,各种 if 嵌套、case 分支,最后自己都看不懂了。后来全部重写成 Python,代码量减少了一半,可读性还提高了。所以,别在 Shell 里写「业务逻辑」。
3.5 本章小结
这一章我们聊了:
- Shell 脚本的基本语法:变量、判断、循环
- Python 脚本的优势和基本写法
- 参数传递的两种方式:
getopts和argparse - 错误处理的核心思想:不要静默失败
下一章,我们会把这些脚本知识应用到实际的安装流程中,看看怎么用脚本实现一个完整的自动化安装。到时候你会发现,基础打好了,后面的路就好走了。