别再写丑陋的Shell脚本了,dyad如何让Unix命令行工具开发优雅10倍

别再写丑陋的Shell脚本了,dyad如何让Unix命令行工具开发优雅10倍

别再写丑陋的Shell脚本了,dyad如何让Unix命令行工具开发优雅10倍

在Unix/Linux的开发日常中,Shell脚本几乎无处不在。无论是自动化部署、数据处理,还是快速原型开发,Shell都是运维工程师和后端开发者手中最锋利的瑞士军刀。然而,当你真正深入编写复杂脚本时,会发现传统的Bash/Zsh脚本存在大量痛点:参数解析繁琐、错误处理混乱、代码可读性差、调试困难……

今天要介绍的这个开源项目 dyad-sh/dyad,正是为解决这些痛点而生。它是一个现代化的Shell脚本框架,旨在将现代软件工程的最佳实践带入传统的Shell编程世界,让命令行工具的开发变得既优雅又可靠。

如果你受够了写维护成本高、可读性差的Shell脚本,这篇文章将带你从零开始掌握dyad,学会如何用它构建专业级的命令行工具。


为什么dyad值得关注

传统Shell脚本的困境

在深入了解dyad之前,让我们先回顾一下传统Shell脚本开发中常见的几个典型场景:

场景一:参数解析的噩梦

#!/bin/bash
# 传统方式处理命令行参数
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
    echo "Usage: $0 [-n name] [-v] [--config FILE]"
    exit 0
fi

NAME=""
VERBOSE=0
CONFIG="default.conf"

while [ $# -gt 0 ]; do
    case "$1" in
        -n|--name)
            NAME="$2"
            shift 2
            ;;
        -v|--verbose)
            VERBOSE=1
            shift
            ;;
        --config)
            CONFIG="$2"
            shift 2
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
done

这段代码看起来已经够复杂了,但实际项目中参数往往更多,类型检查、默认值、参数校验等需求会让代码膨胀到难以维护。

场景二:错误处理的迷茫

#!/bin/bash
# 传统的错误处理方式
some_command
if [ $? -ne 0 ]; then
    echo "Command failed"
    exit 1
fi

another_command || {
    echo "Another command failed"
    exit 1
}

错误处理散落在各处,状态码管理混乱,难以形成统一的错误报告机制。

场景三:代码组织的混乱

当脚本超过几百行时,没有模块化设计的Shell脚本会变成一团乱麻。函数定义散布各处,全局变量到处可见,修改一个功能可能影响完全不相关的逻辑。

dyad的核心价值

dyad针对上述痛点提供了系统性的解决方案:

声明式的参数定义:告别手动解析,使用简洁的DSL定义参数,dyad自动处理类型转换、默认值、必填检查。

结构化的输出系统:内置日志、进度条、表格输出等功能,让脚本输出既美观又专业。

健壮的错误处理:统一的错误收集与报告机制,支持错误堆栈追踪。

模块化的架构设计:支持脚本拆分、依赖管理、代码复用。

跨Shell兼容性:同时支持Bash和Zsh,部分功能兼容Dash。

更重要的是,dyad采用零外部依赖的设计,所有功能都是纯Shell实现,这意味着你可以轻松地将它集成到任何Unix/Linux环境中,无需担心依赖问题。


环境搭建

前置要求

在开始使用dyad之前,确保你的系统满足以下要求:

支持的Shell环境

  • Bash 4.0 或更高版本
  • Zsh 5.0 或更高版本
  • 部分功能支持Dash(基础工具函数)

基础工具

  • POSIX标准工具(awk、sed、grep等)
  • Git(用于克隆仓库)

安装方式

dyad提供了多种安装方式,你可以根据实际需求选择最适合的方法。

方式一:单文件引入(推荐入门)

这是最简单的方式,直接下载dyad的核心文件到你的项目中:

# 创建项目目录
mkdir my-project && cd my-project

# 下载dyad核心文件
curl -fsSL https://raw.githubusercontent.com/dyad-sh/dyad/main/dyad.sh -o dyad.sh

# 在脚本中引入
cat > my_script.sh << 'EOF'
#!/usr/bin/env bash
# 引入dyad框架
source "$(dirname "$0")/dyad.sh"

# 你的代码
dyad_log info "Hello from dyad!"
EOF

chmod +x my_script.sh
./my_script.sh

方式二:Git子模块(推荐项目管理)

如果你使用Git管理项目,可以通过子模块的方式集成dyad:

# 初始化Git仓库(如果没有)
git init my-project && cd my-project

# 添加dyad作为子模块
git submodule add https://github.com/dyad-sh/dyad.git lib/dyad

# 创建项目脚本
cat > deploy.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail

# 设置脚本目录为项目根目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# 引入dyad
source "$PROJECT_ROOT/lib/dyad/dyad.sh"

# 你的部署逻辑
dyad_log info "Starting deployment..."
EOF

chmod +x deploy.sh

初始化子模块时需要执行:

git submodule update --init --recursive

方式三:系统全局安装

如果你希望在系统级别使用dyad,可以将其安装到系统路径:

# 克隆仓库
git clone https://github.com/dyad-sh/dyad.git /tmp/dyad

# 安装到系统路径(需要sudo权限)
sudo cp -r /tmp/dyad /opt/dyad
sudo ln -s /opt/dyad/dyad.sh /usr/local/bin/dyad

# 验证安装
dyad --version

方式四:使用安装脚本

dyad官方提供了快速安装脚本:

# 默认安装到 ~/.local/share/dyad
curl -fsSL https://get.dyad.sh | bash

# 自定义安装路径
curl -fsSL https://get.dyad.sh | bash -s -- --prefix=/opt/dyad

验证安装

安装完成后,运行以下命令验证dyad是否正常工作:

#!/usr/bin/env bash
source dyad.sh  # 或 /usr/local/bin/dyad

# 检查dyad版本
echo "dyad version: $(dyad_version)"

你应该能看到dyad的版本信息。如果出现错误,检查以下几点:

  • Shell版本是否满足要求(bash –version)
  • 文件权限是否正确(chmod +x)
  • 路径是否设置正确

核心功能详解

dyad的功能可以分为几个主要模块,每个模块解决Shell脚本开发中的特定问题。深入理解这些模块,将帮助你构建出专业级的命令行工具。

模块一:日志系统

日志是任何脚本不可或缺的功能。dyad提供了一个功能完善的日志系统,支持多种日志级别和输出格式。

基本日志输出

#!/usr/bin/env bash
source dyad.sh

# 使用不同级别的日志
dyad_log debug "这是一条调试信息"
dyad_log info "这是一条普通信息"
dyad_log warn "这是一条警告信息"
dyad_log error "这是一条错误信息"
dyad_log success "任务执行成功"
dyad_log step "步骤1:初始化环境"

默认情况下,日志会输出到标准错误(stderr),并带有颜色标记。如果需要输出到文件,可以设置日志路径:

#!/usr/bin/env bash
source dyad.sh

# 设置日志文件
dyad_log_set_file /var/log/myapp.log

# 设置日志级别(只显示info及以上)
dyad_log_set_level info

dyad_log debug "这条消息不会显示"
dyad_log info "这条消息会显示"

日志格式控制

#!/usr/bin/env bash
source dyad.sh

# 自定义日志格式
# %t - 时间戳
# %l - 日志级别
# %m - 消息内容
# %n - 脚本名称
dyad_log_set_format "[%t] [%l] %n: %m"

# 禁用颜色输出(适用于日志文件)
dyad_log_set_color false

带进度条的日志

对于耗时较长的任务,可以使用带进度的日志:

#!/usr/bin/env bash
source dyad.sh

# 显示进度条
for i in {1..100}; do
    dyad_progress $i 100 "Processing files..."
    sleep 0.05
done

echo ""  # 换行,结束进度条输出
dyad_log success "处理完成!"

模块二:参数解析

这是dyad最核心的功能之一。通过声明式的方式定义命令行参数,自动处理解析、类型转换、默认值和验证。

基础参数定义

#!/usr/bin/env bash
source dyad.sh

# 定义参数
dyad_param_add --name name -n "User" required "用户名" \
    --type string --placeholder "请输入用户名"

dyad_param_add --age age -a "18" "年龄" \
    --type integer --min 0 --max 150

dyad_param_add --config config -c "config.json" "配置文件路径" \
    --type file --exists

dyad_param_add --verbose verbose -v false "详细输出" \
    --type boolean

dyad_param_add --files files -f [] "输入文件列表" \
    --type string --nargs +

# 解析命令行参数
dyad_parse "$@"

# 访问解析后的值
echo "用户名: $(dyad_param_get name)"
echo "年龄: $(dyad_param_get age)"
echo "详细输出: $(dyad_param_get verbose)"
echo "文件数量: $(dyad_param_get files | wc -w)"

支持的参数类型

dyad支持多种内置类型,每种类型都有内置的验证逻辑:

#!/usr/bin/env bash
source dyad.sh

# 字符串类型(默认)
dyad_param_add --title title -t "" "标题" --type string

# 整数类型
dyad_param_add --port port -p "8080" "端口号" --type integer

# 浮点数类型
dyad_param_add --threshold threshold -th "0.75" "阈值" --type float

# 布尔类型
dyad_param_add --force force -f false "强制执行" --type boolean

# 文件路径类型(可选验证)
dyad_param_add --input input -i "" "输入文件" --type file
dyad_param_add --output output -o "" "输出文件" --type file --writable

# 目录路径类型
dyad_param_add --dir dir -d "" "工作目录" --type directory --exists

# 枚举类型
dyad_param_add --env env -e "dev" "环境" --type enum --choices dev staging production

# URL类型
dyad_param_add --url url -u "" "服务地址" --type url

参数验证

dyad提供了丰富的验证选项:

#!/usr/bin/env bash
source dyad.sh

# 数值范围验证
dyad_param_add --count count -c "10" "数量" \
    --type integer --min 1 --max 100

# 字符串长度验证
dyad_param_add --username username -u "" "用户名" \
    --type string --min-length 3 --max-length 20

# 正则表达式验证
dyad_param_add --email email -e "" "邮箱" \
    --type string --pattern "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"

# 自定义验证函数
dyad_param_add --date date -d "" "日期" \
    --type string --validate my_date_validator

my_date_validator() {
    local value="$1"
    if [[ "$value" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
        return 0
    fi
    echo "日期格式错误,应为 YYYY-MM-DD"
    return 1
}

自动帮助信息

定义参数后,dyad可以自动生成专业的帮助信息:

#!/usr/bin/env bash
source dyad.sh

dyad_param_add --name name -n "" required "用户名"
dyad_param_add --email email -e "" "邮箱地址" --type email
dyad_param_add --verbose verbose -v false "详细输出" --type boolean

# 生成帮助信息
dyad_usage

# 自定义帮助头部
dyad_usage_set_header "我的工具 v1.0.0"
dyad_usage_set_footer "更多信息请访问 https://example.com"

执行 --help-h 时会自动显示:

用法: mytool [选项]

必填参数:
  -n, --name <STRING>          用户名

可选参数:
  -e, --email <EMAIL>         邮箱地址
  -v, --verbose <BOOLEAN>       详细输出 (默认: false)
  -h, --help                    显示帮助信息

模块三:表格输出

在构建CLI工具时,美观的表格输出能极大提升用户体验:

#!/usr/bin/env bash
source dyad.sh

# 定义表格数据
declare -A headers=(
    ["name"]="姓名"
    ["age"]="年龄"
    ["city"]="城市"
)

declare -a data=(
    "张三|28|北京"
    "李四|35|上海"
    "王五|42|广州"
)

# 创建并输出表格
dyad_table_create "users" headers

for row in "${data[@]}"; do
    IFS='|' read -r name age city <<< "$row"
    dyad_table_add_row "users" "$name" "$age" "$city"
done

dyad_table_render "users"

输出效果:

┌──────────┬──────┬────────┐
│ 姓名     │ 年龄 │ 城市   │
├──────────┼──────┼────────┤
│ 张三     │ 28   │ 北京   │
│ 李四     │ 35   │ 上海   │
│ 王五     │ 42   │ 广州   │
└──────────┴──────┴────────┘

表格样式定制

#!/usr/bin/env bash
source dyad.sh

# 设置表格样式
dyad_table_set_style users bordered  # 可选: simple, bordered, compact, rounded

# 设置列对齐方式
dyad_table_set_align users name left
dyad_table_set_align users age center
dyad_table_set_align users city right

# 设置列宽度
dyad_table_set_width users name 20
dyad_table_set_width users city 15

# 显示/隐藏边框
dyad_table_set_border users true

模块四:文件操作工具

dyad提供了一系列便捷的文件操作函数:

#!/usr/bin/env bash
source dyad.sh

# 安全读取文件
dyad_file_read() {
    local file="$1"
    if [ -f "$file" ]; then
        cat "$file"
    else
        dyad_log error "文件不存在: $file"
        return 1
    fi
}

# 安全写入文件(支持原子操作)
dyad_file_write() {
    local file="$1"
    local content="$2"
    echo "$content" > "$file"
}

# 文件比较(内容对比)
if dyad_file_diff "file1.txt" "file2.txt"; then
    dyad_log info "文件内容相同"
else
    dyad_log warn "文件内容不同"
fi

# 临时文件管理
dyad_tmpfile_create  # 创建临时文件
local tmpfile
tmpfile=$(dyad_tmpfile_create)
echo "临时文件: $tmpfile"

# 使用完后清理
dyad_tmpfile_cleanup "$tmpfile"

# 递归创建目录
dyad_mkdir_p "/path/to/deep/nested/directory"

模块五:进度与任务管理

对于长时间运行的任务,dyad提供了任务管理功能:

#!/usr/bin/env bash
source dyad.sh

# 创建任务
dyad_task_create "deploy" "部署应用到服务器"

# 设置任务步骤
dyad_task_add_step "deploy" "prepare" "准备环境"
dyad_task_add_step "deploy" "build" "构建项目"
dyad_task_add_step "deploy" "upload" "上传文件"
dyad_task_add_step "deploy" "restart" "重启服务"

# 开始执行任务
dyad_task_start "deploy"

dyad_step_start "prepare"
sleep 1
dyad_step_complete "prepare"

dyad_step_start "build"
sleep 2
dyad_step_complete "build"

dyad_step_start "upload"
sleep 1
dyad_step_complete "upload"

dyad_step_start "restart"
sleep 1
dyad_step_complete "restart"

# 标记任务完成
dyad_task_complete "deploy"

模块六:并发与锁机制

在多进程环境下,dyad提供了文件锁功能:

#!/usr/bin/env bash
source dyad.sh

# 尝试获取锁
local lockfile="/tmp/myapp.lock"

if dyad_lock_acquire "$lockfile"; then
    dyad_log info "成功获取锁,开始执行任务"

    # 执行需要独占的操作
    do_exclusive_work()

    # 释放锁
    dyad_lock_release "$lockfile"
else
    dyad_log warn "无法获取锁,另一个实例正在运行"
    exit 1
fi

# 或者使用超时获取锁
if dyad_lock_acquire "$lockfile" --timeout 10; then
    # 成功获取
else
    # 超时
fi

模块七:配置管理

dyad支持多种配置来源,优先级从高到低:

#!/usr/bin/env bash
source dyad.sh

# 加载配置文件(支持多种格式)
dyad_config_load "/etc/myapp.conf"           # INI格式
dyad_config_load "./config.yaml"              # YAML格式
dyad_config_load "$HOME/.myapprc"             # 键值对格式

# 从环境变量加载
dyad_config_from_env "MYAPP_" "database"

# 访问配置值
local db_host
db_host=$(dyad_config_get "database.host" "localhost")
local db_port
db_port=$(dyad_config_get "database.port" "3306")

# 设置默认值
dyad_config_set "app.name" "My Application"
dyad_config_set "app.debug" false

实战教程:构建一个完整的CLI工具

现在让我们通过一个完整的实战项目,将上述功能串联起来。我们将构建一个名为 gitstat 的工具,用于分析Git仓库的统计数据。

项目初始化

首先创建项目结构:

mkdir -p gitstat/lib
cd gitstat

# 引入dyad作为子模块
git init
git submodule add https://github.com/dyad-sh/dyad.git lib/dyad

# 创建主脚本
cat > gitstat.sh << 'GITSTAT_EOF'
#!/usr/bin/env bash
# =============================================================================
# GitStat - Git仓库统计分析工具
# =============================================================================

set -euo pipefail

# 确保脚本可以从任意目录运行
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/dyad/dyad.sh"

# =============================================================================
# 配置
# =============================================================================

readonly VERSION="1.0.0"
readonly APP_NAME="gitstat"

# =============================================================================
# 参数定义
# =============================================================================

# 定义命令行参数
dyad_param_add --repository repo -r "$(pwd)" \
    "Git仓库路径" --type directory --exists

dyad_param_add --author author -a "" \
    "过滤特定作者的提交" --type string

dyad_param_add --since since -s "1970-01-01" \
    "统计起始日期" --type string

dyad_param_add --until until -u "$(date +%Y-%m-%d)" \
    "统计结束日期" --type string

dyad_param_add --output output -o "" \
    "输出格式 (text/json)" --type enum --choices text json --default text

dyad_param_add --verbose verbose -v false \
    "显示详细输出" --type boolean

dyad_param_add --help help -h false \
    "显示帮助信息" --type boolean

# =============================================================================
# 核心函数
# =============================================================================

# 显示工具头部信息
show_header() {
    echo ""
    dyad_log info "═══════════════════════════════════════════════════════════"
    dyad_log info "  GitStat v${VERSION} - Git仓库统计分析工具"
    dyad_log info "═══════════════════════════════════════════════════════════"
    echo ""
}

# 验证仓库
validate_repository() {
    local repo_path="$1"

    if [ ! -d "$repo_path/.git" ]; then
        dyad_log error "目录不是Git仓库: $repo_path"
        return 1
    fi

    dyad_log success "✓ 仓库验证通过: $repo_path"
}

# 获取提交统计
get_commit_stats() {
    local repo_path="$1"
    local author="$2"
    local since="$3"
    local until="$4"

    dyad_log step "正在分析提交记录..."

    cd "$repo_path"

    # 构建git命令参数
    local git_args=("log" "--since=$since" "--until=$until" "--pretty=format:%H|%an|%ae|%ai|%s")

    if [ -n "$author" ]; then
        git_args+=("--author=$author")
        dyad_log info "作者过滤: $author"
    fi

    # 统计数据
    local total_commits
    total_commits=$(git "${git_args[@]}" 2>/dev/null | wc -l)

    local total_additions
    total_additions=$(git "${git_args[@]}" 2>/dev/null | \
        xargs -I {} git show {} --stat --pretty=format: 2>/dev/null | \
        grep -E "^\s+[0-9]+" | \
        awk '{sum += $1} END {print sum}')

    local total_deletions
    total_deletions=$(git "${git_args[@]}" 2>/dev/null | \
        xargs -I {} git show {} --stat --pretty=format: 2>/dev/null | \
        grep -E "^\s+[0-9]+" | \
        awk '{sum += $2} END {print sum}')

    # 输出结果
    echo "total_commits:$total_commits"
    echo "total_additions:$total_additions"
    echo "total_deletions:$total_deletions"
}

# 获取作者排名
get_author_leaderboard() {
    local repo_path="$1"
    local since="$2"
    local until="$3"

    dyad_log step "正在生成作者排名..."

    cd "$repo_path"

    local temp_file
    temp_file=$(mktemp)

    git log --since="$since" --until="$until" --pretty=format:"%an" 2>/dev/null | \
        sort | uniq -c | sort -rn > "$temp_file"

    # 定义表格
    declare -A headers=(
        ["rank"]="排名"
        ["author"]="作者"
        ["commits"]="提交数"
        ["percentage"]="占比"
    )

    dyad_table_create "authors" headers
    dyad_table_set_align "authors" rank center
    dyad_table_set_align "authors" author left
    dyad_table_set_align "authors" commits center
    dyad_table_set_align "authors" percentage right

    local total_commits
    total_commits=$(git log --since="$since" --until="$until" --pretty=format:"%an" 2>/dev/null | wc -l)

    local rank=1
    while IFS= read -r line; do
        local count
        local author
        count=$(echo "$line" | awk '{print $1}')
        author=$(echo "$line" | awk '{print $2}')

        local percentage
        percentage=$(echo "scale=1; $count * 100 / $total_commits" | bc)

        dyad_table_add_row "authors" "$rank" "$author" "$count" "${percentage}%"

        rank=$((rank + 1))
    done < "$temp_file"

    rm -f "$temp_file"

    dyad_log success "找到 $total_commits 条提交记录"
}

# 获取文件类型统计
get_file_type_stats() {
    local repo_path="$1"
    local since="$2"
    local until="$3"

    dyad_log step "正在统计文件类型..."

    cd "$repo_path"

    local temp_file
    temp_file=$(mktemp)

    git log --since="$since" --until="$until" --name-only --pretty=format:"" 2>/dev/null | \
        grep -E "\.[a-zA-Z0-9]+$" | \
        sed 's/.*\.//' | \
        sort | \
        uniq -c | \
        sort -rn > "$temp_file"

    # 定义表格
    declare -A headers=(
        ["extension"]="扩展名"
        ["count"]="文件数"
        ["trend"]="趋势"
    )

    dyad_table_create "filetypes" headers

    local top_extensions
    top_extensions=$(head -10 "$temp_file")

    while IFS= read -r line; do
        local count
        local ext
        count=$(echo "$line" | awk '{print $1}')
        ext=$(echo "$line" | awk '{print $2}')

        # 根据数量判断趋势
        local trend="○"
        if [ "$count" -gt 100 ]; then
            trend="●"
        elif [ "$count" -gt 50 ]; then
            trend="◐"
        elif [ "$count" -gt 20 ]; then
            trend="◑"
        fi

        dyad_table_add_row "filetypes" ".$ext" "$count" "$trend"
    done <<< "$top_extensions"

    rm -f "$temp_file"
}

# 获取提交时间分布
get_commit_timeline() {
    local repo_path="$1"
    local since="$2"
    local until="$3"

    dyad_log step "正在分析提交时间分布..."

    cd "$repo_path"

    local temp_file
    temp_file=$(mktemp)

    git log --since="$since" --until="$until" --pretty=format:"%ai" 2>/dev/null | \
        cut -d' ' -f1 | \
        sort | \
        uniq -c | \
        sort -k2 > "$temp_file"

    # 定义表格
    declare -A headers=(
        ["date"]="日期"
        ["commits"]="提交数"
        ["bar"]="趋势"
    )

    dyad_table_create "timeline" headers

    local max_count=0
    while IFS= read -r line; do
        local count
        count=$(echo "$line" | awk '{print $1}')
        if [ "$count" -gt "$max_count" ]; then
            max_count="$count"
        fi
    done < "$temp_file"

    while IFS= read -r line; do
        local count
        local date
        count=$(echo "$line" | awk '{print $1}')
        date=$(echo "$line" | awk '{print $2}')

        # 生成简单的条形图
        local bar_length=$((count * 50 / max_count))
        local bar
        bar=$(printf "%${bar_length}s" | tr ' ' '█')

        dyad_table_add_row "timeline" "$date" "$count" "$bar"
    done < "$temp_file"

    rm -f "$temp_file"
}

# JSON格式输出
output_json() {
    local repo_path="$1"
    local since="$2"
    local until="$3"
    local author="$4"

    cd "$repo_path"

    local git_args=("log" "--since=$since" "--until=$until" "--pretty=format:%H|%an|%ae|%ai|%s")

    if [ -n "$author" ]; then
        git_args+=("--author=$author")
    fi

    echo "{"
    echo "  \"repository\": \"$repo_path\","
    echo "  \"date_range\": {"
    echo "    \"since\": \"$since\","
    echo "    \"until\": \"$until\""
    echo "  },"

    local total_commits
    total_commits=$(git "${git_args[@]}" 2>/dev/null | wc -l)
    echo "  \"total_commits\": $total_commits,"

    # 作者统计
    echo "  \"authors\": ["
    git log --since="$since" --until="$until" --pretty=format:"%an" 2>/dev/null | \
        sort | uniq -c | sort -rn | head -5 | \
        while IFS= read -r line; do
            local count
            local author_name
            count=$(echo "$line" | awk '{print $1}')
            author_name=$(echo "$line" | awk '{print $2}')
            echo "    {\"name\": \"$author_name\", \"commits\": $count},"
        done
    echo "  ]"

    echo "}"
}

# 主函数
main() {
    # 解析命令行参数
    if ! dyad_parse "$@"; then
        dyad_usage
        exit 1
    fi

    # 显示帮助
    if [ "$(dyad_param_get help)" = "true" ]; then
        dyad_usage_set_header "GitStat v${VERSION} - Git仓库统计分析工具"
        dyad_usage_set_footer "示例: gitstat -r /path/to/repo -a \"John\" -s 2024-01-01"
        dyad_usage
        exit 0
    fi

    # 获取参数值
    local repo_path
    repo_path=$(dyad_param_get repository)
    local author
    author=$(dyad_param_get author)
    local since
    since=$(dyad_param_get since)
    local until
    until=$(dyad_param_get until)
    local output_format
    output_format=$(dyad_param_get output)

    # 显示头部
    show_header

    # 设置日志级别
    if [ "$(dyad_param_get verbose)" = "true" ]; then
        dyad_log_set_level debug
    else
        dyad_log_set_level info
    fi

    # 验证仓库
    if ! validate_repository "$repo_path"; then
        exit 1
    fi

    dyad_log info "统计时间范围: $since 至 $until"
    echo ""

    if [ "$output_format" = "json" ]; then
        output_json "$repo_path" "$since" "$until" "$author"
    else
        # 获取提交统计
        get_commit_stats "$repo_path" "$author" "$since" "$until"
        echo ""

        # 获取作者排名
        get_author_leaderboard "$repo_path" "$since" "$until"
        dyad_table_render "authors"
        echo ""

        # 获取文件类型统计
        get_file_type_stats "$repo_path" "$since" "$until"
        dyad_table_render "filetypes"
        echo ""

        # 获取时间分布
        get_commit_timeline "$repo_path" "$since" "$until"
        dyad_table_render "timeline"
        echo ""
    fi

    dyad_log success "分析完成!"
}

# 执行主函数
main "$@"
GITSTAT_EOF

chmod +x gitstat.sh

使用示例

创建测试仓库并运行工具:

# 创建测试Git仓库
mkdir -p /tmp/test-repo
cd /tmp/test-repo
git init

# 创建一些测试提交
echo "initial" > README.md
git add README.md
git commit -m "Initial commit"

for i in {1..5}; do
    echo "content $i" >> README.md
    git add .
    git commit -m "Update $i" --date="$(date -d "$i days ago" +%Y-%m-%d)"
done

# 回到项目目录
cd -

# 运行gitstat
./gitstat.sh -r /tmp/test-repo -s "2024-01-01"

# 显示帮助
./gitstat.sh --help

# JSON格式输出
./gitstat.sh -r /tmp/test-repo --output json

常见使用场景

dyad的设计考虑到了实际开发中的各种场景。以下是几个典型的应用示例:

场景一:自动化部署脚本

#!/usr/bin/env bash
source dyad.sh

# =============================================================================
# 自动化部署脚本
# =============================================================================

dyad_param_add --environment env -e "staging" required \
    "部署环境" --type enum --choices dev staging production

dyad_param_add --version version -v "" \
    "部署版本(留空使用最新)" --type string

dyad_param_add --skip-tests skip-tests -s false \
    "跳过测试" --type boolean

dyad_task_create "deploy" "应用部署流程"

main() {
    if ! dyad_parse "$@"; then
        dyad_usage
        exit 1
    fi

    local env
    env=$(dyad_param_get environment)
    local version
    version=$(dyad_param_get version)

    dyad_log info "开始部署到 $env 环境..."

    # 部署逻辑...
}

main "$@"

场景二:数据处理管道

#!/usr/bin/env bash
source dyad.sh

# =============================================================================
# 数据处理管道
# =============================================================================

dyad_param_add --input input -i "" required \
    "输入文件" --type file --exists

dyad_param_add --output output -o "" required \
    "输出文件" --type file

dyad_param_add --filter filter -f "" \
    "过滤条件" --type string

dyad_param_add --parallel parallel -p 4 \
    "并行任务数" --type integer --min 1 --max 16

process_batch() {
    local batch="$1"
    local output="$2"

    # 处理逻辑
    echo "$batch" | processing_command >> "$output"
}

main() {
    dyad_parse "$@"

    local input
    input=$(dyad_param_get input)
    local output
    output=$(dyad_param_get output)

    dyad_log step "开始数据处理..."

    # 分批处理
    split -l 1000 "$input" /tmp/batch_

    # ...并行处理...

    dyad_log success "处理完成: $output"
}

main "$@"

场景三:系统健康检查工具

#!/usr/bin/env bash
source dyad.sh

check_disk_usage() {
    local threshold="${1:-90}"
    local usage
    usage=$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')

    if [ "$usage" -gt "$threshold" ]; then
        dyad_log error "磁盘使用率过高: ${usage}%"
        return 1
    fi

    dyad_log success "磁盘使用率正常: ${usage}%"
    return 0
}

check_memory() {
    local available
    available=$(free -m | grep Mem | awk '{print $7}')

    if [ "$available" -lt 512 ]; then
        dyad_log warn "可用内存不足: ${available}MB"
        return 1
    fi

    dyad_log success "内存充足: ${available}MB"
    return 0
}

check_services() {
    local services=("nginx" "mysql" "redis")

    for service in "${services[@]}"; do
        if systemctl is-active --quiet "$service" 2>/dev/null; then
            dyad_log success "服务运行正常: $service"
        else
            dyad_log error "服务异常: $service"
        fi
    done
}

main() {
    dyad_log info "开始系统健康检查..."

    check_disk_usage 80
    check_memory
    check_services

    dyad_log info "健康检查完成"
}

main "$@"

最佳实践与技巧

脚本结构规范

遵循良好的脚本结构可以大大提高可维护性:

#!/usr/bin/env bash
# =============================================================================
# 脚本名称:example.sh
# 功能描述:简要描述脚本功能
# 使用方式:./example.sh [参数]
# 作者信息:作者 <email@example.com>
# 创建日期:2024-01-01
# 版本信息:1.0.0
# =============================================================================

set -euo pipefail  # 严格模式

# =============================================================================
# 变量定义(常量大写,变量小写)
# =============================================================================

readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_DIR="/var/log"
readonly CONFIG_DIR="/etc/${SCRIPT_NAME}"

# =============================================================================
# 依赖检查
# =============================================================================

check_dependencies() {
    local deps=("curl" "jq" "git")
    local missing=()

    for dep in "${deps[@]}"; do
        if ! command -v "$dep" &>/dev/null; then
            missing+=("$dep")
        fi
    done

    if [ ${#missing[@]} -gt 0 ]; then
        dyad_log error "缺少依赖: ${missing[*]}"
        return 1
    fi

    return 0
}

# =============================================================================
# 主函数
# =============================================================================

main() {
    # 前置检查
    check_dependencies || exit 1

    # 主逻辑
    # ...
}

# =============================================================================
# 入口点
# =============================================================================

main "$@"

错误处理策略

采用分级错误处理策略:

#!/usr/bin/env bash
source dyad.sh

# 设置错误处理回调
dyad_error_set_handler() {
    local exit_code=$?
    local line_num=$1

    dyad_log error "脚本在第 ${line_num} 行以退出码 ${exit_code} 退出"

    # 清理临时文件
    [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"

    # 发送告警通知(可选)
    # send_alert "Script failed at line $line_num"

    exit "$exit_code"
}

# 启用错误追踪
trap 'dyad_error_set_handler $LINENO' ERR

# 使用try-catch风格的错误处理
safe_execute() {
    local description="$1"
    shift
    local cmd="$@"

    dyad_log step "$description..."

    if eval "$cmd"; then
        dyad_log success "$description - 完成"
        return 0
    else
        dyad_log error "$description - 失败"
        return 1
    fi
}

# 使用示例
main() {
    safe_execute "下载配置文件" curl -fsSL "$CONFIG_URL" -o config.json
    safe_execute "验证配置" jq empty config.json
    safe_execute "启动服务" systemctl start myservice
}

性能优化建议

对于大型脚本,注意性能优化:

#!/usr/bin/env bash
source dyad.sh

# =============================================================================
# 性能优化技巧
# =============================================================================

# 技巧1:使用局部变量
# 不推荐:
process_items_slow() {
    for item in "${items[@]}"; do
        echo "$item"  # 每次都创建子进程
    done
}

# 推荐:
process_items_fast() {
    local item
    for item in "${items[@]}"; do
        echo "$item"  # 局部变量避免全局查找
    done
}

# 技巧2:避免子shell
# 不推荐:
count=$(echo "$large_string" | grep -c "pattern")

# 推荐:
count=$(printf '%s\n' "$large_string" | grep -c "pattern")

# 技巧3:使用内联操作代替管道
# 不推荐:
result=$(echo "$input" | sed 's/a/b/g' | awk '{print $1}')

# 推荐:
result=$(awk '{gsub(/a/, "b"); print $1}' <<< "$input")

# 技巧4:批量处理减少循环开销
# 不推荐:
for file in *.txt; do
    process_file "$file"
done

# 推荐:
find . -name "*.txt" -exec process_file {} +

调试技巧

#!/usr/bin/env bash
source dyad.sh

# 启用调试模式
DEBUG=${DEBUG:-0}

debug() {
    if [ "$DEBUG" = "1" ]; then
        dyad_log debug "$*"
    fi
}

# 追踪变量变化
trace_var() {
    local var_name="$1"
    debug "TRACE: ${var_name}=${!var_name}"
}

# 性能计时
time_it() {
    local start_time
    start_time=$(date +%s.%N)

    "$@"

    local end_time
    end_time=$(date +%s.%N)
    local elapsed
    elapsed=$(echo "$end_time - $start_time" | bc)

    dyad_log info "执行时间: ${elapsed}秒"
}

# 使用示例
main() {
    DEBUG=1 time_it heavy_operation
}

总结与资源链接

通过这篇教程,我们系统地学习了 dyad-sh/dyad 这个现代化的Shell脚本框架。从环境搭建到核心功能,从基础用法到实战项目,你应该已经掌握了使用dyad构建专业级命令行工具的核心技能。

dyad的核心价值在于:将现代软件工程的最佳实践引入Shell脚本开发,让原本混乱的脚本变得结构清晰、可维护性强、功能完善。它特别适合以下场景:

  • 需要频繁使用的运维脚本
  • 对外提供服务的命令行工具
  • 团队共享的自动化脚本
  • 需要良好用户体验的工具

相关资源链接

  • GitHub仓库:https://github.com/dyad-sh/dyad
  • 官方文档:https://dyad.sh/docs
  • 问题反馈:https://github.com/dyad-sh/dyad/issues
  • 社区讨论:https://github.com/dyad-sh/dyad/discussions

推荐学习的相关项目

  • Bash Boilerplate:另一个优秀的Bash脚本模板项目,提供了许多实用的脚本模式
  • Google Shell Style Guide:Google发布的Shell编码规范,有助于编写更专业的脚本
  • ShellCheck:静态分析工具,可以帮助你发现脚本中的问题
  • httpie:现代的命令行HTTP客户端,展示了如何构建优秀的CLI工具

如果你觉得dyad对你有帮助,欢迎在GitHub上给项目一个Star,也可以参与贡献代码或文档。开源社区的进步离不开每个人的参与。

最后,动手实践是最好的学习方式。建议你选择一个自己日常工作中的痛点,尝试用dyad来重写那些曾经让你头疼的脚本。相信你会很快感受到dyad带来的效率提升。

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注