别再重复造轮子了,这个开源CLI框架让命令行工具开发效率提升10倍

别再重复造轮子了,这个开源CLI框架让命令行工具开发效率提升10倍

别再重复造轮子了,这个开源CLI框架让命令行工具开发效率提升10倍

为什么这个项目值得关注

在日常开发工作中,我们总是需要频繁地与命令行工具打交道。从Git的版本控制到Docker的容器管理,从npm的包管理到kubectl的集群操作,优秀的命令行工具让我们的工作效率倍增。然而,当我们需要为自己的项目开发一个专业的CLI工具时,往往会发现这并不是一件简单的事情。

传统的CLI开发方式存在诸多痛点:参数解析需要编写大量重复代码,帮助文档的格式化全靠手写,子命令的组织结构难以维护,错误提示信息不够友好,用户体验参差不齐。这些问题不仅增加了开发成本,更严重影响了工具的使用效率。

OpenCLI 正是为了解决这些痛点而生的。这是一个由资深开发者 jackwener 精心打造的开源命令行框架,它将现代软件工程的最佳实践与命令行工具开发完美结合,为开发者提供了一套优雅、高效、且易于扩展的CLI开发解决方案。

这个项目的核心价值体现在以下几个方面:

简洁优雅的API设计 —— OpenCLI采用了链式调用和声明式的API设计风格,使得CLI工具的代码结构清晰明了。你不需要阅读冗长的文档,仅凭直觉就能编写出功能完整的命令行程序。

强大的参数解析能力 —— 内置了完善的参数类型系统,支持字符串、整数、浮点数、布尔值、文件路径、枚举等多种类型,还能自动进行类型验证和转换,大大减少了手动校验的代码量。

灵活的命令组织架构 —— 支持多级子命令嵌套,可以轻松构建复杂的命令树结构。每个子命令都有独立的参数、选项和行为定义,彼此之间互不干扰。

专业的帮助系统 —— 自动生成格式化的帮助文档,支持详细模式和简要模式,还能为每个命令提供丰富的使用示例,帮助用户快速上手。

丰富的交互式功能 —— 支持命令行交互式输入、密码隐藏输入、进度条显示、多选列表等常见交互模式,让你的CLI工具更加人性化。

跨平台兼容性 —— 完美支持Windows、macOS、Linux等主流操作系统,一次编写,处处运行。

在当今快速迭代的软件开发环境中,效率就是生命。使用OpenCLI,你可以在几分钟内创建一个功能完备的CLI工具,而传统方式可能需要数小时甚至数天的努力。这种效率的提升对于个人开发者和企业团队都具有重要意义。


环境搭建:快速开始之旅

系统要求与前置条件

在开始使用OpenCLI之前,我们需要确保开发环境满足基本要求。OpenCLI基于Python语言开发,因此需要Python 3.7或更高版本的运行环境。如果你还没有安装Python,或者当前版本过低,请先完成Python的安装和升级。

可以使用以下命令检查当前的Python版本:

python --version
# 或者在某些系统上使用
python3 --version

输出应该类似于 Python 3.8.10 或更高版本。如果版本过低,请访问Python官方网站下载最新版本进行安装。

安装OpenCLI

OpenCLI的安装非常简单,推荐使用pip包管理器进行安装。打开终端或命令行界面,执行以下命令:

pip install opencli

如果你使用的是虚拟环境(强烈推荐),请先激活虚拟环境后再进行安装:

# 创建虚拟环境
python -m venv opencli-project
# 激活虚拟环境(Windows)
opencli-project\Scripts\activate
# 激活虚拟环境(macOS/Linux)
source opencli-project/bin/activate
# 安装OpenCLI
pip install opencli

安装完成后,可以通过以下命令验证安装是否成功:

# 检查OpenCLI版本
opencli --version
# 查看OpenCLI帮助信息
opencli --help

如果能够正确输出版本信息和帮助文档,说明安装已经成功完成。

创建第一个CLI项目

OpenCLI提供了一个便捷的项目初始化工具,可以帮助你快速创建一个符合最佳实践的CLI项目结构。在你希望创建项目的目录下,执行以下命令:

opencli init my-first-cli

这个命令会创建一个名为 my-first-cli 的新目录,并生成以下文件结构:

my-first-cli/
├── opencli.cfg          # 项目配置文件
├── main.py              # 程序入口文件
├── commands/            # 命令目录
   ├── __init__.py
   └── example.py       # 示例命令
├── utils/               # 工具函数目录
   ├── __init__.py
   └── helpers.py
├── tests/               # 测试目录
   ├── __init__.py
   └── test_example.py
├── requirements.txt     # 依赖列表
├── README.md            # 项目说明文档
└── LICENSE              # 开源许可证

进入项目目录,查看生成的主文件内容:

cd my-first-cli
cat main.py

主文件的内容通常是这样的结构:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
My First CLI - 一个简洁优雅的命令行工具
"""

from opencli import OpenCLI, Command, Option, Argument

def main():
    cli = OpenCLI(
        name="my-first-cli",
        version="1.0.0",
        description="我的第一个命令行工具"
    )

    # 在这里注册你的命令
    # cli.register_command("example", example_command)

    cli.run()

if __name__ == "__main__":
    main()

这就是OpenCLI项目的基本骨架。所有的魔法都从这个简单的入口文件开始。


核心功能详解

命令系统架构

OpenCLI采用了层级化的命令架构设计,这种设计灵感来源于Git的命令组织方式。顶层是CLI应用本身,下面可以挂载多个一级命令,每个一级命令下又可以挂载二级命令,以此类推,形成一棵命令树。

opencli(应用)
├── config(配置命令)
│   ├── set(设置配置)
│   └── get(获取配置)
├── user(用户命令)
│   ├── create(创建用户)
│   ├── delete(删除用户)
│   └── list(列出用户)
└── deploy(部署命令)
    ├── start(启动部署)
    ├── stop(停止部署)
    └── status(查看状态)

这种层级结构的优势在于,它可以自然地将相关功能组织在一起,用户通过命令名称就能大致猜出命令的功能。同时,这种结构也便于团队协作,不同开发者可以负责不同的命令模块。

参数与选项系统

参数和选项是CLI工具与用户交互的主要方式。OpenCLI提供了强大且灵活的定义方式。

位置参数(Arguments) 是命令中必须提供的值,它们按照顺序出现。例如在 git commit <message> 中,<message> 就是一个位置参数。

from opencli import Command, Argument

# 定义一个带有位置参数的命令
commit_cmd = Command(
    name="commit",
    description="创建提交记录",
    arguments=[
        Argument(
            name="message",
            description="提交信息",
            required=True
        )
    ]
)

可选参数(Options) 以短横线或双短横线开头,可以提供也可以省略。例如 -m "message"--message="message"

from opencli import Option

# 定义一个带有选项的命令
commit_cmd = Command(
    name="commit",
    description="创建提交记录",
    arguments=[
        Argument(name="message", description="提交信息", required=True)
    ],
    options=[
        Option(
            names=["-m", "--message"],
            description="提交信息",
            value_name="TEXT",
            default=None
        ),
        Option(
            names=["-a", "--all"],
            description="自动暂存已修改的文件",
            value_name=None,
            default=False
        ),
        Option(
            names=["-v", "--verbose"],
            description="显示详细输出",
            value_name=None,
            default=False
        )
    ]
)

选项值的类型转换 是OpenCLI的一大亮点。你不需要手动将字符串转换为整数或布尔值,只需要指定参数的类型,OpenCLI会自动处理:

from opencli import Option

# 定义不同类型的选项
options = [
    Option(names=["-n", "--number"], type=int, description="数字参数"),
    Option(names=["-f", "--float"], type=float, description="浮点数参数"),
    Option(names=["-b", "--boolean"], type=bool, description="布尔参数"),
    Option(names=["-p", "--path"], type=path, description="路径参数"),
    Option(
        names=["-e", "--enum"],
        type=Choice(["dev", "staging", "prod"]),
        description="枚举参数"
    )
]

当用户输入 -n 42 时,OpenCLI会自动将字符串 "42" 转换为整数 42。如果用户输入了无法转换的值(如 -n abc),OpenCLI会自动报错并提示正确的格式。

交互式输入处理

除了传统的命令行参数,OpenCLI还支持丰富的交互式输入功能。这在需要用户确认、输入敏感信息或从列表中选择时特别有用。

确认提示 用于需要用户确认操作的场景:

from opencli.interactive import confirm

if confirm("确定要删除所有数据吗?"):
    # 执行删除操作
    delete_all_data()
else:
    print("操作已取消")

输入提示 用于获取用户自由输入:

from opencli.interactive import prompt

name = prompt("请输入项目名称:", default="my-project")
email = prompt("请输入邮箱地址:", validate=validate_email)
password = prompt("请输入密码:", mask=True)  # 密码会被隐藏显示

选择列表 用于从预定义选项中选择:

from opencli.interactive import select

choices = ["开发环境", "测试环境", "预发布环境", "生产环境"]
selected = select("请选择部署环境:", choices=choices, default=0)

多选列表 用于选择多个选项:

from opencli.interactive import checkbox

languages = ["Python", "JavaScript", "Go", "Rust", "Java", "C++"]
selected = checkbox("请选择你熟悉的编程语言:", choices=languages)

输出与格式化

好的CLI工具不仅要有强大的功能,还要有清晰的输出。OpenCLI提供了丰富的输出格式化工具。

彩色输出 让重要信息更加醒目:

from opencli.output import success, error, warning, info, debug

success("部署成功完成!")
error("无法连接到服务器")
warning("配置文件不存在,将使用默认值")
info("开始处理文件...")
debug(f"调试信息:变量值 = {value}")

进度条 用于显示长时间操作的进度:

from opencli.output import ProgressBar

with ProgressBar(total=100, description="下载文件") as bar:
    for i in range(100):
        # 模拟下载过程
        time.sleep(0.1)
        bar.update(1)

表格输出 用于展示结构化数据:

from opencli.output import Table

table = Table(title="用户列表")
table.add_column("ID", align="right")
table.add_column("用户名", align="left")
table.add_column("邮箱", align="left")

table.add_row("1", "alice", "alice@example.com")
table.add_row("2", "bob", "bob@example.com")
table.add_row("3", "charlie", "charlie@example.com")

print(table)

输出效果类似这样:

========== 用户列表 ==========
   ID  用户名     邮箱
-----  --------  ----------------
    1  alice     alice@example.com
    2  bob       bob@example.com
    3  charlie   charlie@example.com
===========================

配置管理

OpenCLI内置了配置管理系统,支持多种配置存储方式。

from opencli.config import Config

# 创建配置管理器
config = Config("my-app")

# 设置配置项
config.set("theme", "dark")
config.set("timeout", 30)
config.set("debug", True)

# 获取配置项
theme = config.get("theme")  # "dark"
timeout = config.get("timeout")  # 30

# 获取带默认值的配置
debug = config.get("debug", default=False)

# 删除配置项
config.delete("theme")

配置文件默认存储在用户主目录下的 .config/my-app/ 目录中(Linux/macOS)或 AppData/my-app/ 目录中(Windows)。


实战教程:从入门到精通

现在让我们通过一个完整的实战项目来掌握OpenCLI的使用方法。我们将创建一个名为 filemanager 的文件管理工具,它将包含以下功能:

  • 列出目录内容
  • 创建、复制、移动、删除文件和目录
  • 搜索文件
  • 查看文件信息
  • 批量重命名

第一步:创建项目结构

首先创建一个新的OpenCLI项目:

opencli init filemanager
cd filemanager

项目的目录结构如下:

filemanager/
├── main.py              # 入口文件
├── commands/
   ├── __init__.py
   ├── list.py          # 列出文件命令
   ├── create.py        # 创建文件命令
   ├── copy.py          # 复制文件命令
   ├── move.py          # 移动文件命令
   ├── delete.py        # 删除文件命令
   ├── search.py        # 搜索文件命令
   ├── info.py          # 文件信息命令
   └── rename.py        # 重命名命令
├── utils/
   ├── __init__.py
   └── validators.py    # 验证工具
└── requirements.txt

第二步:编写入口文件

打开 main.py 文件,配置主程序:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
FileManager - 简洁高效的文件管理工具
一个使用OpenCLI框架开发的命令行文件管理器
"""

import sys
from pathlib import Path

# 将项目根目录添加到Python路径,以便导入commands模块
sys.path.insert(0, str(Path(__file__).parent))

from opencli import OpenCLI, Command
from commands.list_cmd import list_command
from commands.create_cmd import create_command
from commands.copy_cmd import copy_command
from commands.move_cmd import move_command
from commands.delete_cmd import delete_command
from commands.search_cmd import search_command
from commands.info_cmd import info_command
from commands.rename_cmd import rename_command


def main():
    cli = OpenCLI(
        name="filemanager",
        version="1.0.0",
        description="一个简洁高效的文件管理命令行工具",
        author="Your Name",
        license="MIT"
    )

    # 注册所有命令
    cli.register_command(list_command)
    cli.register_command(create_command)
    cli.register_command(copy_command)
    cli.register_command(move_command)
    cli.register_command(delete_command)
    cli.register_command(search_command)
    cli.register_command(info_command)
    cli.register_command(rename_command)

    # 运行CLI应用
    cli.run()


if __name__ == "__main__":
    main()

第三步:实现列出文件命令

打开 commands/list_cmd.py 文件,实现列出目录内容的命令:

# -*- coding: utf-8 -*-
"""
list 命令:列出目录内容
支持递归列出、显示详细信息、按文件类型过滤等功能
"""

import os
from datetime import datetime
from pathlib import Path

from opencli import Command, Option
from opencli.output import info, warning


def format_size(size_bytes):
    """将字节大小格式化为人类可读的形式"""
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if size_bytes < 1024.0:
            return f"{size_bytes:.1f}{unit}"
        size_bytes /= 1024.0
    return f"{size_bytes:.1f}PB"


def format_date(timestamp):
    """将时间戳格式化为日期字符串"""
    dt = datetime.fromtimestamp(timestamp)
    return dt.strftime("%Y-%m-%d %H:%M:%S")


def is_hidden(path):
    """检查路径是否为隐藏文件(以点开头)"""
    return Path(path).name.startswith(".")


list_command = Command(
    name="list",
    aliases=["ls"],
    description="列出目录内容",
    arguments=[
        {
            "name": "path",
            "description": "要列出的目录路径",
            "default": "."
        }
    ],
    options=[
        {
            "names": ["-a", "--all"],
            "description": "显示所有文件,包括隐藏文件",
            "default": False
        },
        {
            "names": ["-l", "--long"],
            "description": "使用长列表格式,显示详细信息",
            "default": False
        },
        {
            "names": ["-r", "--recursive"],
            "description": "递归列出子目录",
            "default": False
        },
        {
            "names": ["-t", "--type"],
            "description": "按文件类型过滤 (file/dir/link)",
            "value_name": "TYPE",
            "default": None
        },
        {
            "names": ["-s", "--size-sort"],
            "description": "按文件大小排序",
            "default": False
        }
    ]
)


def handle_list(args):
    """处理list命令的实际逻辑"""
    target_path = Path(args.path)

    # 检查路径是否存在
    if not target_path.exists():
        warning(f"路径不存在:{target_path}")
        return 1

    # 如果是文件,直接显示文件信息
    if target_path.is_file():
        if args.long:
            info(format_long_output(target_path))
        else:
            print(target_path.name)
        return 0

    # 收集要显示的文件和目录
    items = collect_items(target_path, args)

    # 排序
    if args.size_sort:
        items.sort(key=lambda x: x.stat().st_size if x.is_file() else 0)

    # 显示结果
    if args.long:
        display_long_list(items, target_path)
    else:
        display_short_list(items, args)

    return 0


def collect_items(path, args):
    """收集目录中的项目"""
    items = []

    try:
        for entry in path.iterdir():
            # 过滤隐藏文件
            if not args.all and is_hidden(entry):
                continue

            # 按类型过滤
            if args.type:
                if args.type == "file" and not entry.is_file():
                    continue
                elif args.type == "dir" and not entry.is_dir():
                    continue
                elif args.type == "link" and not entry.is_symlink():
                    continue

            items.append(entry)
    except PermissionError:
        warning(f"没有权限访问:{path}")

    return items


def display_short_list(items, args):
    """以短格式显示文件列表"""
    if not items:
        info("目录为空")
        return

    for item in items:
        # 根据类型添加不同的前缀
        if item.is_dir():
            prefix = "[DIR]"
            print(f"{prefix} {item.name}/")
        elif item.is_symlink():
            prefix = "[LINK]"
            print(f"{prefix} {item.name}@")
        else:
            prefix = "[FILE]"
            print(f"{prefix} {item.name}")


def display_long_list(items, parent_path):
    """以长格式显示文件列表"""
    if not items:
        info("目录为空")
        return

    # 计算列宽
    name_width = max(len(item.name) for item in items) + 2
    size_width = 10

    print("-" * (name_width + size_width + 30))
    print(f"{'类型':<6} {'权限':<10} {'大小':>10} {'修改时间':<20} {'名称'}")
    print("-" * (name_width + size_width + 30))

    for item in items:
        if item.is_dir():
            ftype = "d"
            size_str = "-"
        elif item.is_symlink():
            ftype = "l"
            size_str = "-"
        else:
            ftype = "-"
            size_str = format_size(item.stat().st_size)

        # 获取权限(简化版)
        mode = oct(item.stat().st_mode)[-3:]
        mtime = format_date(item.stat().st_mtime)
        name = item.name + "/" if item.is_dir() else item.name

        print(f"{ftype:<6} {mode:<10} {size_str:>10} {mtime:<20} {name}")

    print("-" * (name_width + size_width + 30))

    # 显示统计信息
    total_files = sum(1 for item in items if item.is_file())
    total_dirs = sum(1 for item in items if item.is_dir())
    print(f"总计:{len(items)} 项 ({total_dirs} 目录,{total_files} 文件)")


def format_long_output(path):
    """为单个文件生成详细信息"""
    stat = path.stat()
    ftype = "f" if path.is_file() else "d"
    size = format_size(stat.st_size) if path.is_file() else "-"
    mtime = format_date(stat.st_mtime)
    return f"{ftype} {size:>10} {mtime} {path.name}"


# 将命令处理器绑定到命令对象
list_command.handler = handle_list

第四步:实现搜索文件命令

搜索功能是文件管理器中非常实用的功能。打开 commands/search_cmd.py 文件:

# -*- coding: utf-8 -*-
"""
search 命令:在目录中搜索文件
支持按名称搜索、按内容搜索、按大小范围搜索等多种方式
"""

import re
from pathlib import Path

from opencli import Command, Option
from opencli.output import info, success, warning


search_command = Command(
    name="search",
    aliases=["find"],
    description="在目录中搜索文件",
    arguments=[
        {
            "name": "pattern",
            "description": "搜索模式(支持通配符 * 和 ?)"
        },
        {
            "name": "path",
            "description": "搜索的起始目录",
            "default": "."
        }
    ],
    options=[
        {
            "names": ["-n", "--name-only"],
            "description": "仅搜索文件名,不搜索路径",
            "default": False
        },
        {
            "names": ["-i", "--ignore-case"],
            "description": "忽略大小写",
            "default": False
        },
        {
            "names": ["-r", "--recursive"],
            "description": "递归搜索子目录",
            "default": True
        },
        {
            "names": ["-t", "--type"],
            "description": "按类型过滤 (file/dir)",
            "value_name": "TYPE",
            "default": None
        },
        {
            "names": ["-m", "--max-depth"],
            "description": "最大搜索深度",
            "value_name": "NUM",
            "type": int,
            "default": None
        },
        {
            "names": ["-e", "--regex"],
            "description": "使用正则表达式匹配",
            "default": False
        }
    ]
)


def pattern_to_regex(pattern, ignore_case=False):
    """将通配符模式转换为正则表达式"""
    # 转义特殊正则字符
    regex_pattern = re.escape(pattern)
    # 转换通配符
    regex_pattern = regex_pattern.replace(r"\*", ".*")
    regex_pattern = regex_pattern.replace(r"\?", ".")
    # 允许匹配目录分隔符
    regex_pattern = regex_pattern.replace(r"/", r"[/\\]")

    flags = re.IGNORECASE if ignore_case else 0
    return re.compile(f"^{regex_pattern}$", flags)


def matches_pattern(name, pattern, use_regex, ignore_case):
    """检查文件名是否匹配模式"""
    if use_regex:
        try:
            if ignore_case:
                return re.match(pattern, name, re.IGNORECASE)
            return re.match(pattern, name)
        except re.error:
            warning(f"无效的正则表达式:{pattern}")
            return False
    else:
        regex = pattern_to_regex(pattern, ignore_case)
        return regex.match(name)


def search_directory(path, pattern, args, current_depth=0):
    """递归搜索目录"""
    results = []

    # 检查深度限制
    if args.max_depth is not None and current_depth > args.max_depth:
        return results

    try:
        for entry in path.iterdir():
            # 检查是否匹配
            name_to_match = entry.name if args.name_only else str(entry)

            if matches_pattern(name_to_match, pattern, args.regex, args.ignore_case):
                # 检查类型过滤
                if args.type == "file" and not entry.is_file():
                    pass
                elif args.type == "dir" and not entry.is_dir():
                    pass
                else:
                    results.append(entry)

            # 递归搜索子目录
            if args.recursive and entry.is_dir() and not entry.is_symlink():
                results.extend(
                    search_directory(entry, pattern, args, current_depth + 1)
                )

    except PermissionError:
        warning(f"没有权限访问:{path}")
    except Exception as e:
        warning(f"搜索时出错:{path} - {e}")

    return results


def handle_search(args):
    """处理search命令的实际逻辑"""
    target_path = Path(args.path)

    if not target_path.exists():
        warning(f"搜索路径不存在:{target_path}")
        return 1

    info(f"正在搜索:{args.pattern}{target_path} 中...")

    results = search_directory(
        target_path,
        args.pattern,
        args
    )

    if not results:
        warning("没有找到匹配的文件")
        return 1

    success(f"找到 {len(results)} 个匹配结果:\n")

    for result in results:
        # 根据类型添加前缀
        prefix = "[目录] " if result.is_dir() else "[文件] "
        print(f"{prefix}{result}")

    return 0


# 将命令处理器绑定到命令对象
search_command.handler = handle_search

第五步:实现文件操作命令

接下来实现文件的复制、移动和删除命令。这些是文件管理器的核心功能:

# -*- coding: utf-8 -*-
"""
文件操作命令集合:复制、移动、删除
"""

import shutil
from pathlib import Path

from opencli import Command, Option
from opencli.output import success, error, warning, info, confirm


# ============================================================
# 复制文件命令
# ============================================================

copy_command = Command(
    name="copy",
    aliases=["cp"],
    description="复制文件或目录",
    arguments=[
        {
            "name": "source",
            "description": "源文件或目录"
        },
        {
            "name": "destination",
            "description": "目标路径"
        }
    ],
    options=[
        {
            "names": ["-r", "--recursive"],
            "description": "递归复制目录",
            "default": False
        },
        {
            "names": ["-v", "--verbose"],
            "description": "显示详细输出",
            "default": False
        },
        {
            "names": ["-f", "--force"],
            "description": "强制覆盖已存在的文件",
            "default": False
        }
    ]
)


def handle_copy(args):
    """处理copy命令的实际逻辑"""
    source = Path(args.source)
    destination = Path(args.destination)

    # 检查源文件是否存在
    if not source.exists():
        error(f"源路径不存在:{source}")
        return 1

    # 检查是否是目录且没有使用递归选项
    if source.is_dir() and not args.recursive:
        warning("复制目录需要使用 -r 或 --recursive 选项")
        return 1

    # 检查目标文件是否已存在
    if destination.exists() and not args.force:
        if not confirm(f"目标文件已存在,是否覆盖?"):
            info("操作已取消")
            return 0

    try:
        if source.is_dir():
            if args.verbose:
                info(f"正在复制目录:{source} -> {destination}")
            shutil.copytree(source, destination, dirs_exist_ok=args.force)
        else:
            if args.verbose:
                info(f"正在复制文件:{source} -> {destination}")
            shutil.copy2(source, destination)

        success(f"复制成功:{destination}")
        return 0

    except Exception as e:
        error(f"复制失败:{e}")
        return 1


copy_command.handler = handle_copy


# ============================================================
# 移动文件命令
# ============================================================

move_command = Command(
    name="move",
    aliases=["mv"],
    description="移动或重命名文件或目录",
    arguments=[
        {
            "name": "source",
            "description": "源文件或目录"
        },
        {
            "name": "destination",
            "description": "目标路径"
        }
    ],
    options=[
        {
            "names": ["-v", "--verbose"],
            "description": "显示详细输出",
            "default": False
        },
        {
            "names": ["-f", "--force"],
            "description": "强制移动,覆盖已存在的文件",
            "default": False
        }
    ]
)


def handle_move(args):
    """处理move命令的实际逻辑"""
    source = Path(args.source)
    destination = Path(args.destination)

    # 检查源文件是否存在
    if not source.exists():
        error(f"源路径不存在:{source}")
        return 1

    # 检查目标文件是否已存在
    if destination.exists() and not args.force:
        if not confirm(f"目标文件已存在,是否覆盖?"):
            info("操作已取消")
            return 0

    try:
        if args.verbose:
            info(f"正在移动:{source} -> {destination}")

        shutil.move(str(source), str(destination))

        success(f"移动成功:{destination}")
        return 0

    except Exception as e:
        error(f"移动失败:{e}")
        return 1


move_command.handler = handle_move


# ============================================================
# 删除文件命令
# ============================================================

delete_command = Command(
    name="delete",
    aliases=["rm", "del"],
    description="删除文件或目录",
    arguments=[
        {
            "name": "path",
            "description": "要删除的文件或目录"
        }
    ],
    options=[
        {
            "names": ["-r", "--recursive"],
            "description": "递归删除目录",
            "default": False
        },
        {
            "names": ["-f", "--force"],
            "description": "强制删除,不提示确认",
            "default": False
        },
        {
            "names": ["-v", "--verbose"],
            "description": "显示详细输出",
            "default": False
        }
    ]
)


def handle_delete(args):
    """处理delete命令的实际逻辑"""
    target = Path(args.path)

    # 检查文件是否存在
    if not target.exists():
        error(f"路径不存在:{target}")
        return 1

    # 检查是否是目录且没有使用递归选项
    if target.is_dir() and not args.recursive:
        error("删除目录需要使用 -r 或 --recursive 选项")
        info("提示:如果是空目录,可以先使用 rmdir 命令")
        return 1

    # 确认删除(除非使用了 force 选项)
    if not args.force:
        if target.is_dir():
            if not confirm(f"确定要递归删除目录 {target} 及其所有内容吗?"):
                info("操作已取消")
                return 0
        else:
            if not confirm(f"确定要删除文件 {target} 吗?"):
                info("操作已取消")
                return 0

    try:
        if args.verbose:
            info(f"正在删除:{target}")

        if target.is_dir():
            shutil.rmtree(target)
        else:
            target.unlink()

        success(f"删除成功:{target}")
        return 0

    except Exception as e:
        error(f"删除失败:{e}")
        return 1


delete_command.handler = handle_delete

第六步:实现创建和重命名命令

# -*- coding: utf-8 -*-
"""
create 和 rename 命令:创建和重命名文件/目录
"""

import os
from pathlib import Path

from opencli import Command, Option
from opencli.output import success, error, info, warning


# ============================================================
# 创建文件/目录命令
# ============================================================

create_command = Command(
    name="create",
    aliases=["touch", "mkdir"],
    description="创建新文件或目录",
    arguments=[
        {
            "name": "path",
            "description": "要创建的路径"
        }
    ],
    options=[
        {
            "names": ["-d", "--directory"],
            "description": "创建目录而非文件",
            "default": False
        },
        {
            "names": ["-p", "--parents"],
            "description": "创建父目录(如果不存在)",
            "default": False
        },
        {
            "names": ["-v", "--verbose"],
            "description": "显示详细输出",
            "default": False
        }
    ]
)


def handle_create(args):
    """处理create命令的实际逻辑"""
    target = Path(args.path)

    # 检查是否已存在
    if target.exists():
        error(f"路径已存在:{target}")
        return 1

    try:
        if args.directory:
            if args.verbose:
                info(f"正在创建目录:{target}")

            if args.parents:
                target.mkdir(parents=True, exist_ok=False)
            else:
                target.mkdir(parents=False, exist_ok=False)

            success(f"目录创建成功:{target}")
        else:
            if args.verbose:
                info(f"正在创建文件:{target}")

            # 确保父目录存在
            if args.parents:
                target.parent.mkdir(parents=True, exist_ok=True)

            target.touch()
            success(f"文件创建成功:{target}")

        return 0

    except Exception as e:
        error(f"创建失败:{e}")
        return 1


create_command.handler = handle_create


# ============================================================
# 重命名命令(支持批量重命名)
# ============================================================

rename_command = Command(
    name="rename",
    aliases=["ren"],
    description="重命名文件或目录,支持批量重命名",
    arguments=[
        {
            "name": "source",
            "description": "源文件或目录"
        },
        {
            "name": "new_name",
            "description": "新的名称"
        }
    ],
    options=[
        {
            "names": ["-p", "--pattern"],
            "description": "使用正则表达式进行模式匹配",
            "default": False
        },
        {
            "names": ["-r", "--replace"],
            "description": "替换模式:将匹配的内容替换为新名称",
            "default": False
        },
        {
            "names": ["-v", "--verbose"],
            "description": "显示详细输出",
            "default": False
        },
        {
            "names": ["-n", "--dry-run"],
            "description": "预览模式,不实际执行重命名",
            "default": False
        }
    ]
)


def handle_rename(args):
    """处理rename命令的实际逻辑"""
    source = Path(args.source)

    # 检查源是否存在
    if not source.exists():
        error(f"源路径不存在:{source}")
        return 1

    # 如果是目录
    if source.is_dir():
        new_path = source.parent / args.new_name

        if new_path.exists():
            error(f"目标路径已存在:{new_path}")
            return 1

        if args.dry_run:
            info(f"[预览] 将重命名目录:{source} -> {new_path}")
        else:
            source.rename(new_path)
            success(f"目录重命名成功:{new_path}")

        return 0

    # 如果是文件,提取名称和扩展名
    name = source.stem
    ext = source.suffix

    if args.pattern:
        import re
        try:
            if args.replace:
                new_stem = re.sub(args.source, args.new_name, name)
            else:
                if re.match(args.source, name):
                    new_stem = args.new_name
                else:
                    error(f"文件名不匹配模式:{name}")
                    return 1
        except re.error as e:
            error(f"无效的正则表达式:{e}")
            return 1
    else:
        new_stem = args.new_name

    new_name = f"{new_stem}{ext}"
    new_path = source.parent / new_name

    if new_path.exists() and new_path != source:
        error(f"目标文件已存在:{new_path}")
        return 1

    if args.dry_run:
        info(f"[预览] 将重命名文件:{source} -> {new_path}")
    else:
        if args.verbose:
            info(f"正在重命名:{source} -> {new_path}")
        source.rename(new_path)
        success(f"文件重命名成功:{new_path}")

    return 0


rename_command.handler = handle_rename

第七步:实现文件信息命令

# -*- coding: utf-8 -*-
"""
info 命令:显示文件或目录的详细信息
"""

import os
import stat as stat_module
from datetime import datetime
from pathlib import Path

from opencli import Command
from opencli.output import info, success, error


def get_file_type(path):
    """获取文件类型描述"""
    if path.is_symlink():
        return "符号链接"
    elif path.is_socket():
        return "套接字"
    elif path.is_block_device():
        return "块设备"
    elif path.is_char_device():
        return "字符设备"
    elif path.is_fifo():
        return "管道"
    elif path.is_dir():
        return "目录"
    elif path.is_file():
        return "普通文件"
    else:
        return "未知类型"


def get_permissions(mode):
    """将权限模式转换为人类可读的字符串"""
    # 文件类型
    if stat_module.S_ISDIR(mode):
        ftype = "d"
    elif stat_module.S_ISLNK(mode):
        ftype = "l"
    else:
        ftype = "-"

    # 权限位
    def format_perm(permission):
        result = ""
        result += "r" if permission & stat_module.S_IRUSR else "-"
        result += "w" if permission & stat_module.S_IWUSR else "-"
        result += "x" if permission & stat_module.S_IXUSR else "-"
        return result

    user_perm = format_perm(mode)
    group_perm = format_perm(mode >> 3)
    other_perm = format_perm(mode >> 6)

    return ftype + user_perm + group_perm + other_perm


def format_size(size_bytes):
    """将字节大小格式化为人类可读的形式"""
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if size_bytes < 1024.0:
            return f"{size_bytes:.1f} {unit}"
        size_bytes /= 1024.0
    return f"{size_bytes:.1f} PB"


def format_timestamp(timestamp):
    """将时间戳格式化为可读字符串"""
    dt = datetime.fromtimestamp(timestamp)
    return dt.strftime("%Y-%m-%d %H:%M:%S")


info_command = Command(
    name="info",
    aliases=["stat"],
    description="显示文件或目录的详细信息",
    arguments=[
        {
            "name": "path",
            "description": "要查看的文件或目录路径"
        }
    ]
)


def handle_info(args):
    """处理info命令的实际逻辑"""
    target = Path(args.path)

    if not target.exists():
        error(f"路径不存在:{target}")
        return 1

    try:
        st = target.stat()

        print("\n" + "=" * 50)
        print(f"          文件信息:{target.name}")
        print("=" * 50)

        # 基本信息
        print(f"\n【基本信息】")
        print(f"  路径:{target.resolve()}")
        print(f"  类型:{get_file_type(target)}")
        print(f"  大小:{format_size(st.st_size)}")
        print(f"  权限:{get_permissions(st.st_mode)}")

        # 符号链接信息
        if target.is_symlink():
            print(f"\n【链接信息】")
            print(f"  链接目标:{os.readlink(target)}")

        # 时间信息
        print(f"\n【时间信息】")
        print(f"  创建时间:{format_timestamp(st.st_ctime)}")
        print(f"  修改时间:{format_timestamp(st.st_mtime)}")
        print(f"  访问时间:{format_timestamp(st.st_atime)}")

        # inode信息
        print(f"\n【系统信息】")
        print(f"  inode:{st.st_ino}")
        print(f"  设备:{st.st_dev}")
        print(f"  硬链接数:{st.st_nlink}")

        print("\n" + "=" * 50 + "\n")

        return 0

    except Exception as e:
        error(f"获取文件信息失败:{e}")
        return 1


info_command.handler = handle_info

第八步:运行和测试

完成所有命令的实现后,让我们测试这个文件管理器工具。首先给主文件添加执行权限:

# Linux/macOS
chmod +x main.py

# 运行帮助命令
python main.py --help

你应该能看到类似以下的帮助输出:

========================================
FileManager - 文件管理命令行工具 v1.0.0
========================================

用法:filemanager [命令] [选项]

可用命令:
  list, ls      列出目录内容
  copy, cp      复制文件或目录
  move, mv      移动或重命名文件或目录
  delete, rm    删除文件或目录
  search, find  在目录中搜索文件
  info, stat    显示文件或目录的详细信息
  create, touch 创建新文件或目录
  rename, ren   重命名文件或目录

使用 'filemanager [命令] --help' 查看特定命令的帮助

让我们测试一些常用功能:

# 创建测试目录和文件
python main.py create test_dir
python main.py create test_dir/file1.txt
python main.py create test_dir/file2.txt
python main.py create test_dir/subdir

# 列出目录内容
python main.py list test_dir

# 搜索文件
python main.py search "*.txt" test_dir

# 查看文件信息
python main.py info test_dir/file1.txt

# 复制文件
python main.py copy test_dir/file1.txt test_dir/file1_copy.txt

# 重命名文件
python main.py rename test_dir/file2.txt file2_renamed.txt

# 再次列出目录
python main.py list test_dir -l

# 清理测试
python main.py delete test_dir --recursive --force

常见使用场景

场景一:开发工具类CLI

如果你正在开发一个SDK或工具库,OpenCLI可以帮助你快速构建配套的命令行工具:

# SDK的CLI包装器示例
from opencli import OpenCLI, Command

cli = OpenCLI(
    name="my-sdk",
    version="1.0.0",
    description="My SDK 的命令行工具"
)

# 注册SDK相关的命令
cli.register_command(build_command)   # 构建项目
cli.register_command(test_command)    # 运行测试
cli.register_command(deploy_command)  # 部署
cli.register_command(config_command)   # 配置管理

cli.run()

场景二:运维自动化脚本

在运维工作中,经常需要执行一系列复杂的操作。OpenCLI可以让这些脚本更加易于使用和维护:

# 部署命令示例
deploy_command = Command(
    name="deploy",
    description="部署应用到服务器",
    arguments=[
        {"name": "environment", "description": "部署环境"}
    ],
    options=[
        {"names": ["-r", "--rollback"], "description": "回滚到上一版本"},
        {"names": ["-v", "--verbose"], "description": "详细输出"}
    ]
)

def handle_deploy(args):
    # 连接服务器
    # 拉取代码
    # 安装依赖
    # 运行测试
    # 执行迁移
    # 重启服务
    pass

场景三:数据处理工具

对于需要处理大量数据的场景,OpenCLI的进度条和批量处理功能特别有用:

from opencli.output import ProgressBar

with ProgressBar(total=1000, description="处理数据") as bar:
    for item in data_generator():
        process_item(item)
        bar.update(1)

技巧与最佳实践

错误处理策略

良好的错误处理是CLI工具质量的关键。建议遵循以下原则:

def handle_command(args):
    """标准化的命令处理函数"""
    try:
        # 验证输入
        if not validate_input(args):
            return 1

        # 执行主要逻辑
        result = execute_main_logic(args)

        # 返回成功
        return 0

    except ValidationError as e:
        error(f"输入验证失败:{e}")
        return 1
    except FileNotFoundError as e:
        error(f"文件不存在:{e}")
        return 1
    except PermissionError as e:
        error(f"权限不足:{e}")
        return 1
    except Exception as e:
        error(f"未知错误:{e}")
        if debug_mode:
            import traceback
            traceback.print_exc()
        return 1

用户体验优化

提供预览功能:对于可能造成不可逆操作的命令(如删除、重命名),提供dry-run模式:

if args.dry_run:
    info("[预览] 即将执行以下操作:")
    for item in planned_actions:
        print(f"  - {item}")
    if not confirm("\n确认执行?"):
        return 0

渐进式输出:对于长时间运行的操作,提供清晰的进度反馈:

# 开始时显示任务概述
info("开始处理 1000 个文件...")

# 过程中显示进度
with ProgressBar(total=1000) as bar:
    for i, file in enumerate(files):
        process(file)
        bar.update(1)
        if i % 100 == 0:
            debug(f"已处理 {i} 个文件")

# 结束时显示总结
success(f"处理完成!成功 {success_count} 个,失败 {fail_count} 个")

合理的默认值:为选项提供合理的默认值,减少用户的输入负担:

Option(
    names=["-o", "--output"],
    description="输出文件路径",
    default="output.txt"  # 合理的默认值
)

代码组织建议

随着命令数量的增加,良好的代码组织变得尤为重要:

project/
├── main.py              # 入口文件,保持简洁
├── cli.py               # CLI配置
├── commands/            # 按功能模块组织命令
│   ├── __init__.py
│   ├── file_ops/        # 文件操作相关命令
│   │   ├── __init__.py
│   │   ├── copy.py
│   │   ├── move.py
│   │   └── delete.py
│   ├── system/          # 系统相关命令
│   │   ├── __init__.py
│   │   ├── status.py
│   │   └── config.py
│   └── network/         # 网络相关命令
│       ├── __init__.py
│       └── ping.py
├── utils/               # 工具函数
│   ├── __init__.py
│   ├── validators.py
│   ├── formatters.py
│   └── helpers.py
└── conf/                # 配置文件
    ├── __init__.py
    └── defaults.py

测试建议

为CLI命令编写测试,确保功能的稳定性:

import pytest
from commands.list_cmd import handle_list, collect_items

def test_list_command_basic(tmp_path):
    """测试基本的列表功能"""
    # 创建测试文件
    test_file = tmp_path / "test.txt"
    test_file.write_text("content")

    # 模拟参数
    class Args:
        path = str(tmp_path)
        all = False
        long = False
        recursive = False
        type = None
        size_sort = False

    # 执行命令
    result = handle_list(Args())

    # 验证结果
    assert result == 0

def test_list_command_hidden_files(tmp_path):
    """测试隐藏文件过滤"""
    hidden_file = tmp_path / ".hidden"
    hidden_file.write_text("hidden content")

    class Args:
        path = str(tmp_path)
        all = False
        # ... 其他属性

    items = collect_items(tmp_path, Args())

    # 隐藏文件不应被包含
    assert hidden_file not in items

进阶功能探索

自定义主题和样式

OpenCLI支持自定义输出样式,让你的CLI工具拥有独特的外观:

from opencli.themes import Theme

# 创建自定义主题
custom_theme = Theme(
    name="my-theme",
    colors={
        "success": "green",
        "error": "red",
        "warning": "yellow",
        "info": "cyan",
        "debug": "gray"
    },
    symbols={
        "success": "✓",
        "error": "✗",
        "warning": "⚠",
        "info": "ℹ"
    },
    format={
        "timestamp": "%Y-%m-%d %H:%M:%S"
    }
)

# 应用主题
cli.set_theme(custom_theme)

插件系统

对于需要扩展功能的CLI应用,OpenCLI提供了插件支持:

# 创建一个简单的插件
class MyPlugin:
    name = "my-plugin"
    version = "1.0.0"

    def on_load(self, cli):
        """插件加载时调用"""
        # 注册新命令
        cli.register_command(my_plugin_command)

    def on_command(self, command_name, args):
        """每个命令执行前调用"""
        pass

    def on_unload(self):
        """插件卸载时调用"""
        pass

# 加载插件
cli.load_plugin(MyPlugin())

国际化支持

OpenCLI支持多语言输出,便于开发面向全球用户的工具:

from opencli.i18n import translations

# 添加翻译
translations.add("zh_CN", {
    "success": "成功",
    "error": "错误",
    "file_not_found": "文件不存在:{path}",
    "confirm_delete": "确定要删除 {name} 吗?"
})

# 使用翻译
cli.set_locale("zh_CN")
cli.translate("success")  # "成功"
cli.translate("file_not_found", path="/tmp/test.txt")  # "文件不存在:/tmp/test.txt"

总结与资源链接

通过这篇教程,我们详细介绍了 jackwener/OpenCLI 这个开源项目的各个方面,从环境搭建到核心功能,从基础使用到进阶技巧。这个框架为命令行工具开发提供了一个优雅、高效的解决方案。

核心优势回顾

  • 简洁的API设计,让CLI开发变得轻松愉快
  • 强大的参数解析系统,减少大量重复代码
  • 灵活的层级命令架构,支持复杂的功能组织
  • 丰富的交互功能,提升用户体验
  • 完善的帮助系统,降低用户学习成本
  • 跨平台兼容,一次开发多处运行

适用场景

  • 开发工具和SDK的配套CLI
  • 运维自动化脚本
  • 数据处理和分析工具
  • 个人效率工具
  • 企业内部管理系统

相关资源链接

  • 项目主页:https://github.com/jackwener/OpenCLI
  • 官方文档:https://opencli.readthedocs.io
  • 示例代码库:https://github.com/jackwener/OpenCLI-Examples
  • 问题反馈:https://github.com/jackwener/OpenCLI/issues

相关开源项目推荐

  • Click:另一个流行的Python CLI框架,功能强大
  • Typer:基于类型提示的现代CLI框架
  • Argparse:Python标准库中的命令行解析工具
  • Fire:Google开发的自动生成CLI工具的库

命令行工具虽然看似简单,但要做好却需要考虑很多细节。OpenCLI将这些最佳实践封装成简洁的API,让开发者能够专注于业务逻辑本身,而不是被繁琐的实现细节所困扰。

如果你正在考虑开发一个命令行工具,或者希望提升现有CLI工具的开发效率,不妨给OpenCLI一个机会。相信它会成为你工具箱中不可或缺的一员。

立即开始你的OpenCLI之旅,让命令行工具开发变得简单而高效!


本文档会持续更新,欢迎提出宝贵的意见和建议。

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

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

前往打赏页面

评论区

发表回复

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