Python命令行工具开发
Python命令行工具开发一、argparse标准库import argparseimport sysdef create_parser():parser argparse.ArgumentParser(progmytool,description一个示例命令行工具,epilog更多信息请访问 https://example.com)# 位置参数parser.add_argument(input, help输入文件路径)# 可选参数parser.add_argument(-o, --output, defaultoutput.txt, help输出文件路径)parser.add_argument(-v, --verbose, actionstore_true, help详细输出)parser.add_argument(-n, --count, typeint, default10, help处理数量)parser.add_argument(--format, choices[json, csv, xml], defaultjson)parser.add_argument(--tags, nargs, help标签列表)return parserdef main():parser create_parser()args parser.parse_args()if args.verbose:print(f输入: {args.input})print(f输出: {args.output})print(f格式: {args.format})process(args.input, args.output, args.count, args.format)二、子命令def build_parser():parser argparse.ArgumentParser(progdevtool)subparsers parser.add_subparsers(destcommand, help可用命令)# init子命令init_parser subparsers.add_parser(init, help初始化项目)init_parser.add_argument(name, help项目名称)init_parser.add_argument(--template, -t, defaultdefault)# build子命令build_parser subparsers.add_parser(build, help构建项目)build_parser.add_argument(--release, actionstore_true)build_parser.add_argument(--target, defaultall)# deploy子命令deploy_parser subparsers.add_parser(deploy, help部署项目)deploy_parser.add_argument(environment, choices[dev, staging, prod])deploy_parser.add_argument(--dry-run, actionstore_true)return parserdef main():parser build_parser()args parser.parse_args()if args.command init:init_project(args.name, args.template)elif args.command build:build_project(args.release, args.target)elif args.command deploy:deploy_project(args.environment, args.dry_run)else:parser.print_help()三、Click框架import clickimport osclick.group()click.version_option(1.0.0)click.option(--debug/--no-debug, defaultFalse, envvarDEBUG)click.pass_contextdef cli(ctx, debug):开发工具集ctx.ensure_object(dict)ctx.obj[DEBUG] debugcli.command()click.argument(name)click.option(--template, -t, typeclick.Choice([flask, fastapi, django]))click.option(--directory, -d, typeclick.Path(), default.)def init(name, template, directory):初始化新项目project_path os.path.join(directory, name)click.echo(f创建项目: {project_path})click.echo(f模板: {template or default})if os.path.exists(project_path):if not click.confirm(f目录 {project_path} 已存在是否覆盖):click.echo(已取消)return# 创建项目...click.secho(f项目 {name} 创建成功!, fggreen, boldTrue)cli.command()click.option(--port, -p, default8000, typeint)click.option(--host, -h, default127.0.0.1)click.option(--reload/--no-reload, defaultTrue)def serve(port, host, reload):启动开发服务器click.echo(f服务器启动: http://{host}:{port})if reload:click.echo(热重载已启用)cli.command()click.argument(files, nargs-1, typeclick.Path(existsTrue))click.option(--fix, is_flagTrue, help自动修复问题)click.pass_contextdef lint(ctx, files, fix):代码检查if not files:files (.,)with click.progressbar(files, label检查文件) as bar:for f in bar:check_file(f, fixfix)if __name__ __main__:cli()四、交互式输入import clickdef interactive_setup():交互式项目配置name click.prompt(项目名称, typestr)version click.prompt(版本号, default0.1.0)author click.prompt(作者)license_type click.prompt(许可证,typeclick.Choice([MIT, Apache-2.0, GPL-3.0]),defaultMIT)description click.prompt(项目描述, default)click.echo(\n配置摘要:)click.echo(f 名称: {name})click.echo(f 版本: {version})click.echo(f 作者: {author})click.echo(f 许可证: {license_type})if click.confirm(\n确认创建, defaultTrue):create_project(name, version, author, license_type, description)click.secho(创建成功!, fggreen)else:click.echo(已取消)# 密码输入password click.prompt(密码, hide_inputTrue, confirmation_promptTrue)# 编辑器打开message click.edit(请输入提交信息) # 打开$EDITOR五、输出格式化import clickimport jsonclass OutputFormatter:支持多种输出格式def __init__(self, format_typetable):self.format_type format_typedef output(self, data, headersNone):if self.format_type json:click.echo(json.dumps(data, indent2, ensure_asciiFalse))elif self.format_type csv:self._output_csv(data, headers)else:self._output_table(data, headers)def _output_table(self, data, headers):if not data:returnif headers is None:headers list(data[0].keys()) if isinstance(data[0], dict) else []# 计算列宽rows []for item in data:if isinstance(item, dict):rows.append([str(item.get(h, )) for h in headers])else:rows.append([str(x) for x in item])widths [len(h) for h in headers]for row in rows:for i, cell in enumerate(row):widths[i] max(widths[i], len(cell))# 输出表头header_line | .join(h.ljust(widths[i]) for i, h in enumerate(headers))click.echo(header_line)click.echo(--.join(- * w for w in widths))# 输出数据for row in rows:line | .join(cell.ljust(widths[i]) for i, cell in enumerate(row))click.echo(line)def _output_csv(self, data, headers):import csvimport iooutput io.StringIO()if isinstance(data[0], dict):writer csv.DictWriter(output, fieldnamesheaders or data[0].keys())writer.writeheader()writer.writerows(data)click.echo(output.getvalue())# 彩色输出def print_status(message, statusinfo):colors {info: blue, success: green, warning: yellow, error: red}symbols {info: ℹ, success: ✓, warning: ⚠, error: ✗}click.secho(f {symbols[status]} {message}, fgcolors[status])# 进度条import timedef process_files(files):with click.progressbar(files, label处理中, show_percentTrue) as bar:for f in bar:time.sleep(0.1) # 模拟处理六、配置文件集成import osimport jsonfrom pathlib import Pathclass CLIConfig:CLI工具配置管理CONFIG_FILE .mytool.jsondef __init__(self):self.config_path self._find_config()self.data self._load()def _find_config(self):向上查找配置文件current Path.cwd()while current ! current.parent:config current / self.CONFIG_FILEif config.exists():return configcurrent current.parentreturn Path.cwd() / self.CONFIG_FILEdef _load(self):if self.config_path.exists():return json.loads(self.config_path.read_text())return {}def save(self):self.config_path.write_text(json.dumps(self.data, indent2))def get(self, key, defaultNone):keys key.split(.)value self.datafor k in keys:if isinstance(value, dict):value value.get(k)else:return defaultreturn value if value is not None else defaultdef set(self, key, value):keys key.split(.)data self.datafor k in keys[:-1]:data data.setdefault(k, {})data[keys[-1]] valueself.save()七、插件系统import importlibimport pkgutilclass PluginManager:CLI插件管理器def __init__(self, namespacemytool.plugins):self.namespace namespaceself.plugins {}def discover(self):发现已安装的插件try:package importlib.import_module(self.namespace)for importer, modname, ispkg in pkgutil.iter_modules(package.__path__, package.__name__ .):try:module importlib.import_module(modname)if hasattr(module, register):plugin_info module.register()self.plugins[plugin_info[name]] moduleexcept Exception as e:print(f加载插件 {modname} 失败: {e})except ImportError:passdef get_commands(self):获取所有插件提供的命令commands {}for name, module in self.plugins.items():if hasattr(module, cli_commands):commands.update(module.cli_commands())return commands# 插件示例 (mytool/plugins/git_plugin.py)def register():return {name: git, version: 1.0}def cli_commands():return {git-status: git_status_command}八、错误处理与退出码import sysclass CLIError(Exception):CLI错误基类def __init__(self, message, exit_code1):super().__init__(message)self.exit_code exit_codeclass ConfigError(CLIError):def __init__(self, message):super().__init__(f配置错误: {message}, exit_code2)class NetworkError(CLIError):def __init__(self, message):super().__init__(f网络错误: {message}, exit_code3)def cli_main():带错误处理的主入口try:cli()except CLIError as e:click.secho(f错误: {e}, fgred, errTrue)sys.exit(e.exit_code)except KeyboardInterrupt:click.echo(\n操作已取消, errTrue)sys.exit(130)except Exception as e:click.secho(f未预期的错误: {e}, fgred, errTrue)if os.environ.get(DEBUG):import tracebacktraceback.print_exc()sys.exit(1)九、测试CLIfrom click.testing import CliRunnerdef test_init_command():runner CliRunner()with runner.isolated_filesystem():result runner.invoke(cli, [init, myproject, -t, flask])assert result.exit_code 0assert 创建成功 in result.outputdef test_serve_command():runner CliRunner()result runner.invoke(cli, [serve, --port, 9000])assert result.exit_code 0assert 9000 in result.outputdef test_interactive_input():runner CliRunner()result runner.invoke(cli, [init], inputmyproject\n0.1.0\nAuthor\nMIT\n\ny\n)assert result.exit_code 0十、总结CLI开发要点1. 简单工具用argparse复杂工具用Click2. 提供--help和--version选项3. 使用子命令组织复杂功能4. 支持配置文件和环境变量5. 提供有意义的退出码6. 彩色输出和进度条提升用户体验7. 编写测试确保CLI行为正确8. 支持管道操作stdin/stdout