配置管理:配置文件解析与环境变量 | Configuration Management: Config Files and Environment Variables

章节概述

本章全面讲解 Shell 脚本中的配置管理策略,包括 KEY=VALUE 配置文件解析、INI/TOML 格式解析、环境变量优先级机制、.env 文件使用、配置文件模板系统,以及与 C 语言配置文件读取的对比分析。

核心理念:配置是代码与环境之间的契约。良好的配置管理让你的脚本在开发、测试、生产环境中无缝切换,零修改部署。


第1节:KEY=VALUE 配置文件解析

标准配置文件格式

# config/app.conf
APP_NAME="MyApp"
APP_VERSION="1.0.0"
APP_PORT=8080
APP_HOST="0.0.0.0"
APP_DEBUG=true
APP_LOG_LEVEL="info"
DATABASE_URL="mysql://localhost:3306/mydb"

解析函数

#!/usr/bin/env bash
 
# 读取单个配置值
read_config() {
  local file="${1}"
  local key="${2}"
  local default="${3:-}"
 
  if [ ! -f "${file}" ]; then
    echo "${default}"
    return 1
  fi
 
  local value
  value=$(grep -E "^${key}=" "${file}" | head -1 | cut -d'=' -f2-)
 
  # 去除引号
  value="${value%"}"
  value="${value#"}"
  value="${value%'}"
  value="${value#'}"
 
  if [ -z "${value}" ]; then
    echo "${default}"
  else
    echo "${value}"
  fi
}
 
# 加载所有配置到环境变量
load_config() {
  local file="${1}"
  local prefix="${2:-}"
 
  if [ ! -f "${file}" ]; then
    echo "Config file not found: ${file}" >&2
    return 1
  fi
 
  while IFS='=' read -r key value; do
    [[ "${key}" =~ ^[[:space:]]*# ]] && continue
    [[ -z "${key}" ]] && continue
 
    value="${value%"}"
    value="${value#"}"
    value="${value%'}"
    value="${value#'}"
 
    if [ -n "${prefix}" ]; then
      key="${prefix}_${key}"
    fi
 
    export "${key}=${value}"
  done < "${file}"
}

使用示例

# 读取单个值
APP_PORT=$(read_config "config/app.conf" "APP_PORT" "8080")
echo "Server running on port: ${APP_PORT}"
 
# 加载所有配置
load_config "config/app.conf"
echo "App: ${APP_NAME}"
 
# 带前缀加载(避免命名冲突)
load_config "config/database.conf" "DB"
echo "Database: ${DB_DATABASE_URL}"

第2节:INI/TOML 格式解析

INI 文件解析

# config/settings.ini
[database]
host=localhost
port=3306
name=mydb
user=admin
 
[server]
host=0.0.0.0
port=8080
workers=4
 
[logging]
level=info
file=/var/log/app.log
parse_ini() {
  local file="${1}"
  local section=""
 
  while IFS='=' read -r key value || [ -n "${key}" ]; do
    key=$(echo "${key}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    value=$(echo "${value}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
 
    [[ "${key}" =~ ^[[:space:]]*\; ]] && continue
    [[ "${key}" =~ ^[[:space:]]*\# ]] && continue
    [[ -z "${key}" ]] && continue
 
    if [[ "${key}" =~ ^\[(.+)\]$ ]]; then
      section="${BASH_REMATCH[1]}"
      continue
    fi
 
    value="${value%"}"
    value="${value#"}"
    value="${value%'}"
    value="${value#'}"
 
    export "${section:+${section}_}${key}=${value}"
  done < "${file}"
}
 
# 使用
parse_ini "config/settings.ini"
echo "${database_host}"    # localhost
echo "${server_port}"      # 8080

TOML 文件解析(简单格式)

# config/config.toml
[app]
name = "MyApp"
version = "1.0.0"
debug = true
 
[database]
host = "localhost"
port = 3306
parse_toml() {
  local file="${1}"
  local section=""
 
  while IFS='=' read -r key value || [ -n "${key}" ]; do
    key=$(echo "${key}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    value=$(echo "${value}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
 
    [[ "${key}" =~ ^\# ]] && continue
    [[ -z "${key}" ]] && continue
 
    if [[ "${key}" =~ ^\[(.+)\]$ ]]; then
      section="${BASH_REMATCH[1]}"
      continue
    fi
 
    value="${value%"}"
    value="${value#"}"
    value="${value%'}"
    value="${value#'}"
    case "${value}" in
      true)  value=1 ;;
      false) value=0 ;;
    esac
 
    export "${section:+${section}_}${key}=${value}"
  done < "${file}"
}

第3节:环境变量优先级机制

优先级顺序(从低到高)

# 优先级:默认值 < 配置文件 < 环境变量 < 命令行参数
 
# 1. 默认值
APP_PORT="${APP_PORT:-8080}"
 
# 2. 配置文件
if [ -f "config/app.conf" ]; then
  file_port=$(read_config "config/app.conf" "APP_PORT")
  [ -n "${file_port}" ] && APP_PORT="${file_port}"
fi
 
# 3. 环境变量
# APP_PORT 已经在环境中,直接使用
 
# 4. 命令行参数
while [[ $# -gt 0 ]]; do
  case "${1}" in
    --port)
      APP_PORT="${2}"
      shift 2
      ;;
    *)
      shift
      ;;
  esac
done

优先级实现函数

get_config() {
  local key="${1}"
  local default="${2:-}"
  local config_file="${3:-config/app.conf}"
 
  local value="${default}"
 
  # 优先级1:配置文件
  if [ -f "${config_file}" ]; then
    local file_value
    file_value=$(read_config "${config_file}" "${key}")
    [ -n "${file_value}" ] && value="${file_value}"
  fi
 
  # 优先级2:环境变量
  local env_value="${!key:-}"
  [ -n "${env_value}" ] && value="${env_value}"
 
  echo "${value}"
}
 
# 使用
PORT=$(get_config "APP_PORT" "8080" "config/app.conf")

优先级对照表

优先级来源覆盖方式示例
1 (最低)默认值硬编码在脚本中PORT=8080
2配置文件编辑文件APP_PORT=9090
3环境变量export 设置export APP_PORT=7070
4 (最高)命令行参数传参./script --port 6060

第4节:.env 文件使用

.env 文件格式

# .env - 环境变量配置文件
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DB_USER=admin
DB_PASSWORD=secret123
APP_ENV=development
APP_DEBUG=true
APP_SECRET_KEY=abc123def456
API_BASE_URL=https://api.example.com
API_TIMEOUT=30

.env 加载函数

load_dotenv() {
  local env_file="${1:-.env}"
 
  if [ ! -f "${env_file}" ]; then
    echo "Warning: ${env_file} not found" >&2
    return 1
  fi
 
  while IFS='=' read -r key value; do
    [[ "${key}" =~ ^[[:space:]]*# ]] && continue
    [[ -z "${key}" ]] && continue
 
    key=$(echo "${key}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
 
    value="${value%"}"
    value="${value#"}"
    value="${value%'}"
    value="${value#'}"
 
    # 仅在环境变量未设置时加载
    if [ -z "${!key:-}" ]; then
      export "${key}=${value}"
    fi
  done < "${env_file}"
}
 
# 强制覆盖模式
load_dotenv_force() {
  local env_file="${1:-.env}"
 
  while IFS='=' read -r key value; do
    [[ "${key}" =~ ^[[:space:]]*# ]] && continue
    [[ -z "${key}" ]] && continue
    key=$(echo "${key}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    value="${value%"}"
    value="${value#"}"
    value="${value%'}"
    value="${value#'}"
    export "${key}=${value}"
  done < "${env_file}"
}

.env 安全实践

# .env.example(提交到版本控制)
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DB_USER=admin
DB_PASSWORD=changeme
 
# .gitignore 中添加
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.production" >> .gitignore
 
# 加载 .env(不覆盖现有环境变量)
load_dotenv ".env"
# 然后加载环境特定配置
load_dotenv ".env.${APP_ENV:-development}"

第5节:配置文件模板与多环境

模板系统

render_template() {
  local template="${1}"
  local output="${2}"
 
  if [ ! -f "${template}" ]; then
    echo "Template not found: ${template}" >&2
    return 1
  fi
 
  local content
  content=$(cat "${template}")
 
  local vars
  vars=$(grep -oE '\$\{[A-Z_]+\}' "${template}" | sort -u | tr -d '${}}')
 
  for var in ${vars}; do
    local value="${!var:-}"
    content="${content//\${${var}}/${value}}"
  done
 
  echo "${content}" > "${output}"
}

多环境配置

#!/usr/bin/env bash
 
# 环境检测
detect_environment() {
  local env="${APP_ENV:-development}"
 
  case "${HOSTNAME}" in
    *-prod*)  env="production" ;;
    *-stg*)   env="staging" ;;
    *-dev*)   env="development" ;;
  esac
 
  echo "${env}"
}
 
# 加载环境配置
load_environment() {
  local env
  env=$(detect_environment)
 
  local base_config="config/app.conf"
  local env_config="config/environments/${env}.conf"
 
  # 先加载基础配置
  if [ -f "${base_config}" ]; then
    load_config "${base_config}"
  fi
 
  # 再加载环境配置(覆盖基础配置)
  if [ -f "${env_config}" ]; then
    load_config "${env_config}"
  fi
 
  export APP_ENV="${env}"
  log_info "Loaded config for environment: ${env}"
}

第6节:C 配置文件读取对比

Bash vs C 配置解析对比

特性BashC
速度较慢
依赖需要库
复杂度简单较高
类型支持仅字符串多种类型
错误处理简单完善
适用场景脚本部署应用程序

C 语言配置解析示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct {
    char key[256];
    char value[1024];
} ConfigEntry;
 
typedef struct {
    ConfigEntry entries[100];
    int count;
} Config;
 
void parse_config(Config *config, const char *filename) {
    FILE *file = fopen(filename, "r");
    if (!file) return;
 
    char line[2048];
    config->count = 0;
 
    while (fgets(line, sizeof(line), file)) {
        if (line[0] == '#' || line[0] == '\n') continue;
 
        char *eq = strchr(line, '=');
        if (!eq) continue;
 
        *eq = '\0';
        char *key = line;
        char *value = eq + 1;
 
        value[strcspn(value, "\n")] = '\0';
 
        strncpy(config->entries[config->count].key, key, 255);
        strncpy(config->entries[config->count].value, value, 1023);
        config->count++;
    }
 
    fclose(file);
}
 
const char *get_config(Config *config, const char *key) {
    for (int i = 0; i < config->count; i++) {
        if (strcmp(config->entries[i].key, key) == 0) {
            return config->entries[i].value;
        }
    }
    return "";
}

Bash 调用 C 配置程序

# 编译 C 配置解析器
gcc -o config_parser config_parser.c
 
# Bash 调用
PORT=$(./config_parser config.conf "APP_PORT")
HOST=$(./config_parser config.conf "APP_HOST")
echo "Server: ${HOST}:${PORT}"

本节帮助你构建灵活的配置管理系统,实现一处配置、多处运行的目标。