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
  • 错误处理的核心思想:不要静默失败

下一章,我们会把这些脚本知识应用到实际的安装流程中,看看怎么用脚本实现一个完整的自动化安装。到时候你会发现,基础打好了,后面的路就好走了。