Shell 文件包含详解

Shell文件包含简介

文件包含是Shell脚本编程中实现代码重用和模块化的重要技术。通过文件包含,我们可以将常用的函数、配置和代码片段分离到独立的文件中,提高代码的可维护性和重用性。

source命令

在当前Shell环境中执行指定文件中的命令。

点操作符

.操作符是source命令的简写形式。

函数库

将相关函数组织到库文件中,实现代码模块化。

配置文件

将配置参数分离到单独文件中,便于管理和修改。

安全检查

在包含文件前进行存在性检查和权限验证。

模块化设计

将大型脚本拆分为多个小模块,提高可维护性。

文件包含的重要性

文件包含是Shell脚本实现模块化编程的基础。通过合理的文件包含,我们可以避免代码重复,提高开发效率,使脚本更加易于维护和扩展。合理的模块化设计是编写高质量Shell脚本的关键。

基本文件包含操作

学习使用source命令和点操作符进行文件包含。

文件包含关系
main.sh
config.sh
utils.sh

main.sh
functions.sh
方法 语法 说明 示例
source source filename 在当前Shell环境中执行文件 source config.sh
.(点) . filename source的简写形式 . functions.sh
相对路径 source ./lib/utils.sh 使用相对路径包含文件 source ../config.sh
绝对路径 source /path/to/file 使用绝对路径包含文件 source /opt/app/config.sh
条件包含 [ -f file ] && source file 检查文件存在性后再包含 [ -f config.sh ] && source config.sh

基本文件包含示例

project/
├── main.sh
├── config.sh
└── utils.sh
config.sh - 配置文件
config.sh
#!/bin/bash

# 应用配置
APP_NAME="MyApplication"
APP_VERSION="1.0.0"
LOG_LEVEL="INFO"
MAX_RETRY=3

# 数据库配置
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="myapp"
DB_USER="app_user"

# 路径配置
LOG_DIR="/var/log/myapp"
DATA_DIR="/var/lib/myapp"
TEMP_DIR="/tmp/myapp"

# 颜色配置(用于输出)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
utils.sh - 工具函数
utils.sh
#!/bin/bash

# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}

log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}

log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}

# 检查命令是否存在
check_command() {
if ! command -v "$1" &> /dev/null; then
    log_error "命令不存在: $1"
    return 1
fi
return 0
}

# 检查文件是否存在
check_file() {
if [ ! -f "$1" ]; then
    log_error "文件不存在: $1"
    return 1
fi
return 0
}

# 创建目录(如果不存在)
ensure_dir() {
if [ ! -d "$1" ]; then
    mkdir -p "$1"
    log_info "创建目录: $1"
fi
}

# 安全删除文件
safe_remove() {
if [ -f "$1" ]; then
    rm -f "$1"
    log_info "删除文件: $1"
fi
}
main.sh
#!/bin/bash

# 主脚本 - 文件包含示例

echo "=== Shell文件包含演示 ==="

# 方法1: 使用source命令包含配置文件
echo "1. 包含配置文件..."
source config.sh

# 方法2: 使用点操作符包含工具函数
echo "2. 包含工具函数..."
. utils.sh

# 使用包含的配置和函数
echo "3. 使用包含的内容..."
log_info "应用程序: $APP_NAME v$APP_VERSION"
log_info "数据库: $DB_HOST:$DB_PORT/$DB_NAME"

# 检查必要命令
echo "4. 检查系统依赖..."
check_command "curl" || exit 1
check_command "wget" || exit 1

# 创建必要目录
echo "5. 初始化目录结构..."
ensure_dir "$LOG_DIR"
ensure_dir "$DATA_DIR"
ensure_dir "$TEMP_DIR"

# 演示函数使用
echo "6. 演示工具函数..."
log_success "所有检查通过!"
log_warn "这是一个警告信息"
log_error "这是一个错误信息"

# 条件包含示例
echo "7. 条件包含演示..."
if [ -f "optional.sh" ]; then
source optional.sh
log_info "可选模块已加载"
else
log_warn "可选模块不存在,跳过"
fi

echo "=== 脚本执行完成 ==="
运行示例:
$ chmod +x main.sh config.sh utils.sh
$ ./main.sh

函数库创建和使用

学习如何创建和使用函数库来实现代码重用。

完整的函数库示例

library_project/
├── main.sh
├── lib/
│ ├── string.sh
│ ├── file.sh
│ ├── network.sh
│ └── system.sh
└── config/
└── app.conf
lib/string.sh - 字符串处理库
lib/string.sh
#!/bin/bash

# 字符串处理函数库

# 转换为大写
string_upper() {
echo "$1" | tr '[:lower:]' '[:upper:]'
}

# 转换为小写
string_lower() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}

# 字符串长度
string_length() {
echo ${#1}
}

# 去除首尾空格
string_trim() {
local str="$1"
str="${str#"${str%%[![:space:]]*}"}"  # 去除前导空格
str="${str%"${str##*[![:space:]]}"}"  # 去除尾部空格
echo "$str"
}

# 字符串替换
string_replace() {
local str="$1"
local search="$2"
local replace="$3"
echo "${str//$search/$replace}"
}

# 检查是否包含子字符串
string_contains() {
[[ "$1" == *"$2"* ]]
}

# 检查是否以指定字符串开头
string_starts_with() {
[[ "$1" == "$2"* ]]
}

# 检查是否以指定字符串结尾
string_ends_with() {
[[ "$1" == *"$2" ]]
}

# 生成随机字符串
string_random() {
local length=${1:-8}
tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c "$length"
}

# 字符串分割为数组
string_split() {
local str="$1"
local delimiter="$2"
IFS="$delimiter" read -ra arr <<< "$str"
printf '%s\n' "${arr[@]}"
}
lib/file.sh - 文件操作库
lib/file.sh
#!/bin/bash

# 文件操作函数库

# 获取文件大小(字节)
file_size() {
stat -f%z "$1" 2>/dev/null || stat -c%s "$1" 2>/dev/null
}

# 获取文件修改时间
file_mtime() {
stat -f%m "$1" 2>/dev/null || stat -c%Y "$1" 2>/dev/null
}

# 获取文件扩展名
file_extension() {
local filename=$(basename "$1")
echo "${filename##*.}"
}

# 获取文件名(不含扩展名)
file_basename() {
local filename=$(basename "$1")
echo "${filename%.*}"
}

# 备份文件
file_backup() {
local file="$1"
local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)"

if cp "$file" "$backup"; then
    echo "$backup"
    return 0
else
    return 1
fi
}

# 安全写入文件(先备份原文件)
file_safe_write() {
local file="$1"
local content="$2"

# 如果文件存在,先备份
if [ -f "$file" ]; then
    file_backup "$file" > /dev/null
fi

# 写入新内容
echo "$content" > "$file"
}

# 在文件中查找并替换
file_replace() {
local file="$1"
local search="$2"
local replace="$3"

if sed -i.tmp "s/$search/$replace/g" "$file" 2>/dev/null; then
    rm -f "${file}.tmp"
    return 0
else
    return 1
fi
}

# 检查文件是否可读
file_is_readable() {
[ -r "$1" ]
}

# 检查文件是否可写
file_is_writable() {
[ -w "$1" ]
}

# 获取文件行数
file_line_count() {
wc -l < "$1" 2>/dev/null || echo "0"
}
main.sh
#!/bin/bash

# 主脚本 - 函数库使用示例

# 设置库文件路径
LIB_DIR="$(dirname "$0")/lib"

# 包含所有库文件
echo "加载函数库..."
for lib_file in "$LIB_DIR"/*.sh; do
if [ -f "$lib_file" ]; then
    echo "  加载: $(basename "$lib_file")"
    source "$lib_file"
fi
done

echo -e "\n=== 字符串函数演示 ==="

# 字符串函数测试
test_str="  Hello, World!  "
echo "原始字符串: '$test_str'"
echo "大写: '$(string_upper "$test_str")'"
echo "小写: '$(string_lower "$test_str")'"
echo "去除空格: '$(string_trim "$test_str")'"
echo "长度: $(string_length "$test_str")"
echo "替换: '$(string_replace "$test_str" "World" "Shell")'"
echo "包含 'Hello': $(string_contains "$test_str" "Hello" && echo "是" || echo "否")"
echo "随机字符串: $(string_random 10)"

echo -e "\n=== 文件函数演示 ==="

# 创建测试文件
TEST_FILE="test_file.txt"
echo "Line 1" > "$TEST_FILE"
echo "Line 2" >> "$TEST_FILE"
echo "Line 3" >> "$TEST_FILE"

echo "测试文件: $TEST_FILE"
echo "文件大小: $(file_size "$TEST_FILE") 字节"
echo "文件行数: $(file_line_count "$TEST_FILE")"
echo "文件扩展名: $(file_extension "$TEST_FILE")"

# 文件备份演示
backup_file=$(file_backup "$TEST_FILE")
echo "备份文件: $backup_file"

# 文件替换演示
file_replace "$TEST_FILE" "Line" "Row"
echo "替换后的内容:"
cat "$TEST_FILE"

echo -e "\n=== 综合示例 ==="

# 处理文件名
filename="/path/to/example.tar.gz"
echo "文件名: $filename"
echo "基础名: $(file_basename "$filename")"
echo "扩展名: $(file_extension "$filename")"

# 清理测试文件
rm -f "$TEST_FILE" "$backup_file"

echo -e "\n=== 库函数验证完成 ==="
函数库设计原则:
  • 使用有意义的函数名前缀避免命名冲突
  • 每个函数库专注于一个特定的功能领域
  • 函数应该尽可能独立,减少依赖
  • 提供清晰的错误处理和返回值
  • 为函数添加适当的注释说明
  • 避免在库文件中执行实际代码,只定义函数

配置文件管理

学习如何使用文件包含来管理应用程序配置。

配置文件管理示例

config/app.conf - 主配置文件
config/app.conf
#!/bin/bash

# 应用程序配置文件

# 应用基本信息
APP_NAME="MyApplication"
APP_VERSION="2.1.0"
APP_DESCRIPTION="一个强大的Shell应用程序"

# 日志配置
LOG_LEVEL="INFO"
LOG_FILE="/var/log/myapp/app.log"
LOG_MAX_SIZE="10M"
LOG_BACKUP_COUNT=5

# 数据库配置
DB_DRIVER="postgresql"
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="production_db"
DB_USER="app_user"
DB_PASSWORD="secure_password"

# API配置
API_BASE_URL="https://api.example.com"
API_TIMEOUT=30
API_RETRY_COUNT=3

# 功能开关
FEATURE_AUTH_ENABLED=true
FEATURE_CACHE_ENABLED=true
FEATURE_LOGGING_ENABLED=true

# 路径配置
UPLOAD_DIR="/var/www/uploads"
TEMP_DIR="/tmp/myapp"
BACKUP_DIR="/var/backups/myapp"
config/database.conf - 数据库配置
config/database.conf
#!/bin/bash

# 数据库配置文件

# 连接池配置
DB_POOL_MIN=2
DB_POOL_MAX=20
DB_CONNECTION_TIMEOUT=30
DB_QUERY_TIMEOUT=60

# 性能配置
DB_CACHE_SIZE="256MB"
DB_WAL_BUFFER_SIZE="16MB"

# 备份配置
DB_BACKUP_ENABLED=true
DB_BACKUP_SCHEDULE="0 2 * * *"  # 每天凌晨2点
DB_BACKUP_RETENTION=30          # 保留30天

# 监控配置
DB_MONITOR_ENABLED=true
DB_MONITOR_INTERVAL=60          # 60秒
DB_SLOW_QUERY_THRESHOLD=1000    # 1秒
config_loader.sh
#!/bin/bash

# 配置文件加载器

# 配置目录
CONFIG_DIR="$(dirname "$0")/config"

# 默认配置值
declare -A DEFAULT_CONFIG=(
["LOG_LEVEL"]="INFO"
["API_TIMEOUT"]=30
["DB_POOL_MIN"]=2
["FEATURE_AUTH_ENABLED"]=true
)

# 加载配置文件
load_config() {
local config_file="$1"

if [ ! -f "$config_file" ]; then
    echo "错误: 配置文件不存在: $config_file" >&2
    return 1
fi

# 检查文件权限(不应该全局可写)
if [ "$(stat -c %a "$config_file" 2>/dev/null || stat -f %A "$config_file")" -eq 666 ] ||
   [ "$(stat -c %a "$config_file" 2>/dev/null || stat -f %A "$config_file")" -eq 777 ]; then
    echo "警告: 配置文件权限过于宽松: $config_file" >&2
fi

# 加载配置文件
source "$config_file"
echo "加载配置文件: $config_file"
}

# 加载所有配置文件
load_all_configs() {
echo "开始加载配置文件..."

# 加载主配置
load_config "$CONFIG_DIR/app.conf"

# 加载数据库配置
if [ -f "$CONFIG_DIR/database.conf" ]; then
    load_config "$CONFIG_DIR/database.conf"
fi

# 加载环境特定配置
local env_config="$CONFIG_DIR/app.${APP_ENV:-production}.conf"
if [ -f "$env_config" ]; then
    load_config "$env_config"
fi

# 加载本地开发配置(如果存在)
if [ -f "$CONFIG_DIR/app.local.conf" ]; then
    load_config "$CONFIG_DIR/app.local.conf"
fi

echo "配置文件加载完成"
}

# 验证配置
validate_config() {
local errors=0

echo "验证配置..."

# 检查必要配置项
local required_vars=("APP_NAME" "DB_HOST" "DB_NAME")
for var in "${required_vars[@]}"; do
    if [ -z "${!var}" ]; then
        echo "错误: 缺少必要配置项: $var" >&2
        ((errors++))
    fi
done

# 验证数值范围
if [ "$API_TIMEOUT" -lt 1 ] || [ "$API_TIMEOUT" -gt 300 ]; then
    echo "错误: API_TIMEOUT 必须在 1-300 之间" >&2
    ((errors++))
fi

# 验证布尔值
local bool_vars=("FEATURE_AUTH_ENABLED" "FEATURE_CACHE_ENABLED")
for var in "${bool_vars[@]}"; do
    if [ "${!var}" != "true" ] && [ "${!var}" != "false" ]; then
        echo "错误: $var 必须是 'true' 或 'false'" >&2
        ((errors++))
    fi
done

return $errors
}

# 显示当前配置
show_config() {
echo "=== 当前配置 ==="

# 显示应用配置
echo "应用信息:"
echo "  名称: $APP_NAME"
echo "  版本: $APP_VERSION"
echo "  日志级别: $LOG_LEVEL"

# 显示数据库配置(隐藏密码)
echo "数据库:"
echo "  主机: $DB_HOST"
echo "  端口: $DB_PORT"
echo "  数据库: $DB_NAME"
echo "  用户: $DB_USER"
echo "  密码: ******"

# 显示功能开关
echo "功能开关:"
echo "  认证: $FEATURE_AUTH_ENABLED"
echo "  缓存: $FEATURE_CACHE_ENABLED"
echo "  日志: $FEATURE_LOGGING_ENABLED"
}

# 导出配置为环境变量
export_config() {
export APP_NAME APP_VERSION LOG_LEVEL
export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
export API_BASE_URL API_TIMEOUT
export FEATURE_AUTH_ENABLED FEATURE_CACHE_ENABLED FEATURE_LOGGING_ENABLED
echo "配置已导出为环境变量"
}

# 主函数
main() {
# 设置环境(开发/测试/生产)
export APP_ENV="${1:-production}"

echo "运行环境: $APP_ENV"

# 加载配置
if ! load_all_configs; then
    echo "配置加载失败" >&2
    return 1
fi

# 验证配置
if ! validate_config; then
    echo "配置验证失败" >&2
    return 1
fi

# 显示配置
show_config

# 导出配置
export_config

echo "配置管理完成"
}

# 如果直接执行此脚本,则运行主函数
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
配置管理最佳实践:
$ source config_loader.sh
$ main
$ # 或者直接使用配置
$ source config_loader.sh && echo "应用: $APP_NAME"

高级文件包含技巧

学习文件包含的高级用法和最佳实践。

高级文件包含示例

advanced_includes.sh
#!/bin/bash

# 高级文件包含技巧示例

echo "=== 动态文件包含 ==="

# 根据条件动态包含文件
OS_TYPE=$(uname -s)
case $OS_TYPE in
"Linux")
    source "lib/linux.sh"
    echo "已加载 Linux 特定库"
    ;;
"Darwin")
    source "lib/macos.sh"
    echo "已加载 macOS 特定库"
    ;;
"CYGWIN"*|"MINGW"*|"MSYS"*)
    source "lib/windows.sh"
    echo "已加载 Windows 特定库"
    ;;
*)
    echo "未知操作系统: $OS_TYPE"
    ;;
esac

echo -e "\n=== 安全的文件包含 ==="

# 安全的文件包含函数
safe_source() {
local file="$1"

# 检查文件是否存在
if [ ! -f "$file" ]; then
    echo "错误: 文件不存在: $file" >&2
    return 1
fi

# 检查文件权限(不应该全局可写)
local permissions=$(stat -c %a "$file" 2>/dev/null || stat -f %A "$file")
if [ "$permissions" -eq 666 ] || [ "$permissions" -eq 777 ]; then
    echo "错误: 文件权限不安全: $file" >&2
    return 1
fi

# 检查文件大小(避免包含过大的文件)
local file_size=$(stat -c %s "$file" 2>/dev/null || stat -f %z "$file")
if [ "$file_size" -gt 1048576 ]; then  # 1MB
    echo "错误: 文件过大: $file" >&2
    return 1
fi

# 包含文件
source "$file"
echo "安全加载: $file"
return 0
}

# 使用安全包含
safe_source "config.sh"

echo -e "\n=== 递归文件包含 ==="

# 自动加载目录下的所有库文件
load_libraries() {
local lib_dir="$1"

if [ ! -d "$lib_dir" ]; then
    echo "错误: 库目录不存在: $lib_dir" >&2
    return 1
fi

# 查找所有 .sh 文件(除了以 _ 开头的)
while IFS= read -r -d '' lib_file; do
    local basename=$(basename "$lib_file")
    # 跳过以 _ 开头的文件(如 _template.sh)
    if [[ "$basename" != _* ]]; then
        safe_source "$lib_file"
    fi
done < <(find "$lib_dir" -name "*.sh" -type f -print0)

echo "已加载 $lib_dir 目录下的所有库文件"
}

# 创建示例库目录结构
mkdir -p libs/{string,file,network}
echo "# 字符串库" > libs/string/strings.sh
echo "# 文件库" > libs/file/files.sh
echo "# 网络库" > libs/network/network.sh
echo "# 模板文件" > libs/string/_template.sh

# 加载所有库
load_libraries "libs"

echo -e "\n=== 条件函数定义 ==="

# 避免函数重复定义
if ! declare -f "my_function" > /dev/null; then
my_function() {
    echo "这是 my_function"
}
echo "函数 my_function 已定义"
else
echo "函数 my_function 已存在,跳过定义"
fi

echo -e "\n=== 命名空间模拟 ==="

# 使用前缀模拟命名空间
namespace_example() {
local ns="$1"

# 定义命名空间函数
eval "${ns}_hello() { echo \"Hello from $ns\"; }"
eval "${ns}_config() { echo \"Config for $ns: \$1\"; }"
}

# 创建不同命名空间的函数
namespace_example "app"
namespace_example "db"
namespace_example "api"

# 使用命名空间函数
app_hello
db_hello
app_config "database"
db_config "connection"

echo -e "\n=== 配置覆盖机制 ==="

# 基础配置
load_base_config() {
DEFAULT_TIMEOUT=30
DEFAULT_RETRIES=3
LOG_LEVEL="INFO"
}

# 用户配置(如果存在)
load_user_config() {
local user_config="$HOME/.myapp/config.sh"
if [ -f "$user_config" ]; then
    source "$user_config"
    echo "已加载用户配置: $user_config"
fi
}

# 环境特定配置
load_env_config() {
local env="${APP_ENV:-production}"
local env_config="config/$env.sh"
if [ -f "$env_config" ]; then
    source "$env_config"
    echo "已加载环境配置: $env_config"
fi
}

# 加载配置(后面的配置会覆盖前面的)
load_base_config
load_user_config
load_env_config

echo "最终配置: TIMEOUT=$DEFAULT_TIMEOUT, RETRIES=$DEFAULT_RETRIES, LOG_LEVEL=$LOG_LEVEL"

echo -e "\n=== 错误处理和恢复 ==="

# 带错误恢复的文件包含
source_with_fallback() {
local primary_file="$1"
local fallback_file="$2"

if safe_source "$primary_file"; then
    echo "使用主文件: $primary_file"
    return 0
elif [ -n "$fallback_file" ] && safe_source "$fallback_file"; then
    echo "使用备用文件: $fallback_file"
    return 0
else
    echo "错误: 无法加载任何配置文件" >&2
    return 1
fi
}

# 尝试加载配置,有备用方案
source_with_fallback "production.conf" "default.conf"

# 清理临时文件
rm -rf libs

echo -e "\n=== 高级技巧演示完成 ==="
安全注意事项:
  • 始终验证要包含的文件是否存在且有适当权限
  • 避免包含用户可写的配置文件,防止代码注入
  • 对来自不可信来源的文件要格外小心
  • 考虑使用set -e在包含失败时退出脚本
  • 定期审计包含的文件内容和权限

综合示例

通过实际项目示例展示Shell文件包含的综合应用。

完整的应用框架

使用文件包含技术构建一个完整的Shell应用框架。

myapp/
├── bin/
│ └── myapp
├── lib/
│ ├── core.sh
│ ├── utils.sh
│ ├── logger.sh
│ └── validator.sh
├── config/
│ ├── app.conf
│ ├── database.conf
│ └── logging.conf
└── commands/
├── init.sh
├── deploy.sh
└── backup.sh
lib/core.sh - 核心库
lib/core.sh
#!/bin/bash

# 应用核心库

# 应用基本信息
APP_NAME="MyApp Framework"
APP_VERSION="1.0.0"
APP_AUTHOR="Dev Team"

# 应用根目录
APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# 初始化应用
app_init() {
echo "初始化 $APP_NAME v$APP_VERSION"

# 加载配置
load_config

# 加载库文件
load_libraries

# 设置信号处理
setup_signal_handlers

# 验证环境
validate_environment
}

# 加载配置
load_config() {
local config_dir="$APP_ROOT/config"

# 加载主配置
if [ -f "$config_dir/app.conf" ]; then
    source "$config_dir/app.conf"
fi

# 加载数据库配置
if [ -f "$config_dir/database.conf" ]; then
    source "$config_dir/database.conf"
fi

# 加载日志配置
if [ -f "$config_dir/logging.conf" ]; then
    source "$config_dir/logging.conf"
fi

# 设置默认值
: ${LOG_LEVEL:="INFO"}
: ${LOG_FILE:="$APP_ROOT/logs/app.log"}
: ${DB_HOST:="localhost"}
: ${DB_PORT:="3306"}
}

# 加载库文件
load_libraries() {
local lib_dir="$APP_ROOT/lib"

for lib_file in "$lib_dir"/*.sh; do
    if [ -f "$lib_file" ] && [ "$(basename "$lib_file")" != "core.sh" ]; then
        source "$lib_file"
    fi
done
}

# 设置信号处理
setup_signal_handlers() {
trap 'cleanup' EXIT INT TERM
}

# 清理函数
cleanup() {
log_info "应用正在关闭..."
# 执行清理操作
log_info "清理完成"
}

# 验证环境
validate_environment() {
log_info "验证运行环境..."

# 检查必要命令
local required_commands=("bash" "curl" "awk" "sed")
for cmd in "${required_commands[@]}"; do
    if ! command -v "$cmd" > /dev/null; then
        log_error "缺少必要命令: $cmd"
        return 1
    fi
done

# 检查目录权限
local required_dirs=("$APP_ROOT/logs" "$APP_ROOT/tmp")
for dir in "${required_dirs[@]}"; do
    if [ ! -d "$dir" ]; then
        mkdir -p "$dir"
    fi
    if [ ! -w "$dir" ]; then
        log_error "目录不可写: $dir"
        return 1
    fi
done

log_success "环境验证通过"
return 0
}

# 运行命令
run_command() {
local command_name="$1"
shift
local command_file="$APP_ROOT/commands/${command_name}.sh"

if [ ! -f "$command_file" ]; then
    log_error "命令不存在: $command_name"
    return 1
fi

# 在子shell中运行命令,避免污染当前环境
(
    source "$command_file"
    if declare -f "command_$command_name" > /dev/null; then
        "command_$command_name" "$@"
    else
        log_error "命令函数未定义: command_$command_name"
        return 1
    fi
)
}
commands/init.sh - 初始化命令
commands/init.sh
#!/bin/bash

# 初始化命令

command_init() {
echo "执行初始化操作..."

# 创建必要目录
local directories=(
    "$APP_ROOT/logs"
    "$APP_ROOT/tmp"
    "$APP_ROOT/data"
    "$APP_ROOT/backups"
)

for dir in "${directories[@]}"; do
    if [ ! -d "$dir" ]; then
        mkdir -p "$dir"
        log_info "创建目录: $dir"
    fi
done

# 创建配置文件(如果不存在)
init_config_files

# 初始化数据库
init_database

log_success "初始化完成"
}

# 初始化配置文件
init_config_files() {
local config_dir="$APP_ROOT/config"

# 应用配置
if [ ! -f "$config_dir/app.conf" ]; then
    cat > "$config_dir/app.conf" << 'EOF'
#!/bin/bash

# 应用配置
APP_NAME="MyApp"
APP_ENV="development"
LOG_LEVEL="INFO"

# 功能开关
FEATURE_API=true
FEATURE_JOBS=true
FEATURE_REPORTS=true
EOF
    log_info "创建应用配置文件"
fi

# 数据库配置
if [ ! -f "$config_dir/database.conf" ]; then
    cat > "$config_dir/database.conf" << 'EOF'
#!/bin/bash

# 数据库配置
DB_DRIVER="mysql"
DB_HOST="localhost"
DB_PORT="3306"
DB_NAME="myapp"
DB_USER="app_user"
DB_PASSWORD="password"

# 连接池
DB_POOL_MIN=2
DB_POOL_MAX=20
EOF
    log_info "创建数据库配置文件"
fi
}

# 初始化数据库
init_database() {
log_info "初始化数据库..."

# 这里应该是实际的数据库初始化逻辑
# 例如:创建数据库、表、初始数据等

# 模拟数据库初始化
if [ "$DB_DRIVER" = "mysql" ]; then
    log_info "初始化 MySQL 数据库"
    # mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;"
elif [ "$DB_DRIVER" = "postgresql" ]; then
    log_info "初始化 PostgreSQL 数据库"
    # psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;"
fi

log_success "数据库初始化完成"
}
bin/myapp
#!/bin/bash

# MyApp 主程序

# 应用根目录
APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# 加载核心库
source "$APP_ROOT/lib/core.sh"

# 显示帮助信息
show_help() {
cat << EOF
MyApp - Shell 应用框架

用法: $0 [命令] [选项]

命令:
init     初始化应用
deploy   部署应用
backup   备份数据
status   查看状态
help     显示此帮助信息

选项:
-h, --help     显示帮助
-v, --version  显示版本

示例:
$0 init
$0 deploy --environment=production
$0 status
EOF
}

# 显示版本
show_version() {
echo "$APP_NAME v$APP_VERSION"
}

# 主函数
main() {
local command="${1:-help}"

case "$command" in
    -h|--help|help)
        show_help
        ;;
    -v|--version|version)
        show_version
        ;;
    init|deploy|backup|status)
        # 初始化应用
        app_init || exit 1

        # 执行命令
        run_command "$command" "${@:2}"
        ;;
    *)
        echo "错误: 未知命令 '$command'" >&2
        echo "使用 '$0 help' 查看可用命令" >&2
        exit 1
        ;;
esac
}

# 运行主函数
main "$@"
使用示例:
$ chmod +x bin/myapp
$ ./bin/myapp init
$ ./bin/myapp status
$ ./bin/myapp help

文件包含最佳实践

不推荐的写法
  • 直接包含不可信的文件
  • 在函数内部使用source
  • 不检查文件存在性
  • 使用相对路径混乱
  • 不处理包含错误
  • 函数命名没有前缀
推荐的写法
  • 使用安全包含函数
  • 在脚本开头集中包含
  • 验证文件存在和权限
  • 使用绝对路径或可靠相对路径
  • 适当的错误处理
  • 使用命名空间前缀
项目结构建议:
  • bin/ - 可执行脚本
  • lib/ - 库文件和函数
  • config/ - 配置文件
  • commands/ - 命令模块
  • tests/ - 测试文件
  • docs/ - 文档
调试技巧

使用 set -x 开启调试模式查看文件包含过程。使用 declare -f 查看已定义的函数。在库文件中使用 return 而不是 exit 避免意外退出主脚本。