别再重复造轮子了,这个开源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之旅,让命令行工具开发变得简单而高效!
本文档会持续更新,欢迎提出宝贵的意见和建议。
评论区