یک برنامه، چند فرمان
برنامههای خط فرمان حرفهای (مثل git و pip) یک «فرمان اصلی» و چند «زیرفرمان» دارند. ماژول استاندارد argparse همین را با چند خط میسازد و راهنمای --help، بررسی نوع و پیام خطا را هم رایگان تحویل میدهد؛ دیگر لازم نیست با input() و منوهای عددی کلنجار بروید.
cli.py
# cli.py — رابط خط فرمان مدیریت سفارش فرش
import argparse
import sys
from collections import Counter
from datetime import datetime
from zoneinfo import ZoneInfo
import jdatetime
from store import STATUSES, Order, load_orders, next_code, save_orders, toman
TEHRAN = ZoneInfo("Asia/Tehran")
def to_jalali(iso: str, fmt: str = "%Y/%m/%d") -> str:
local = datetime.fromisoformat(iso).astimezone(TEHRAN)
return jdatetime.datetime.fromgregorian(datetime=local).strftime(fmt)
def cmd_add(args) -> int:
orders = load_orders()
order = Order(next_code(orders), args.customer, args.design, args.size, args.price)
save_orders(orders + [order])
print(f"سفارش {order.code} ثبت شد.")
return 0
def cmd_list(args) -> int:
for o in load_orders():
if args.status is None or o.status == args.status:
print(f"{o.code} {o.customer} {o.design} {o.price:,} {to_jalali(o.created)} {o.status}")
return 0
def cmd_status(args) -> int:
orders = load_orders()
for o in orders:
if o.code == args.code.upper():
o.status = args.status
save_orders(orders)
print(f"{o.code}: {o.status}")
return 0
print(f"سفارش {args.code} پیدا نشد.", file=sys.stderr)
return 1
def cmd_report(args) -> int:
orders = load_orders()
sales = Counter()
for o in orders:
sales[to_jalali(o.created, "%Y/%m")] += o.price
print("بر اساس وضعیت:", dict(Counter(o.status for o in orders)))
for design, count in Counter(o.design for o in orders).most_common(3):
print(f" طرح {design}: {count} سفارش")
for month, total in sorted(sales.items()):
print(f" {month}: {total:,} تومان")
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="orders", description="مدیریت سفارش فرش")
sub = parser.add_subparsers(dest="command", required=True)
p = sub.add_parser("add", help="ثبت سفارش")
p.add_argument("customer")
p.add_argument("design")
p.add_argument("size")
p.add_argument("--price", type=toman, required=True)
p.set_defaults(func=cmd_add)
p = sub.add_parser("list", help="فهرست سفارشها")
p.add_argument("--status", choices=STATUSES)
p.set_defaults(func=cmd_list)
p = sub.add_parser("status", help="تغییر وضعیت")
p.add_argument("code")
p.add_argument("status", choices=STATUSES)
p.set_defaults(func=cmd_status)
sub.add_parser("report", help="گزارش").set_defaults(func=cmd_report)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
return args.func(args)
except ValueError as exc:
print(f"خطا: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
اجرا در PowerShell
python cli.py add "رضا نراقی" افشان 3x4 --price ۵۷٬۰۰۰٬۰۰۰
python cli.py add "زهرا قمصری" ماهی 2x3 --price 31,500,000
python cli.py status ksh-101 weaving
python cli.py list --status weaving
python cli.py report
python cli.py add --help
سه ایدهی کلیدی
- set_defaults(func=...): هر زیرفرمان تابع خودش را با خود میآورد؛ بهجای زنجیرهی بلند if/elif فقط
args.func(args)صدا زده میشود. - Counter سه کار گزارش را انجام میدهد: شمارش وضعیتها، پرسفارشترین طرحها با
most_commonو جمع فروش هر ماه شمسی با+=بدون مقداردهی اولیه. - main(argv) لیست آرگومان میگیرد؛ پس در تست یا REPL مینویسید
main(["report"])و دیگر به ترمینال نیازی نیست.
نکتههایی که کمتر کسی میداند
- هر تابعی که رشته بگیرد و ValueError بدهد، میتواند
type=در argparse باشد؛ argparse خطا را به پیام «invalid toman value» تبدیل میکند و با کد خروج 2 بیرون میرود، بدون اینکه try بنویسید. sys.exit(main())کد خروج را به سیستمعامل میدهد؛ در PowerShell متغیر$LASTEXITCODEآن را نشان میدهد و در Task Scheduler یا اسکریپتهای پشتیبان، شکست برنامه تشخیصپذیر میشود.- اگر خروجی را در PowerShell با
>به فایل بفرستید، پایتون در ویندوز برای stdout هدایتشده کدگذاری ANSI سیستم (cp1252 یا cp1256) را برمیدارد و ممکن است UnicodeEncodeError بگیرید؛$env:PYTHONUTF8 = "1"را تنظیم کنید. - تراز ستونی با
:16برای متن فارسی در ترمینال قابل اعتماد نیست، چون ترمینال حروف را متصل و راستبهچپ نمایش میدهد؛ برای گزارش جدی CSV با utf-8-sig بسازید و در اکسل باز کنید. Counter.total()(از 3.10) جمع همهی مقادیر را میدهد و دو Counter را میتوان با+جمع کرد؛ مثلاً فروش دو شعبهی کاشان و آران.