前言
在日常开发中,我们经常需要创建一些小工具来提高工作效率。如果能将这些工具打包成可以通过 pip install 安装的命令行工具,就能在任何地方轻松使用。本文将详细介绍如何从零开始创建一个专业的Python命令行工具,并通过pip进行安装和分发。
项目概述
我们将创建一个名为 MyTasks 的任务管理命令行工具,它具备以下功能:
- ✅ 添加、完成、删除任务
- 🔍 搜索任务
- 📊 统计信息
- 🎯 优先级管理
- 💾 本地JSON存储
- 🌈 彩色输出
最终效果:
# 本地开发安装
pip install -e .
# 使用工具
mytasks add "学习Python" -d "完成CLI教程" -p high
mytasks list
mytasks complete 1
重要说明: 由于 mytasks 可能与PyPI上的现有包名冲突,本教程重点演示本地开发和安装流程。如需发布到PyPI,请选择唯一的包名。
第一步:项目结构设计
创建项目目录结构:
my_cli/
├── demo.py # 主程序文件
├── setup.py # 传统安装配置
├── pyproject.toml # 现代安装配置
├── README.md # 项目说明
├── LICENSE # 许可证
├── requirements.txt # 依赖列表
└── demo_installation.py # 演示脚本
第二步:核心代码实现
2.1 主程序文件 (demo.py)
首先创建核心功能模块:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
命令行工具演示 - 使用 pip 安装的 CLI 应用
"""
import argparse
import sys
import os
import json
from pathlib import Path
from typing import List, Dict, Any
# 解决Windows下的Unicode显示问题
if sys.platform == "win32":
import codecs
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach())
class TaskManager:
"""任务管理器核心类"""
def __init__(self, data_file: str = None):
self.data_file = data_file or os.path.expanduser("~/.tasks.json")
self.tasks = self._load_tasks()
def _load_tasks(self) -> List[Dict[str, Any]]:
"""从JSON文件加载任务数据"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return []
return []
def _save_tasks(self):
"""保存任务数据到JSON文件"""
try:
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.tasks, f, ensure_ascii=False, indent=2)
except IOError as e:
print(f"❌ 保存失败: {e}")
sys.exit(1)
def add_task(self, title: str, description: str = "", priority: str = "medium"):
"""添加新任务"""
task = {
"id": len(self.tasks) + 1,
"title": title,
"description": description,
"priority": priority,
"completed": False,
"created_at": self._get_timestamp()
}
self.tasks.append(task)
self._save_tasks()
print(f"✅ 任务已添加: {title}")
def list_tasks(self, show_completed: bool = False):
"""列出任务"""
if not self.tasks:
print("📝 暂无任务")
return
print("\n📋 任务列表:")
print("-" * 60)
for task in self.tasks:
if not show_completed and task["completed"]:
continue
status = "✅" if task["completed"] else "⏳"
priority_icon = self._get_priority_icon(task["priority"])
print(f"{status} [{task['id']}] {priority_icon} {task['title']}")
if task["description"]:
print(f" 📝 {task['description']}")
print(f" 🕒 {task['created_at']}")
print()
def complete_task(self, task_id: int):
"""标记任务为已完成"""
for task in self.tasks:
if task["id"] == task_id:
task["completed"] = True
task["completed_at"] = self._get_timestamp()
self._save_tasks()
print(f"✅ 任务已完成: {task['title']}")
return
print(f"❌ 未找到任务 ID: {task_id}")
def _get_priority_icon(self, priority: str) -> str:
"""获取优先级对应的图标"""
icons = {
"high": "🔴",
"medium": "🟡",
"low": "🟢"
}
return icons.get(priority, "⚪")
def _get_timestamp(self) -> str:
"""获取当前时间戳"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2.2 命令行参数解析
使用 argparse 创建专业的命令行界面:
def create_parser() -> argparse.ArgumentParser:
"""创建命令行参数解析器"""
parser = argparse.ArgumentParser(
prog="mytasks",
description="🚀 简单易用的任务管理命令行工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
mytasks add "学习Python" -d "完成Django教程" -p high
mytasks list
mytasks complete 1
mytasks search "Python"
"""
)
# 版本信息
parser.add_argument("--version", action="version", version="%(prog)s 1.0.0")
# 自定义数据文件
parser.add_argument("--data-file", help="指定任务数据文件路径")
# 子命令
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# add 命令
add_parser = subparsers.add_parser("add", help="添加新任务")
add_parser.add_argument("title", help="任务标题")
add_parser.add_argument("-d", "--description", help="任务描述", default="")
add_parser.add_argument("-p", "--priority",
choices=["high", "medium", "low"],
default="medium", help="任务优先级")
# list 命令
list_parser = subparsers.add_parser("list", help="列出任务")
list_parser.add_argument("-a", "--all", action="store_true",
help="显示包括已完成的所有任务")
# complete 命令
complete_parser = subparsers.add_parser("complete", help="完成任务")
complete_parser.add_argument("task_id", type=int, help="任务ID")
return parser
2.3 主函数入口
def main():
"""主函数 - 命令行入口点"""
parser = create_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
# 创建任务管理器
task_manager = TaskManager(args.data_file)
try:
if args.command == "add":
task_manager.add_task(args.title, args.description, args.priority)
elif args.command == "list":
task_manager.list_tasks(show_completed=args.all)
elif args.command == "complete":
task_manager.complete_task(args.task_id)
except KeyboardInterrupt:
print("\n👋 再见!")
sys.exit(0)
except Exception as e:
print(f"❌ 发生错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
第三步:配置文件编写
3.1 setup.py 配置
setup.py 是传统的包配置方式,仍被广泛使用:
#!/usr/bin/env python3
"""
setup.py - 用于 pip 安装的配置文件
"""
from setuptools import setup, find_packages
import os
def read_readme():
with open("README.md", "r", encoding="utf-8") as f:
return f.read()
setup(
# 基本信息
name="mytasks",
version="1.0.0",
description="🚀 简单易用的命令行任务管理工具",
long_description=read_readme() if os.path.exists("README.md") else "",
long_description_content_type="text/markdown",
# 作者信息
author="Your Name",
author_email="your.email@example.com",
url="https://github.com/yourname/mytasks",
# 项目分类
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Utilities",
"Environment :: Console",
],
# 包配置
py_modules=["demo"], # 单文件模块
python_requires=">=3.7",
install_requires=[], # 外部依赖
# 开发依赖
extras_require={
"dev": [
"pytest>=6.0",
"black>=21.0",
"flake8>=3.8",
],
},
# 🔑 关键配置:命令行入口点
entry_points={
"console_scripts": [
"mytasks=demo:main", # 命令名=模块:函数
"task=demo:main", # 提供简短别名
],
},
# 包含额外文件
include_package_data=True,
package_data={"": ["*.md", "*.txt"]},
)
关键说明:
entry_points是最重要的配置,它定义了安装后可以在命令行使用的命令"mytasks=demo:main"表示:命令mytasks对应demo.py文件中的main函数- 可以定义多个别名,如
task作为mytasks的简短版本
3.2 现代配置方式 (pyproject.toml)
pyproject.toml 是Python项目的现代标准配置格式:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mytasks"
version = "1.0.0"
description = "🚀 简单易用的命令行任务管理工具"
readme = "README.md"
license = {text = "MIT"}
authors = [{name = "Your Name", email = "your.email@example.com"}]
requires-python = ">=3.7"
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=6.0", "black>=21.0", "flake8>=3.8"]
[project.scripts]
mytasks = "demo:main"
task = "demo:main"
[project.urls]
Homepage = "https://github.com/yourname/mytasks"
Repository = "https://github.com/yourname/mytasks"
[tool.setuptools]
py-modules = ["demo"]
第四步:项目文档和许可证
4.1 README.md
# MyTasks - 命令行任务管理工具
🚀 一个简单易用的命令行任务管理工具
## 安装
```bash
pip install mytasks
使用方法
# 添加任务
mytasks add "学习Python" -d "完成教程" -p high
# 列出任务
mytasks list
# 完成任务
mytasks complete 1
# 查看帮助
mytasks --help
功能特性
- ✅ 任务增删改查
- 🎯 优先级管理
- 📊 统计信息
- 💾 本地存储
### 4.2 LICENSE
MIT License
Copyright (c) 2025 Your Name
Permission is hereby granted, free of charge, to any person obtaining a copy...
### 4.3 requirements.txt
生产依赖
本项目暂无外部依赖
开发依赖在 setup.py 的 extras_require 中定义
## 第五步:安装和测试
### 5.1 本地开发安装
```bash
# 进入项目目录
cd my_cli
# 开发模式安装(推荐)
pip install -e .
# 普通安装
pip install .
开发模式安装的优势:
- 代码修改后无需重新安装
- 便于调试和开发
- 使用
-e参数(editable)
重要提醒:包名冲突问题
在实际开发中,你可能会遇到包名冲突的问题:
# 卸载本地包后尝试从PyPI安装
$ pip uninstall mytasks
$ pip install mytasks
ERROR: Could not find a version that satisfies the requirement mytasks
这是因为:
- PyPI上没有该包:你的包只是本地开发,未发布到PyPI
- 包名被占用:常见名称可能已被其他开发者使用
- 名称规范:PyPI要求包名唯一且符合命名规范
解决方案:
# 1. 继续使用本地安装(开发阶段推荐)
pip install -e .
# 2. 或者使用构建的包文件安装
pip install dist/mytasks-1.0.0-py3-none-any.whl
# 3. 发布前检查包名是否可用
python -c "import requests; print('可用' if requests.get('https://pypi.org/simple/your-unique-name/').status_code == 404 else '已占用')"
5.2 验证安装
# 检查版本
mytasks --version
# 查看帮助
mytasks --help
# 测试功能
mytasks add "测试任务" -p high
mytasks list
5.3 不同安装方式对比
| 安装方式 | 命令 | 适用场景 | 优缺点 |
|---|---|---|---|
| 开发模式 | pip install -e . | 本地开发 | ✅ 修改后无需重装 ❌ 依赖源码目录 |
| 标准安装 | pip install . | 本地测试 | ✅ 独立安装 ❌ 修改需重装 |
| Wheel安装 | pip install dist/*.whl | 分发测试 | ✅ 快速安装 ❌ 需先构建 |
| PyPI安装 | pip install mytasks | 生产使用 | ✅ 简单方便 ❌ 需发布到PyPI |
实际演示对比:
# 场景1:开发阶段 - 推荐开发模式
cd my_cli
pip install -e .
# 修改代码后直接测试,无需重新安装
# 场景2:测试分发包
python -m build
pip install dist/mytasks-1.0.0-py3-none-any.whl
# 测试打包后的效果
# 场景3:卸载和重装
pip uninstall mytasks
pip install -e . # 重新安装开发版本
# 场景4:检查安装状态
pip list | grep mytasks
pip show mytasks
5.4 功能测试
创建测试脚本 demo_installation.py:
#!/usr/bin/env python3
"""安装和使用演示脚本"""
import subprocess
import sys
def run_command(cmd, description):
"""运行命令并显示结果"""
print(f"\n🔧 {description}")
print(f"💻 命令: {cmd}")
print("-" * 50)
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.stdout:
print("📤 输出:")
print(result.stdout)
if result.returncode != 0:
print(f"❌ 命令执行失败,返回码: {result.returncode}")
else:
print("✅ 命令执行成功")
except Exception as e:
print(f"❌ 执行异常: {e}")
def main():
"""演示完整流程"""
print("🚀 MyTasks CLI 工具演示")
# 安装
run_command("pip install -e .", "安装工具")
# 功能演示
run_command('mytasks add "学习CLI开发" -p high', "添加任务")
run_command("mytasks list", "列出任务")
run_command("mytasks complete 1", "完成任务")
if __name__ == "__main__":
main()
第六步:构建和分发
6.1 构建分发包
# 安装构建工具
pip install build
# 构建分发包
python -m build
# 或使用传统方式
python setup.py sdist bdist_wheel
这会在 dist/ 目录生成:
mytasks-1.0.0.tar.gz:源码包mytasks-1.0.0-py3-none-any.whl:wheel包
6.2 本地测试安装
# 从wheel安装
pip install dist/mytasks-1.0.0-py3-none-any.whl
# 从源码包安装
pip install dist/mytasks-1.0.0.tar.gz
6.3 发布到PyPI
注意:发布前请确保包名唯一
# 1. 检查包名是否可用
pip search your-unique-package-name # 已废弃,建议用浏览器访问 pypi.org
# 2. 或者用Python检查
python -c "import requests; r=requests.get('https://pypi.org/simple/your-unique-name/'); print('✅ 可用' if r.status_code==404 else '❌ 已存在')"
# 3. 安装上传工具
pip install twine
# 4. 检查包
twine check dist/*
# 5. 先上传到测试PyPI
twine upload --repository testpypi dist/*
# 6. 测试安装
pip install -i https://test.pypi.org/simple/ your-package-name
# 7. 确认无误后上传到正式PyPI
twine upload dist/*
PyPI发布最佳实践:
- 唯一包名:使用
your-name-toolname格式避免冲突 - 语义化版本:遵循
主版本.次版本.修订版格式 - 完整文档:包含详细的README和使用说明
- 测试覆盖:确保代码质量和功能完整性
第七步:实际运行演示
让我们看看实际运行效果:
# 安装工具
$ pip install -e .
Successfully installed mytasks-1.0.0
# 查看版本
$ mytasks --version
mytasks 1.0.0
# 添加任务
$ mytasks add "学习Python CLI开发" -d "完成命令行工具demo" -p high
✅ 任务已添加: 学习Python CLI开发
$ mytasks add "阅读技术文档" -p medium
✅ 任务已添加: 阅读技术文档
# 列出任务
$ mytasks list
📋 任务列表:
------------------------------------------------------------
⏳ [1] 🔴 学习Python CLI开发
📝 完成命令行工具demo
🕒 2025-08-02 23:48:49
⏳ [2] 🟡 阅读技术文档
🕒 2025-08-02 23:48:58
# 完成任务
$ mytasks complete 1
✅ 任务已完成: 学习Python CLI开发
# 使用别名
$ task list
📋 任务列表:
------------------------------------------------------------
⏳ [2] 🟡 阅读技术文档
🕒 2025-08-02 23:48:58
进阶特性
8.1 添加配置文件支持
# 支持配置文件
def load_config():
config_file = os.path.expanduser("~/.mytasks_config.json")
if os.path.exists(config_file):
with open(config_file, 'r') as f:
return json.load(f)
return {"default_priority": "medium", "data_file": "~/.tasks.json"}
8.2 添加颜色输出
# 使用 colorama 库添加颜色
from colorama import Fore, Style, init
init(autoreset=True)
def print_success(message):
print(f"{Fore.GREEN}✅ {message}{Style.RESET_ALL}")
def print_error(message):
print(f"{Fore.RED}❌ {message}{Style.RESET_ALL}")
8.3 添加单元测试
# tests/test_demo.py
import pytest
from demo import TaskManager
import tempfile
import os
def test_add_task():
with tempfile.NamedTemporaryFile(delete=False) as f:
tm = TaskManager(f.name)
tm.add_task("测试任务", "描述", "high")
assert len(tm.tasks) == 1
assert tm.tasks[0]["title"] == "测试任务"
os.unlink(f.name)
常见问题和解决方案
9.1 包名冲突问题
问题现象:
$ pip uninstall mytasks
$ pip install mytasks
ERROR: Could not find a version that satisfies the requirement mytasks
原因分析:
- 本地包未发布:你的包只在本地,PyPI上不存在
- 包名冲突:常见名称可能被其他开发者占用
- 网络问题:无法访问PyPI服务器
解决方案:
# 方案1:继续使用本地安装
pip install -e . # 开发模式
pip install . # 标准安装
# 方案2:使用构建的wheel文件
python -m build
pip install dist/mytasks-1.0.0-py3-none-any.whl
# 方案3:更改包名(推荐用于发布)
# 修改 setup.py 中的 name="your-unique-name"
包名选择建议:
- 使用个人或组织前缀:
yourname-mytasks - 添加功能描述:
cli-task-manager - 检查可用性:访问
https://pypi.org/project/your-package-name/
9.2 Unicode编码问题
在Windows下可能遇到Unicode显示问题:
# 解决方案:设置输出编码
if sys.platform == "win32":
import codecs
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach())
9.3 路径问题
处理不同操作系统的路径差异:
# 使用 pathlib 处理路径
from pathlib import Path
data_file = Path.home() / ".tasks.json"
9.4 依赖管理
明确区分生产依赖和开发依赖:
# setup.py
install_requires=[
"click>=8.0", # 生产依赖
],
extras_require={
"dev": [
"pytest>=6.0", # 开发依赖
"black>=21.0",
],
}
9.5 命令行工具调试
常见问题:
- 命令未找到:
mytasks: command not found - 权限错误:
Permission denied - 模块导入失败:
ModuleNotFoundError
调试技巧:
# 检查安装状态
pip list | grep mytasks
# 查看安装位置
pip show mytasks
# 检查可执行文件
which mytasks # Linux/Mac
where mytasks # Windows
# 重新安装
pip uninstall mytasks
pip install -e . --force-reinstall
# 查看详细错误信息
python -c "from demo import main; main()" --help
最佳实践总结
10.1 代码结构
- 单一职责:每个函数只做一件事
- 清晰的接口:使用类型提示
- 错误处理:合理的异常处理和用户提示
- 可测试性:便于编写单元测试
10.2 用户体验
- 清晰的帮助信息:详细的命令说明和示例
- 友好的错误提示:告诉用户出了什么问题以及如何解决
- 一致的界面:统一的命令格式和输出风格
- 渐进式功能:从基础功能开始,逐步增加高级特性
10.3 项目管理
- 版本控制:使用语义化版本号
- 文档完善:README、注释、类型提示
- 自动化测试:单元测试、集成测试
- 持续改进:根据用户反馈优化功能
结语
通过本文的详细介绍,我们从零开始创建了一个完整的Python命令行工具,涵盖了:
- 🏗️ 项目结构设计
- 💻 核心代码实现
- ⚙️ 配置文件编写
- 📦 打包和安装
- 🚀 分发和发布
- 🧪 测试和调试
- 🔧 问题排查和解决
开发流程总结
日常开发流程:
# 1. 初始设置
git clone your-project
cd your-project
pip install -e .
# 2. 开发循环
# 编辑代码 -> 测试功能 -> 调试问题
mytasks add "测试功能"
mytasks list
# 3. 版本发布
python -m build
pip install dist/*.whl # 测试打包版本
git tag v1.0.1
twine upload dist/* # 发布到PyPI
实际项目建议:
- 选择唯一包名:避免与PyPI现有包冲突
- 本地开发优先:使用
pip install -e .进行开发 - 测试驱动开发:先写测试,再实现功能
- 渐进式发布:从本地 -> 测试PyPI -> 正式PyPI
关键技术点回顾
- entry_points 配置:将Python函数映射为命令行工具
- argparse 使用:创建专业的命令行界面
- 开发模式安装:
pip install -e .提高开发效率 - 包名管理:避免冲突,选择合适的命名策略
- 错误处理:Unicode编码、路径处理、异常捕获
这个框架可以作为你未来开发命令行工具的模板。记住,好的命令行工具应该简单易用、功能完善、文档清晰。从小工具开始,逐步完善,最终可能成为被广泛使用的开源项目!
下一步建议:
- 尝试添加更多功能(搜索、标签、提醒等)
- 集成第三方服务(GitHub、Todoist等)
- 添加配置文件和主题支持
- 发布到PyPI供其他人使用
常见陷阱提醒:
- 包名冲突:发布前检查PyPI包名可用性
- 编码问题:Windows下注意Unicode显示
- 权限问题:确保有足够权限安装和执行
- 依赖管理:明确区分开发和生产依赖
希望这篇文章对你有帮助,开始你的命令行工具开发之旅吧!🚀
附录
A. 完整项目清单
确保你的项目包含以下文件:
my_cli/
├── demo.py # ✅ 主程序
├── setup.py # ✅ 安装配置(传统)
├── pyproject.toml # ✅ 安装配置(现代)
├── README.md # ✅ 项目说明
├── LICENSE # ✅ 许可证
├── requirements.txt # ✅ 依赖列表
├── demo_installation.py # ✅ 演示脚本
├── .gitignore # 📁 Git忽略文件
├── tests/ # 📁 测试目录
│ └── test_demo.py # 🧪 单元测试
├── dist/ # 📁 构建输出(自动生成)
└── build/ # 📁 构建缓存(自动生成)
B. 常用命令速查
# 开发环境设置
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
pip install -e .
# 代码质量检查
black demo.py # 代码格式化
flake8 demo.py # 代码检查
pytest tests/ # 运行测试
# 构建和分发
python -m build # 构建包
twine check dist/* # 检查包
twine upload --repository testpypi dist/* # 测试发布
# 调试命令
pip list | grep mytasks # 检查安装
pip show mytasks # 查看详情
which mytasks # 查找命令位置
C. .gitignore 模板
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
env.bak/
venv.bak/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Project specific
.tasks.json
D. 进阶功能代码片段
配置文件支持:
import json
from pathlib import Path
def load_config():
config_file = Path.home() / ".mytasks_config.json"
default_config = {
"default_priority": "medium",
"data_file": str(Path.home() / ".tasks.json"),
"theme": "default"
}
if config_file.exists():
try:
with open(config_file, 'r') as f:
user_config = json.load(f)
default_config.update(user_config)
except (json.JSONDecodeError, IOError):
pass
return default_config
彩色输出:
try:
from colorama import Fore, Style, init
init(autoreset=True)
COLORS_AVAILABLE = True
except ImportError:
COLORS_AVAILABLE = False
def colored_print(message, color=None):
if COLORS_AVAILABLE and color:
colors = {
'green': Fore.GREEN,
'red': Fore.RED,
'yellow': Fore.YELLOW,
'blue': Fore.BLUE
}
print(f"{colors.get(color, '')}{message}{Style.RESET_ALL}")
else:
print(message)
日志记录:
import logging
from pathlib import Path
def setup_logging(verbose=False):
log_level = logging.DEBUG if verbose else logging.INFO
log_file = Path.home() / ".mytasks.log"
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
)
E. 推荐资源
作者注: 本教程基于实际开发经验编写,所有代码都经过测试验证。如有问题或建议,欢迎反馈交流!