সরাসরি পর্বে যান
পর্ব ৪৯ · ৫৫-এর মধ্যে · মডিউল ৭

অটোমেশন আর CLI টুল — argparse আর ফাইলের কাজ

Python Automation & Command-Line Tools — argparse, pathlib, shutil & subprocess

Taught in Bangla, this lesson turns repetitive tasks into Python command-line tools: argv, stdout, stderr and exit codes, argparse arguments, flags, choices and help, a main() structure with subcommands, file automation with pathlib and shutil, safe scripts with dry-run, logging and idempotency, merging monthly CSV reports, subprocess.run and scheduling.

এই পর্বে যা শিখবেন

  • কোন কাজ অটোমেট করা লাভজনক, আর কোনটি সময় নষ্ট — একটি সহজ হিসাব
  • কমান্ড লাইন কীভাবে প্রোগ্রামের সাথে কথা বলে: sys.argv, stdout, stderr আর exit code
  • argparse গভীরে — positional, flag, type, choices, default, help আর subcommand (add/list/done)
  • pathlib, shutil, csv, zipfile আর subprocess দিয়ে বাস্তব কাজ: বিয়ের ছবির নাম বদল, শাখার রিপোর্ট জোড়া, থিসিসের ব্যাকআপ
  • cron আর Task Scheduler দিয়ে সময়মতো চালানো, টুলকে ইনস্টলযোগ্য কমান্ড বানানো, আর অটোমেশনের নিরাপত্তার নিয়ম

১ · কোন কাজ অটোমেট করবেন — বিরক্তি থেকে স্ক্রিপ্ট

আপনার বোনের বিয়ে শেষ। ফটোগ্রাফার একটি পেনড্রাইভে দিয়েছেন ৬৪০টি ছবি, নাম IMG_0001.jpg থেকে IMG_0640.jpg. আপনি চান নামগুলো হোক biye_2026_001.jpg, biye_2026_002.jpg… যাতে পরে খুঁজে পাওয়া যায়। হাতে করলে প্রতিটি ছবিতে রাইট-ক্লিক, Rename, টাইপ, Enter — ধরুন ১০ সেকেন্ড করে, মোট প্রায় পৌনে দুই ঘণ্টা। মাঝখানে একটি নম্বর বাদ পড়বে বা দুবার বসবে, প্রায় নিশ্চিত। Python-এ একটি ছোট স্ক্রিপ্ট এই কাজ করে এক সেকেন্ডেরও কম সময়ে, আর ভুল করে না।

এই পর্বের বিষয় হলো automation — কম্পিউটারে যে একঘেয়ে কাজ আপনি বারবার হাতে করেন, সেটি একটি script-কে দিয়ে করানো। অফিসে প্রতি মাসে ১২টি শাখার বিক্রির ফাইল জোড়া লাগানো, ডাউনলোড ফোল্ডারের হাজারো ফাইল সাজানো, প্রতি রাতে থিসিসের ফোল্ডারের ব্যাকআপ — এসবই একেকটি স্ক্রিপ্টের অপেক্ষায় আছে।

লাভ-ক্ষতির হিসাব: কতবার × কত সময় × ভুলের ঝুঁকি

সব কাজ অটোমেট করা লাভজনক নয়। স্ক্রিপ্ট লিখতে, পরীক্ষা করতে আর পরে ঠিক রাখতে সময় লাগে। webcomic xkcd-র বিখ্যাত ছবি “Is It Worth the Time?” (নং ১২০৫) একটি টেবিলে দেখায়: একটি কাজ কতবার করেন আর প্রতিবার কত সময় বাঁচবে, তার ভিত্তিতে পাঁচ বছরে মোট কত সময় বাঁচে — অর্থাৎ অটোমেশনে সর্বোচ্চ কত সময় দেওয়া যুক্তিসংগত। যেমন দিনে একবার করা কাজে ৫ মিনিট বাঁচলে পাঁচ বছরে বাঁচে প্রায় ৬ দিন। তিনটি প্রশ্ন জিজ্ঞেস করুন:

🔁 কতবার?

প্রতিদিন, প্রতি সপ্তাহে, প্রতি মাসে? জীবনে একবারের কাজ হাতে করাই ভালো। মাসে একবারের কাজও বছরে ১২ বার, পাঁচ বছরে ৬০ বার।

মাসিক রিপোর্ট, সাপ্তাহিক ব্যাকআপ

⏱️ কত সময়?

প্রতিবার কত মিনিট যায়? আর আপনার মনোযোগের খরচ — একঘেয়ে কাজ মাথা ক্লান্ত করে, আসল কাজের সময় খেয়ে ফেলে।

৬৪০টি ছবি × ১০ সেকেন্ড

⚠️ ভুলের ঝুঁকি?

মানুষ ক্লান্ত হলে ভুল করে; স্ক্রিপ্ট একই কাজ একইভাবে করে। বেতনের হিসাব, ব্যাংকের রিপোর্টে একটি ভুলের দাম অনেক বেশি।

হিসাব, ডেটা কপি

পাড়ার চায়ের দোকানের কথা ভাবুন। দোকানি প্রথম দিকে প্রতিবার আলাদা করে চিনি মেপে, পাতা ফুটিয়ে চা বানান। ভিড় বাড়লে তিনি একটি বড় কেটলিতে আগে থেকে লিকার বানিয়ে রাখেন — এক কাপের জন্য এই ব্যবস্থা বাড়াবাড়ি, কিন্তু দিনে ৩০০ কাপে এটাই বাঁচিয়ে দেয়। আবার কেটলির লিকার ভুল হলে ৩০০ কাপই খারাপ হয়। অটোমেশনও তেমনি: বারবারের কাজে বিশাল লাভ, কিন্তু ভুল স্ক্রিপ্ট ভুলটাও বারবার, দ্রুত আর একসাথে করে। এজন্যই এই পর্বের অর্ধেক কথা নিরাপত্তা নিয়ে।

স্ক্রিপ্ট থেকে টুল

প্রথম সংস্করণ সাধারণত একটি দ্রুত স্ক্রিপ্ট — ফোল্ডারের নাম, prefix সব কোডের ভেতরে লেখা। নিজের জন্য একবার চালাতে এটুকুই যথেষ্ট। কিন্তু পরের মাসে আরেকটি ফোল্ডারে চালাতে কোড খুলে বদলাতে হয়, আর আপনার সহকর্মী তো কোড খুলতেই ভয় পান। তখন স্ক্রিপ্টটিকে বানাতে হয় একটি CLI টুল: কমান্ড লাইন থেকে আর্গুমেন্ট নেবে, -h দিলে নিজের ব্যবহারবিধি বলবে, ভুল ইনপুটে পরিষ্কার বার্তা দেবে, আর ডিফল্ট আচরণ হবে নিরাপদ। পর্ব ২১-এর মাঝপথের প্রজেক্টে আপনি একটি ইন্টারঅ্যাকটিভ CLI বানিয়েছিলেন, যা প্রশ্ন করে উত্তর পড়ত; এই পর্বের টুল চলবে একবারে, আর্গুমেন্ট নিয়ে — যাতে অন্য স্ক্রিপ্ট বা cron-ও তাকে চালাতে পারে।

২ · কমান্ড লাইন কীভাবে কথা বলে — argv, stdout, stderr, exit code

আপনি terminal-এ লেখেন python rename.py photos --dry-run. Shell এই লাইনটি ফাঁকা জায়গায় ভেঙে শব্দের একটি তালিকা বানায় আর Python-কে দেয়। Python সেটি রাখে sys.argv নামের list-এ: ['rename.py', 'photos', '--dry-run']. প্রথম ঘরে স্ক্রিপ্টের নাম, বাকিগুলো command-line argument — সবই str, সংখ্যা দিলেও। এটুকু আপনি আগেও দেখেছেন। একটি CLI প্রোগ্রামের সাথে বাইরের জগতের যোগাযোগের আসলে চারটি পথ:

  1. ইনপুটsys.argv, stdinকী করতে হবে, কোন ডেটায়
  2. আপনার টুলmain(argv)কাজ করে
  3. ফলstdoutআসল আউটপুট
  4. অবস্থাstderr + exit codeসমস্যার খবর, সফল কি না
ভালো CLI টুল এই চারটি পথ আলাদা রাখে — তাই অন্য প্রোগ্রাম তাকে জুড়ে নিতে পারে।
  • stdout — আসল ফল: রিপোর্ট, তালিকা, হিসাব। print() ডিফল্টে এখানেই লেখে। ব্যবহারকারী চাইলে > report.txt দিয়ে ফাইলে পাঠাতে পারেন, বা | দিয়ে আরেকটি প্রোগ্রামে।
  • stderr — সতর্কতা, error, অগ্রগতির খবর: print("ফাইল পাওয়া যায়নি", file=sys.stderr). stdout ফাইলে পাঠালেও stderr পর্দায় থেকে যায় — তাই রিপোর্টের ভেতরে error-এর লেখা মিশে যায় না।
  • exit code — প্রোগ্রাম শেষে OS-কে দেওয়া একটি সংখ্যা। প্রথা অনুযায়ী 0 মানে সফল, অন্য যেকোনো সংখ্যা মানে কোনো সমস্যা। Python-এ sys.exit(1). অন্য স্ক্রিপ্ট, cron বা CI এই সংখ্যা দেখেই ঠিক করে পরের ধাপ চলবে কি না। Linux/macOS-এ শেষ exit code দেখতে echo $?, Windows PowerShell-এ $LASTEXITCODE.
মূল ধারণা

একটি CLI টুল মানুষের সাথে যেমন কথা বলে, অন্য প্রোগ্রামের সাথেও তেমনি। মানুষ পড়ে লেখা; প্রোগ্রাম দেখে exit code আর stdout. তাই নিয়ম তিনটি: ফল stdout-এ, সমস্যার খবর stderr-এ, আর সফল-ব্যর্থ বোঝাতে সঠিক exit code. সব কাজ ব্যর্থ হলেও exit code 0 দিলে cron ভাববে ব্যাকআপ ঠিকঠাক হয়েছে — আর আপনি টের পাবেন যেদিন ব্যাকআপ দরকার হবে।

সরাসরি sys.argv ঘাঁটা ছোট কাজে চলে, কিন্তু দ্রুত জট পাকায়: কোন আর্গুমেন্ট বাধ্যতামূলক, কোনটি ঐচ্ছিক, --start 5-এর 5-কে int বানানো, ভুল দিলে বার্তা, -h দিলে ব্যবহারবিধি — সব নিজে লিখতে হয়। এই সব কাজ করে দেয় standard library-র argparse.

৩ · argparse — আর্গুমেন্ট, flag, type, choices আর help

argparse-এর কাজের ধারা তিন ধাপের: একটি ArgumentParser বানান, তাকে বলুন কোন কোন আর্গুমেন্ট আশা করবেন (add_argument), তারপর parse_args() ডাকুন। ফল আসে একটি Namespace object-এ, যার প্রতিটি attribute একটি আর্গুমেন্টের মান। আর্গুমেন্ট দুই জাতের:

ধরনদেখতেবৈশিষ্ট্যকখন
Positionalphotosঅবস্থান দিয়ে চেনা যায়, ডিফল্টে বাধ্যতামূলকযা ছাড়া কাজই হয় না — কোন ফোল্ডার, কোন ফাইল
Optional (মান-সহ)--prefix biye-- দিয়ে শুরু, যেকোনো ক্রমে, না দিলে defaultসমন্বয় — নতুন নাম, শুরুর নম্বর
Flag (মান ছাড়া)--dry-run, -vথাকলে True, না থাকলে False (action="store_true")হ্যাঁ/না সুইচ

প্রতিটি add_argument-এ কয়েকটি গুরুত্বপূর্ণ বিকল্প দেওয়া যায়: type=int — string-কে রূপান্তর করো, না পারলে পরিষ্কার error; choices=[...] — শুধু এই মানগুলোর একটি চলবে; default=... — না দিলে কী হবে; help="..." — ব্যবহারবিধিতে কী লেখা থাকবে; আর একটি ছোট নাম (-v) ও একটি লম্বা নাম (--verbose) একসাথে। লম্বা নামের মাঝের - attribute-এ হয়ে যায় _: --dry-run থেকে args.dry_run. সবচেয়ে বড় উপহার: আপনি -h বা --help নিজে লেখেন না — argparse আপনার দেওয়া help লেখা থেকে নিজেই সুন্দর help text বানায়।

নিচের কোডে বিয়ের ছবির নাম বদলানোর টুলের parser বানানো হয়েছে। খেয়াল করুন, parse_args()-কে একটি list দেওয়া হয়েছে — না দিলে সে sys.argv[1:] পড়ে। list দিলে terminal ছাড়াই, টেস্টে বা এই পাতায় parser পরীক্ষা করা যায়:

import argparse

def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="rename_photos", description="ছবির নাম একসাথে বদলায়")
    p.add_argument("folder", help="ছবির ফোল্ডার")
    p.add_argument("--prefix", default="photo", help="নতুন নামের শুরু")
    p.add_argument("--start", type=int, default=1, help="প্রথম নম্বর")
    p.add_argument("--case", choices=["lower", "upper"], default="lower")
    p.add_argument("--dry-run", action="store_true", help="শুধু দেখাও, কিছু বদলিও না")
    p.add_argument("-v", "--verbose", action="store_true")
    return p

parser = build_parser()
args = parser.parse_args(["photos", "--prefix", "biye", "--dry-run"])
print(args)
print(args.folder, args.prefix, args.start, args.dry_run)
print(type(args.start).__name__, parser.parse_args(["x", "--start", "7"]).start + 1)
parser.print_help()

প্রথম লাইনে পুরো Namespace: Namespace(folder='photos', prefix='biye', start=1, case='lower', dry_run=True, verbose=False). যা দেওয়া হয়নি (start, case, verbose), সেগুলো default পেয়েছে। দ্বিতীয় লাইন photos biye 1 True. তৃতীয় লাইন int 8 — "7" string এসেছিল, type=int সেটিকে সংখ্যা বানিয়েছে, তাই যোগ করা গেছে। তারপর পুরো help লেখা, যা আমরা এক লাইনও নিজে লিখিনি: usage: rename_photos [-h] [--prefix PREFIX] ..., তারপর বিবরণ, “positional arguments” আর “options” অংশ। terminal-এ -h দিলে ঠিক এটাই ছাপা হয় এবং প্রোগ্রাম exit code 0 দিয়ে থামে। --case আর -v-তে help দিইনি, তাই তাদের পাশে ব্যাখ্যা খালি — প্রতিটি আর্গুমেন্টে help দেওয়া ভালো অভ্যাস।

terminal-এ টাইপ করা একটি লাইন কীভাবে ধাপে ধাপে Namespace হয়, আর -h দিলে কী ঘটে, দেখুন:

python rename_photos.py photos --prefix biye --dry-run
args (Namespace) তৈরি হচ্ছে
sys.argv[1:]
folder
prefix
dry_run
start, case, verbose
  1. Shell শব্দে ভাঙে। sys.argv-এর প্রথম ঘরে স্ক্রিপ্টের নাম; argparse ডিফল্টে sys.argv[1:] পড়ে, আর prog না দিলে usage-এ নাম হিসেবে স্ক্রিপ্টের নাম বসায়।
  2. Positional মিলল। photos শব্দটি - দিয়ে শুরু নয়, তাই এটি প্রথম positional আর্গুমেন্ট folder-এর মান। positional-এর মান তাদের ক্রম দিয়েই চেনা হয়।
  3. Optional তার মান নিল। --prefix দেখে argparse জানে এর পরে একটি মান আসবে, তাই পরের শব্দ biye খেয়ে নিল। --prefix আগে বা পরে, যেকোনো জায়গায় দেওয়া যেত।
  4. Flag-এর মান লাগে না। action="store_true", তাই শুধু উপস্থিতিই যথেষ্ট — dry_run হলো True. নামের - attribute-এ হয়েছে _.
  5. Default বসল, Namespace তৈরি। যা দেওয়া হয়নি তাদের default বসল। কোনো বাধ্যতামূলক আর্গুমেন্ট বাদ পড়লে বা --start abc দিলে এখানেই argparse stderr-এ usage আর error লিখে exit code 2 দিয়ে থামত — আপনার কোডে পৌঁছানোর আগেই।
  6. -h হলে অন্য পথ। argparse নিজের বানানো help stdout-এ লেখে আর exit code 0 দিয়ে প্রোগ্রাম থামায়। কোনো ফাইল ছোঁয়া হয় না। এজন্য অচেনা টুল পেলে প্রথম কাজ: -h.

error-এর সময় argparse কী করে? parse_args() ভুল পেলে stderr-এ লেখে, যেমন rename_photos: error: argument --start: invalid int value: 'abc', তারপর SystemExit(2) তোলে। exit code 2 Unix-এর পুরোনো প্রথা: “ব্যবহারে ভুল”। টেস্টে এটি ধরতে চাইলে pytest-এ with pytest.raises(SystemExit) ব্যবহার করুন (পর্ব ৩৯)। আর Python 3.9 থেকে ArgumentParser(exit_on_error=False) দিলে কিছু ভুলে প্রোগ্রাম না থেমে argparse.ArgumentError ওঠে, যা আপনি নিজে সামলাতে পারেন।

৪ · টুলের কাঠামো — main(), exit code আর subcommand

পর্ব ২২-এ দেখেছেন, ফাইলের শেষে if __name__ == "__main__": (main guard) থাকলে একই ফাইল সরাসরি চালানো যায়, আবার অন্য ফাইল import-ও করতে পারে। CLI টুলের জন্য এর ওপর একটি প্রচলিত কাঠামো দাঁড়িয়েছে:

  1. build_parser() — শুধু parser বানায়। আলাদা রাখলে টেস্টে বা ডকুমেন্টেশনে আবার ব্যবহার করা যায়।
  2. main(argv=None) -> int — আর্গুমেন্ট parse করে, কাজ করে, আর একটি exit code ফেরত দেয়। argv না দিলে argparse নিজেই sys.argv[1:] পড়ে; টেস্টে দেন একটি list.
  3. শেষে if __name__ == "__main__": sys.exit(main()) — main()-এর ফেরত দেওয়া সংখ্যাটিই প্রোগ্রামের exit code.

main()-এর ভেতরে sys.exit() না ডেকে সংখ্যা ফেরত দেওয়ার সুবিধা হলো, টেস্ট সহজে লেখা যায়: assert main(["done", "9"]) == 1. প্রোগ্রাম থামে না, শুধু একটি মান আসে। এবার একটু বড় উদাহরণ — একটি ছোট্ট “todo” টুল, যার তিনটি subcommand: add, list, done. git যেমন git add, git commit — একটি টুল, অনেক কাজ, প্রতিটি কাজের নিজের আর্গুমেন্ট:

import argparse
import json
import sys
from pathlib import Path

DB = Path("todo.json")

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="todo", description="ছোট্ট কাজের তালিকা")
    sub = parser.add_subparsers(dest="command", required=True)
    add = sub.add_parser("add", help="নতুন কাজ যোগ করে")
    add.add_argument("text", help="কাজের বিবরণ")
    sub.add_parser("list", help="সব কাজ দেখায়")
    done = sub.add_parser("done", help="কাজ শেষ বলে চিহ্নিত করে")
    done.add_argument("number", type=int, help="কাজের নম্বর (১ থেকে)")
    return parser

def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    tasks = json.loads(DB.read_text(encoding="utf-8")) if DB.exists() else []
    match args.command:
        case "add":
            tasks.append({"text": args.text, "done": False})
        case "list":
            for i, task in enumerate(tasks, start=1):
                mark = "x" if task["done"] else " "
                print(f"{i}. [{mark}] {task['text']}")
            return 0
        case "done":
            if not 1 <= args.number <= len(tasks):
                print(f"ভুল নম্বর: {args.number}", file=sys.stderr)
                return 1
            tasks[args.number - 1]["done"] = True
    DB.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
    return 0

if __name__ == "__main__":
    sys.exit(main())
$ python todo.py add "বিদ্যুৎ বিল দেওয়া"
$ python todo.py add "থিসিসের ব্যাকআপ"
$ python todo.py done 1
$ python todo.py list
1. [x] বিদ্যুৎ বিল দেওয়া
2. [ ] থিসিসের ব্যাকআপ
$ python todo.py done 9          # ভুল নম্বর → stderr-এ বার্তা
ভুল নম্বর: 9
$ echo $?                        # Windows PowerShell-এ: $LASTEXITCODE
1
$ python todo.py done abc        # type=int ভুল ধরল, exit code 2
usage: todo done [-h] number
todo done: error: argument number: invalid int value: 'abc'

কয়েকটি জিনিস লক্ষ করুন। add_subparsers(dest="command", required=True) — কোন subcommand এসেছে তা থাকে args.command-এ, আর কিছুই না দিলে argparse error দেয়। প্রতিটি subcommand-এর নিজের help আছে: python todo.py done -h. subcommand বাছাইয়ে পর্ব ১১-এর match খুব মানানসই। বার্তা বাংলায় লিখতে ensure_ascii=False (পর্ব ২৬)। আর done 1 দুবার চালালেও ফল একই — কাজটি idempotent; কিন্তু add দুবার চালালে একই কাজ দুবার যোগ হয়। কোন কমান্ড দুবার চালানো নিরাপদ, তা ব্যবহারকারীকে জানানো ভালো টুলের লক্ষণ (অংশ ৬-এ বিস্তারিত)।

error বার্তা stdout-এ লিখবেন না। ধরুন কেউ python todo.py list > tasks.txt চালালেন। “ভুল নম্বর” ধরনের বার্তা stdout-এ গেলে সেটি নিঃশব্দে ফাইলে ঢুকে যায় — ব্যবহারকারী দেখেনই না, আর ফাইলের ডেটা নষ্ট হয়। print(..., file=sys.stderr) আর অশূন্য exit code — দুটিই দিন।

৫ · ফাইলের অটোমেশন — pathlib আর shutil

অটোমেশনের সবচেয়ে বড় অংশ ফাইল নিয়ে। পর্ব ২৫-এ pathlib দিয়ে path বানানো, glob দিয়ে খোঁজা আর ফাইল পড়া-লেখা দেখেছেন। এবার যোগ হলো shutil (“shell utilities”) — কপি, সরানো, পুরো ফোল্ডার কপি, archive বানানো:

কাজউপায়সতর্কতা
ফাইল খোঁজাsorted(folder.glob("IMG_*.jpg")), উপফোল্ডারসহ rglobতালিকার ক্রম নিশ্চিত নয় — সবসময় sorted
নাম বদল / সরানোp.rename(new), shutil.move(src, dst)লক্ষ্যে একই নামের ফাইল থাকলে POSIX-এ নিঃশব্দে মুছে যেতে পারে — আগে exists()
ফাইল কপিshutil.copy2(src, dst)copy2 সময়-তথ্যসহ কপি করে
পুরো ফোল্ডার কপিshutil.copytree(src, dst, dirs_exist_ok=True)dirs_exist_ok Python 3.8 থেকে
zip বানানোshutil.make_archive(base, "zip", root_dir=src), বিস্তারিত কাজে zipfilearchive-টি যে ফোল্ডারের zip হচ্ছে তার ভেতরে রাখবেন না
ফোল্ডার মোছাshutil.rmtree(path)ফেরত আনা যায় না, Recycle Bin-এ যায় না

বিয়ের ছবির নাম বদল — আগে dry-run

নাম বদলের স্ক্রিপ্টে একটি ভুল (যেমন সব ছবির নতুন নাম একই হয়ে যাওয়া) মানে ৬৪০টি ছবির ৬৩৯টি একটির ওপর আরেকটি লিখে হারিয়ে যাওয়া। তাই পেশাদার টুলের প্রথম নিয়ম: dry run — আসল কাজের আগে শুধু দেখানো কী করা হবে, কিছু না বদলে। --dry-run দিয়ে তালিকা দেখুন, মিলিয়ে নিন, তারপর আসল রান। দেখুন একই ফোল্ডারে দুই ধরনের রানে কী ঘটে:

photos/ — শুরুর অবস্থা৪টি ফাইল
IMG_0001.jpg
IMG_0002.jpg
IMG_0003.jpg
biye_2026_002.jpg   ← আগে কেউ হাতে একটি বদলে রেখেছিল
$ python rename_photos.py photos --prefix biye_2026 --dry-runশুধু পরিকল্পনা
[dry-run] IMG_0001.jpg → biye_2026_001.jpg
[বাদ] IMG_0002.jpg - biye_2026_002.jpg আগেই আছে
[dry-run] IMG_0003.jpg → biye_2026_003.jpg
ফোল্ডারে এখনো: IMG_0001, IMG_0002, IMG_0003, biye_2026_002 — কিছুই বদলায়নি
আপনি তালিকা মিলিয়ে দেখছেনসিদ্ধান্তের সময়
নতুন নাম ঠিক আছে?            হ্যাঁ
দুটি ফাইলের নতুন নাম একই?     না
সংঘর্ষ (collision)?           ১টি — IMG_0002 বাদ যাবে, মুছবে না
সিদ্ধান্ত: আসল রান চালানো যায়
$ python rename_photos.py photos --prefix biye_2026আসল রান
[করা হলো] IMG_0001.jpg → biye_2026_001.jpg
[বাদ] IMG_0002.jpg - biye_2026_002.jpg আগেই আছে
[করা হলো] IMG_0003.jpg → biye_2026_003.jpg
ফোল্ডারে এখন: IMG_0002, biye_2026_001, biye_2026_002, biye_2026_003
একই কমান্ড আবার চালানো হলোidempotent
[বাদ] IMG_0002.jpg - biye_2026_002.jpg আগেই আছে
IMG_0001 আর IMG_0003 আর নেই, তাই তাদের ছোঁয়াই হলো না।
ফোল্ডার আগের মতোই — দ্বিতীয় রানে কোনো ক্ষতি নেই।
ফোল্ডারের অবস্থা
রান
বদলানো ফাইল
বাদ পড়া
হারানো ছবি
  1. শুরু। ফটোগ্রাফারের তিনটি ছবি, আর একটি ছবি যা কেউ আগেই হাতে বদলে রেখেছিল। বাস্তবের ফোল্ডার কখনো পুরোপুরি পরিষ্কার থাকে না — ঠিক এই ধরনের অবস্থাই স্ক্রিপ্ট ভাঙে।
  2. Dry-run. স্ক্রিপ্ট সব হিসাব করল — কোন ফাইলের নতুন নাম কী, কোথায় সংঘর্ষ — কিন্তু rename() ডাকল না। আউটপুট আর আসল রানের আউটপুট প্রায় হুবহু এক; পার্থক্য শুধু শুরুর লেবেলে।
  3. মানুষের যাচাই। এই ধাপটিই dry-run-এর আসল উদ্দেশ্য। এখানে আপনি ধরে ফেলতে পারেন prefix-এ বানান ভুল, বা সব নতুন নাম একই হয়ে যাওয়ার মতো বিপর্যয় — কোনো ক্ষতির আগেই।
  4. আসল রান। একই কোড, শুধু dry_run=False. সংঘর্ষের ফাইলটি স্ক্রিপ্ট ওভাররাইট করেনি, বাদ দিয়েছে আর জানিয়েছে। একটি ফাইলের সমস্যা বাকি দুটির কাজ থামায়নি।
  5. আবার চালালেও নিরাপদ। নতুন নামের নম্বর আসে পুরোনো নাম থেকে (IMG_0003 → 003), গণনা থেকে নয়। তাই দ্বিতীয় রানে বদলানো ফাইলগুলো আর মেলে না, আর নম্বর সরে যায় না। একে বলে idempotent আচরণ।

এবার সেই যুক্তি আসল কোডে। স্ক্রিপ্টটি নিজের working directory-তে একটি নকল ফোল্ডার বানায়, তিনবার চালায় (dry-run, আসল, আবার আসল), প্রতিবার ফোল্ডারের sorted তালিকা ছাপে, আর শেষে ফোল্ডার মুছে দেয়:

from pathlib import Path
import shutil

def rename_photos(folder: Path, prefix: str, dry_run: bool) -> list[str]:
    failed = []
    for old in sorted(folder.glob("IMG_*.jpg")):
        new = old.with_name(f"{prefix}_{int(old.stem[4:]):03d}.jpg")
        try:
            if new.exists():
                raise FileExistsError(f"{new.name} আগেই আছে")
            if not dry_run:
                old.rename(new)
            print("[dry-run]" if dry_run else "[করা হলো]", old.name, "->", new.name)
        except OSError as e:          # একটি ফাইলের সমস্যা, বাকিরা চলবে
            print("[বাদ]", old.name, "-", e)
            failed.append(old.name)
    return failed

demo = Path("demo_biye")
shutil.rmtree(demo, ignore_errors=True)
demo.mkdir()
for name in ["IMG_0001.jpg", "IMG_0002.jpg", "IMG_0003.jpg", "biye_2026_002.jpg"]:
    (demo / name).write_text("photo")
for dry in (True, False, False):
    print("ব্যর্থ:", rename_photos(demo, "biye_2026", dry))
    print(sorted(p.name for p in demo.iterdir()))
shutil.rmtree(demo)

প্রথম রান (dry-run): দুটি [dry-run] লাইন, একটি [বাদ] IMG_0002.jpg - biye_2026_002.jpg আগেই আছে, তারপর ব্যর্থ: ['IMG_0002.jpg'] আর ফোল্ডার হুবহু আগের মতো: ['IMG_0001.jpg', 'IMG_0002.jpg', 'IMG_0003.jpg', 'biye_2026_002.jpg']. দ্বিতীয় রানে একই তিনটি লাইন, তবে লেবেল [করা হলো], আর ফোল্ডার হয় ['IMG_0002.jpg', 'biye_2026_001.jpg', 'biye_2026_002.jpg', 'biye_2026_003.jpg']. তৃতীয় রানে শুধু বাদের লাইন — ফোল্ডার বদলায় না। খেয়াল করুন: old.stem[4:] হলো "0001", int() তাকে 1 বানায়, আর :03d তিন অঙ্কে 001 করে। নতুন নাম থাকলে আমরা নিজেই FileExistsError তুলেছি, যাতে ওভাররাইট না হয়, আর except OSError সেটি (এবং অনুমতির মতো অন্য ফাইল-error) ধরে শুধু ওই ফাইলটি বাদ দেয়। sorted থাকায় আউটপুট প্রতিবার একই ক্রমে।

exists() দিয়ে যাচাই কি পুরোপুরি নিরাপদ? প্রায়, তবে নিখুঁত নয়। exists() আর rename()-এর মাঝের ক্ষুদ্র সময়ে আরেকটি প্রোগ্রাম ওই নামে ফাইল বানিয়ে ফেলতে পারে — একে বলে TOCTOU (time-of-check to time-of-use) সমস্যা। নিজের কম্পিউটারে ছবির ফোল্ডারে এটি প্রায় অবাস্তব; কিন্তু সার্ভারে, যেখানে অনেক প্রোগ্রাম একসাথে চলে, সেখানে open(path, "x")-এর মতো এক ধাক্কার কাজ বেশি নিরাপদ। এই ধরনের নিরাপত্তার কথা পর্ব ৫০-এ আরও আসবে।

ডাউনলোড ফোল্ডার সাজানো

পর্ব ২৫-এ extension অনুযায়ী ফাইল সাজানোর ধারণা দেখেছেন। টুল হিসেবে এর সাথে যোগ হয় dry-run আর সংঘর্ষের নিয়ম। extension-কে lower() করতে ভুলবেন না, নাহলে .JPG আর .jpg দুই জায়গায় যাবে:

import shutil
from pathlib import Path

KIND = {".pdf": "Documents", ".docx": "Documents", ".jpg": "Images",
        ".png": "Images", ".mp4": "Videos", ".zip": "Archives"}

def organize(folder: Path, dry_run: bool) -> int:
    problems = 0
    for f in sorted(folder.iterdir()):
        if not f.is_file():
            continue
        target_dir = folder / KIND.get(f.suffix.lower(), "Others")
        target = target_dir / f.name
        if target.exists():
            print(f"[বাদ] {f.name}: {target_dir.name}-এ একই নামের ফাইল আছে")
            problems += 1
            continue
        print(f"{'[dry-run] ' if dry_run else ''}{f.name} → {target_dir.name}/")
        if not dry_run:
            target_dir.mkdir(exist_ok=True)
            shutil.move(f, target)
    return problems

বড় আর পুরোনো ফাইল খোঁজা — মুছবেন না, শুধু দেখান

ল্যাপটপের ডিস্ক ভরে গেছে? rglob("*") দিয়ে সব ফাইল ঘুরে stat() থেকে আকার (st_size, বাইটে) আর শেষ বদলের সময় (st_mtime) দেখা যায়। ১০০ MB-র বড় বা ১৮০ দিনের বেশি না-ছোঁয়া ফাইলের একটি তালিকা বানান — কিন্তু স্ক্রিপ্টকে নিজে থেকে মুছতে দেবেন না। মোছার সিদ্ধান্ত মানুষের; স্ক্রিপ্টের কাজ তালিকা দেওয়া, বড় থেকে ছোট ক্রমে।

import time
from pathlib import Path

LIMIT = 100 * 1024 * 1024                    # 100 MB
cutoff = time.time() - 180 * 24 * 3600       # ১৮০ দিন আগের মুহূর্ত
found = []
for p in Path.home().joinpath("Downloads").rglob("*"):
    try:
        if p.is_file():
            st = p.stat()
            if st.st_size > LIMIT or st.st_mtime < cutoff:
                found.append((st.st_size, p))
    except OSError:                          # অনুমতি নেই বা ফাইল উধাও — বাদ দিন
        continue
for size, p in sorted(found, reverse=True):
    print(f"{size / 1e6:9.1f} MB  {p}")

থিসিস ফোল্ডারের ব্যাকআপ আর zip

থিসিস জমার দুই সপ্তাহ আগে ল্যাপটপ নষ্ট — এই দুঃস্বপ্নের ওষুধ একটি ছোট ব্যাকআপ স্ক্রিপ্ট। প্রতিবার সময়-চিহ্নসহ একটি নতুন archive বানান, যাতে পুরোনো ব্যাকআপ ওভাররাইট না হয়; বানানোর পর zip-টি ঠিক আছে কি না যাচাই করুন; আর শুধু শেষ ৭টি রেখে বাকিগুলো সরান। shutil.make_archive এক লাইনে পুরো ফোল্ডার zip করে, আর zipfile module দিয়ে zip খুলে পরীক্ষা করা যায়:

import shutil
import zipfile
from datetime import datetime
from pathlib import Path

def backup(src: Path, dest: Path, keep: int = 7) -> Path:
    dest.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
    archive = Path(shutil.make_archive(str(dest / f"{src.name}_{stamp}"), "zip", root_dir=src))
    with zipfile.ZipFile(archive) as z:
        if z.testzip() is not None:              # কোনো ফাইল নষ্ট হলে তার নাম আসে
            raise RuntimeError(f"ব্যাকআপ নষ্ট: {archive.name}")
    old = sorted(dest.glob(f"{src.name}_*.zip"))[:-keep]   # নাম = সময়, তাই sorted = পুরোনো আগে
    for p in old:
        print(f"পুরোনো ব্যাকআপ সরানো হলো: {p.name}")
        p.unlink()
    return archive

এখানে মোছা হচ্ছে, কিন্তু তিনটি শর্তে নিরাপদ: শুধু এই স্ক্রিপ্টের বানানো নামের ধাঁচ (thesis_*.zip) মেলে এমন ফাইল; নতুন ব্যাকআপ সফলভাবে যাচাই হওয়ার পরে; আর সবসময় অন্তত ৭টি থেকে যায়। গন্তব্য যেন আলাদা ডিস্ক, পেনড্রাইভ বা ক্লাউড-সিঙ্ক ফোল্ডার হয় — একই ডিস্কে ব্যাকআপ ডিস্ক নষ্ট হলে কোনো কাজে আসে না।

৬ · অটোমেশনের নিরাপত্তা — একটি ফাইলের ভুল যেন সব না থামায়

হাতে কাজ করলে আপনি প্রতিটি ধাপ চোখে দেখেন। স্ক্রিপ্ট দেখে না — হাজারটি ফাইলে এক সেকেন্ডে একই সিদ্ধান্ত নেয়। তাই অটোমেশন স্ক্রিপ্টের জন্য পাঁচটি নিয়ম মানুন:

  1. আগে dry-run. যে স্ক্রিপ্ট কিছু বদলায়, মোছে বা সরায়, তাতে --dry-run থাকবে। আরও সাবধানী নকশা হলো উল্টোটা — ডিফল্টে শুধু দেখানো, আর আসল কাজের জন্য স্পষ্ট --apply লাগবে।
  2. নিশ্চিত না হয়ে মোছা নয়। মোছার আগে তালিকা দেখিয়ে input("মুছব? (y/N) ")-এ স্পষ্ট y চান, ডিফল্ট “না”। cron-এর মতো মানুষ-ছাড়া পরিবেশে --yes flag দিয়ে সচেতনভাবে অনুমতি দিন। আরও ভালো: মোছার বদলে একটি trash/ ফোল্ডারে সরান।
  3. যা করলেন তা লিখে রাখুন। পর্ব ৪০-এর logging দিয়ে প্রতিটি বদল (পুরোনো → নতুন) একটি log ফাইলে। কিছু ভুল হলে এই log-ই বলবে কী ঘটেছে, আর দরকারে উল্টো দিকে ফেরানোর তালিকা দেবে।
  4. প্রতিটি ফাইলের error আলাদা করে ধরুন। লুপের ভেতরে try/except, বাইরে নয়। শেষে সারাংশ, আর কিছু ব্যর্থ হলে exit code 1.
  5. দুবার চালালেও যেন ক্ষতি না হয়। স্ক্রিপ্ট মাঝপথে থেমে গেলে (বিদ্যুৎ গেল, ল্যাপটপ ঘুমিয়ে পড়ল) আবার চালানোই স্বাভাবিক সমাধান — তাই আবার চালানো নিরাপদ হতে হবে।

চতুর্থ নিয়মটি সবচেয়ে বেশি উপেক্ষিত। লুপের বাইরে একটি বড় try থাকলে প্রথম নষ্ট ফাইলেই পুরো কাজ থেমে যায় — আর অর্ধেক ফাইল বদলানো, অর্ধেক পুরোনো, এমন বিশৃঙ্খল অবস্থা থেকে যায়। ১০টি স্ক্যান করা কাগজের ছবি ছোট করার একটি কাজ দেখুন, যেখানে ৫ নম্বর ফাইলটি পড়া যায় না:

1ok, failed = 0, []2for f in sorted(folder.glob("*.jpg")):3    try:4        shrink(f)            # ছবি ছোট করা5        ok += 16    except OSError as e:7        log.error("%s: %s", f.name, e)8        failed.append(f.name)9print(f"{ok}টি সফল, {len(failed)}টি ব্যর্থ")10sys.exit(1 if failed else 0)
অবস্থা (সবুজ লাইনগুলো এই ধাপে চলছে)
এখনকার ফাইল
ok
failed
  1. প্রস্তুতি। দুটি হিসাব — কয়টি সফল, আর কোনগুলো ব্যর্থ (নামসহ, যাতে পরে আবার চেষ্টা করা যায়)। তালিকা sorted, তাই প্রতিবার একই ক্রম।
  2. প্রথম চারটি ঠিকঠাক। প্রতিটি ফাইল নিজের try-এর ভেতরে। সফল হলে ok বাড়ে।
  3. ৫ নম্বর ব্যর্থ। ফাইলটি নষ্ট, shrink() exception তুলল। except সেটি ধরে log-এ নাম ও কারণ লিখল, তালিকায় যোগ করল — আর লুপ পরের ফাইলে চলে গেল। try লুপের বাইরে থাকলে এখানেই সব থেমে যেত, ৬–১০ কখনো হতো না।
  4. বাকি পাঁচটি। একটি নষ্ট ফাইলের দাম শুধু সেই ফাইলটি। ৯টি ছবি তৈরি।
  5. সারাংশ আর exit code. মানুষের জন্য এক লাইনের হিসাব, আর মেশিনের জন্য exit code 1 — “কাজ হয়েছে, কিন্তু পুরোপুরি নয়”। কিছু টুল আংশিক সাফল্যের জন্য আলাদা সংখ্যা (যেমন 2) ব্যবহার করে; যা-ই করুন, help-এ লিখে দিন।

except Exception: pass লিখবেন না। প্রতিটি ফাইলের error ধরা মানে error গিলে ফেলা নয় (পর্ব ২৪)। প্রতিটি ব্যর্থতা log-এ যাবে, সারাংশে গোনা হবে, আর exit code-এ প্রতিফলিত হবে। আর খুব চওড়া ধরবেন না: ফাইলের সমস্যার জন্য OSError (আর দরকারে ValueError) ধরুন; আপনার নিজের কোডের বাগ (NameError, TypeError) যেন চাপা না পড়ে। KeyboardInterrupt (Ctrl+C) তো কখনোই নয় — ব্যবহারকারী থামাতে চাইলে টুল থামবে।

Idempotency — দ্বিতীয় রান নিরাপদ কীভাবে করবেন

কিছু কৌশল প্রায় সব অটোমেশনে কাজে লাগে: নতুন নাম আসুক তথ্য থেকে (পুরোনো নাম, তারিখ), গণনা থেকে নয়; “যোগ করো”-র বদলে “এমন অবস্থা নিশ্চিত করো” ভাবুন — mkdir(exist_ok=True), “ফাইল আছে কি না দেখে তারপর কপি”; আউটপুট ফাইল প্রতিবার পুরোটা নতুন করে বানান, পুরোনোর শেষে যোগ নয়; আর যা আগে করা হয়ে গেছে তা চিনে বাদ দিন। মাসিক রিপোর্ট জোড়ার স্ক্রিপ্ট দুবার চালালে যেন বিক্রি দ্বিগুণ না দেখায় — পরের অংশে ঠিক এটাই দেখবেন।

৭ · রিপোর্ট জোড়া — ১২ মাসের CSV এক ফাইলে

একটি পোশাক কোম্পানির ঢাকা, চট্টগ্রাম, সিলেট শাখা প্রতি মাসে একটি করে বিক্রির ফাইল পাঠায়: sales_2026_01.csv, sales_2026_02.csv… কলাম branch,product,amount. বছর শেষে হিসাবরক্ষক ১২টি ফাইল খুলে কপি-পেস্ট করে একটি বড় শিটে জোড়েন, তারপর শাখাভিত্তিক মোট বের করেন। এক দিনের কাজ, আর প্রতি বছর কোথাও না কোথাও একটি মাস দুবার পেস্ট হয়। পর্ব ২৬-এর csv module দিয়ে পুরোটা একটি স্ক্রিপ্ট:

import csv
import sys
from pathlib import Path

def merge(folder: Path, out_path: Path) -> int:
    totals: dict[str, int] = {}
    bad = 0
    with open(out_path, "w", newline="", encoding="utf-8") as out:   # প্রতিবার নতুন ফাইল
        writer = csv.writer(out)
        writer.writerow(["month", "branch", "product", "amount"])
        for path in sorted(folder.glob("sales_2026_*.csv")):
            month = path.stem.rsplit("_", 1)[-1]                       # sales_2026_03 → 03
            try:
                with open(path, newline="", encoding="utf-8-sig") as f:  # Excel-এর BOM সামলায়
                    rows = [(r["branch"], r["product"], int(r["amount"]))
                            for r in csv.DictReader(f)]
            except (OSError, KeyError, ValueError) as e:
                print(f"[বাদ] {path.name}: {e!r}", file=sys.stderr)
                bad += 1
                continue
            for branch, product, amount in rows:                       # পুরো ফাইল ঠিক হলে তবেই লেখা
                writer.writerow([month, branch, product, amount])
                totals[branch] = totals.get(branch, 0) + amount
    for branch in sorted(totals):
        print(f"{branch}: {totals[branch]:,} টাকা")
    return 1 if bad else 0

তিনটি নকশার সিদ্ধান্ত লক্ষ করুন। এক, প্রতিটি ফাইল আগে পুরোটা পড়ে list-এ রাখা হয়েছে; একটি সারিতে ভুল (amount-এ “১২হাজার”) হলে সেই ফাইলের কোনো সারিই লেখা হয় না — অর্ধেক ফাইল ঢুকে যাওয়ার চেয়ে পুরো ফাইল বাদ দিয়ে জানানো অনেক পরিষ্কার। দুই, আউটপুট "w" মোডে প্রতিবার নতুন করে বানানো হয়, তাই স্ক্রিপ্ট দুবার চালালেও কিছু দ্বিগুণ হয় না — idempotent. তিন, মাসের নম্বর ফাইলের নাম থেকে নেওয়া, যাতে জোড়া ফাইলেও বোঝা যায় কোন সারি কোন মাসের। {:,} হাজারের কমা বসায় (1,250,000).

Excel ফাইল হলে?

অফিসের রিপোর্ট প্রায়ই .xlsx. standard library-তে Excel পড়ার module নেই; প্রচলিত দুটি পথ হলো তৃতীয় পক্ষের openpyxl (সরাসরি শিট, সেল, ফরম্যাটিং) আর পর্ব ৪৬-এর pandas (যা ভেতরে openpyxl ব্যবহার করে)। pandas-এ ১২টি ফাইল জোড়া কয়েক লাইনের কাজ। এগুলো pip দিয়ে আলাদা ইনস্টল করতে হয় (পর্ব ২৩), তাই এই পাতায় চালানো যাবে না:

# pip install pandas openpyxl   (নিজের venv-এ)
from pathlib import Path
import pandas as pd

frames = [pd.read_excel(p).assign(month=p.stem[-2:])
          for p in sorted(Path("reports").glob("sales_2026_*.xlsx"))]
summary = pd.concat(frames, ignore_index=True)
summary.to_excel("summary_2026.xlsx", index=False)
print(summary.groupby("branch")["amount"].sum())

৮ · অন্য প্রোগ্রাম চালানো — subprocess.run

সব কাজ Python-এ নতুন করে লেখার দরকার নেই। git, ffmpeg (ভিডিও), ImageMagick (ছবি), pg_dump (ডেটাবেস ব্যাকআপ) — এসব প্রোগ্রাম আগেই আছে। Python স্ক্রিপ্ট এদের “আঠা” হিসেবে জুড়ে দিতে পারে subprocess module দিয়ে:

import subprocess

result = subprocess.run(
    ["git", "log", "--oneline", "-5"],     # প্রোগ্রাম আর আর্গুমেন্ট — list আকারে
    capture_output=True,                   # stdout/stderr ধরে রাখো, পর্দায় নয়
    text=True,                             # bytes নয়, str দাও
    check=True,                            # exit code অশূন্য হলে CalledProcessError
    timeout=30,                            # আটকে গেলে TimeoutExpired
)
print(result.returncode)                   # 0
print(result.stdout)

প্রতিটি আর্গুমেন্টের কারণ আছে। check=True না দিলে প্রোগ্রাম ব্যর্থ হলেও Python চুপচাপ পরের লাইনে চলে যায় — আপনার স্ক্রিপ্ট ভাববে ব্যাকআপ হয়ে গেছে। প্রোগ্রামটি কম্পিউটারে না থাকলে আসে FileNotFoundError. timeout না দিলে কোনো প্রোগ্রাম প্রশ্ন করে ইনপুটের অপেক্ষায় থাকলে আপনার cron-এর কাজ চিরকাল আটকে থাকতে পারে। subprocess.run এসেছে Python 3.5-এ; capture_output আর text 3.7-এ।

সবচেয়ে বড় বিপদ: shell=True আর ব্যবহারকারীর ইনপুট

subprocess.run("ping -c 1 " + host, shell=True) — দেখতে সহজ, AI-ও প্রায়ই লেখে। কিন্তু shell=True মানে পুরো string-টি একটি shell-কে দেওয়া হয়, আর shell ;, &&, |, $( )-এর মতো চিহ্নকে নির্দেশ হিসেবে পড়ে। ইনপুটটি যদি ব্যবহারকারী বা কোনো ফাইল থেকে আসে, তাহলে কী হয়, দেখুন:

ব্যবহারকারীর ইনপুট (ওয়েব ফর্ম, CSV বা argv থেকে)অবিশ্বস্ত
host = "8.8.8.8; rm -rf ~"
দেখতে একটি IP ঠিকানা, কিন্তু শেষে একটি লুকানো নির্দেশ
subprocess.run(f"ping -c 1 {host}", shell=True)✘ shell string
shell যা পড়ল:  ping -c 1 8.8.8.8 ; rm -rf ~
নির্দেশ ১:      ping -c 1 8.8.8.8
নির্দেশ ২:      rm -rf ~      ← হোম ফোল্ডারের সব মুছে দেওয়ার নির্দেশ
subprocess.run(["ping", "-c", "1", host])✔ list আকার
argv[0] = "ping"
argv[1] = "-c"
argv[2] = "1"
argv[3] = "8.8.8.8; rm -rf ~"   ← পুরোটা একটি আর্গুমেন্ট, কোনো shell নেই
ping: 8.8.8.8; rm -rf ~: Name or service not known   (ক্ষতিহীন ব্যর্থতা)
আরও এক ধাপ সাবধানতাযাচাই + check
ipaddress.ip_address(host)      # ভুল হলে ValueError — চালানোর আগেই থামুন
subprocess.run(["ping", "-c", "1", host],
               capture_output=True, text=True,
               check=True, timeout=10)
ইনপুট "-"-দিয়ে শুরু হলে ping সেটিকে option ভাবতে পারে — যাচাই সেটাও আটকায়
  1. অবিশ্বস্ত ইনপুট। যা আপনার কোডের বাইরে থেকে আসে — ফর্ম, ফাইলের নাম, CSV-র ঘর, এমনকি argv — তা অবিশ্বস্ত। এখানে ইনপুটে ;-এর পরে একটি দ্বিতীয় নির্দেশ লুকানো।
  2. Shell string-এ বিপর্যয়। shell ;-কে “একটি নির্দেশ শেষ, আরেকটি শুরু” হিসেবে পড়ে। ফলে ping চলার পর আপনার প্রোগ্রামের অনুমতিতে দ্বিতীয় নির্দেশটিও চলে। এই আক্রমণের নাম command injection, পর্ব ৫০-এর অন্যতম প্রধান বিষয়।
  3. List আকারে shell-ই নেই। Python সরাসরি ping চালায় আর প্রতিটি list-এর ঘর হুবহু একটি আর্গুমেন্ট হিসেবে দেয়। ; এখানে শুধু একটি অক্ষর। সবচেয়ে খারাপ ফল: ping একটি অদ্ভুত নাম খুঁজে পায় না।
  4. যাচাই আর check. list আকার shell-এর বিপদ সরায়, কিন্তু ইনপুটটি অর্থবহ কি না যাচাই করা তবুও আপনার দায়িত্ব — - দিয়ে শুরু ইনপুট প্রোগ্রামের option হিসেবে পড়া হতে পারে (argument injection). ipaddress দিয়ে যাচাই, আর check=True ও timeout দিয়ে ব্যর্থতা ধরা — এই তিনে মিলে নিরাপদ।

নিয়ম সহজ: সবসময় list আকারে। shell=True শুধু তখন, যখন পুরো নির্দেশটি আপনার নিজের লেখা, কোনো বাইরের অংশ নেই — আর তখনও প্রায়ই দরকার হয় না। পাইপ (|) বা wildcard (*.jpg)-এর জন্য shell লাগবে মনে হলে, Python-এই করুন: glob, আর এক প্রোগ্রামের ফল আরেকটিতে দিতে input=result.stdout. মনে রাখবেন, ping-এর -c Linux/macOS-এর; Windows-এ -n — বাইরের প্রোগ্রাম মানে প্ল্যাটফর্মের পার্থক্যও আপনার মাথাব্যথা।

৯ · সময়মতো চালানো আর সবার জন্য কমান্ড বানানো

প্রতি রাত ১০টায় থিসিসের ব্যাকআপ — এটা মনে রেখে প্রতিদিন হাতে চালানো তো অটোমেশন হলো না। কাজটি OS-এর scheduler-কে দিন। Linux/macOS-এ cron: crontab -e খুলে প্রতিটি লাইনে পাঁচটি সময়ের ঘর (মিনিট, ঘণ্টা, মাসের দিন, মাস, সপ্তাহের দিন) আর একটি কমান্ড:

# মিনিট ঘণ্টা দিন মাস সপ্তাহের-দিন  কমান্ড
0 22 * * *   /home/rafi/thesis-tools/.venv/bin/python /home/rafi/thesis-tools/backup.py --dest /media/rafi/USB >> /home/rafi/backup.log 2>&1
30 9 1 * *   /home/rafi/thesis-tools/.venv/bin/python /home/rafi/thesis-tools/merge_sales.py   # প্রতি মাসের ১ তারিখ সকাল ৯:৩০

Windows-এ একই কাজ করে Task Scheduler — গ্রাফিক্যাল অ্যাপে “Create Basic Task”, অথবা কমান্ডে:

schtasks /create /tn "ThesisBackup" /sc daily /st 22:00 /tr "C:\Users\Rafi\thesis-tools\.venv\Scripts\python.exe C:\Users\Rafi\thesis-tools\backup.py"

scheduler-এর তিনটি ফাঁদ। (১) working directory আলাদা — cron আপনার প্রজেক্ট ফোল্ডারে চালায় না, তাই Path("todo.json")-এর মতো relative path অন্য জায়গায় খোঁজে; স্ক্রিপ্টের নিজের ফোল্ডার থেকে path বানান: Path(__file__).resolve().parent / "todo.json". (২) পরিবেশ আলাদা — PATH ছোট, venv activate নেই; তাই venv-এর python-এর পূর্ণ path দিন। (৩) কেউ পর্দা দেখছে না — আউটপুট আর error log ফাইলে পাঠান (উপরের >> backup.log 2>&1), আর exit code সঠিক দিন। ল্যাপটপ বন্ধ থাকলে cron ওই সময়ের কাজ বাদ দেয়, পরে নিজে থেকে চালায় না।

Python-এর ভেতরেই সময় ধরে চালাতে তৃতীয় পক্ষের schedule লাইব্রেরি জনপ্রিয়: schedule.every().day.at("22:00").do(backup), তারপর একটি অসীম লুপে schedule.run_pending(). সহজ, কিন্তু শর্ত হলো প্রোগ্রামটিকে সারাক্ষণ চালু থাকতে হবে — বন্ধ হলে বা কম্পিউটার রিস্টার্ট হলে সময়সূচিও শেষ। তাই নিয়মিত কাজে OS-এর scheduler সাধারণত বেশি নির্ভরযোগ্য; schedule মানায় এমন প্রোগ্রামে যা এমনিতেই সারাক্ষণ চলে (যেমন একটি ছোট সার্ভার)।

স্ক্রিপ্টকে আসল কমান্ড বানানো

python /path/to/rename_photos.py ... লেখা ঝামেলার। পর্ব ২৩-এ দেখেছেন, pyproject.toml-এর [project.scripts] অংশ একটি entry point ঘোষণা করে — ইনস্টলের পর terminal-এ শুধু টুলের নাম লিখলেই চলে:

[project]
name = "biye-rename"
version = "0.1.0"
requires-python = ">=3.10"

[project.scripts]
biye-rename = "biye_rename.cli:main"     # কমান্ডের নাম = "package.module:function"
$ python -m pip install -e .          # নিজের venv-এ, ডেভেলপমেন্টের সময়
$ biye-rename photos --prefix biye_2026 --dry-run
$ pipx install .                      # সব জায়গা থেকে চালাতে, আলাদা পরিবেশে

pip যে ছোট চালানোর ফাইলটি বানায়, সেটি sys.exit(main()) ডাকে — তাই main()-এর ফেরত দেওয়া সংখ্যাই exit code হয়। অংশ ৪-এর কাঠামো এখানে সরাসরি কাজে লাগল। pipx প্রতিটি টুলকে নিজের venv-এ ইনস্টল করে, কিন্তু কমান্ডটি সবখানে পাওয়া যায় — নিজের বানানো টুল বা ruff-এর মতো টুলের জন্য আদর্শ।

argparse ছাড়া বিকল্প: click আর typer

বড় CLI-তে তৃতীয় পক্ষের দুটি লাইব্রেরি জনপ্রিয়। click (Flask-এর নির্মাতা Armin Ronacher-এর প্রকল্প থেকে আসা) decorator দিয়ে কমান্ড ঘোষণা করে, আর রঙিন আউটপুট, নিশ্চিতকরণ প্রশ্ন, progress bar-এর সুবিধা দেয়। typer (FastAPI-র নির্মাতা Sebastián Ramírez-এর) click-এর ওপর তৈরি, আর আপনার ফাংশনের type hint (পর্ব ৩৭) পড়েই আর্গুমেন্ট বানায়:

# pip install typer   (নিজের venv-এ)
import typer

def main(folder: str, prefix: str = "photo", dry_run: bool = False) -> None:
    """ছবির নাম একসাথে বদলায়।"""
    typer.echo(f"{folder=} {prefix=} {dry_run=}")

if __name__ == "__main__":
    typer.run(main)          # folder → positional, prefix → --prefix, dry_run → --dry-run

কোনটি বাছবেন? ছোট টুল, কোনো ইনস্টল ছাড়া যেকোনো কম্পিউটারে চলতে হবে, বা স্ক্রিপ্ট একটি ফাইলে কাউকে পাঠাবেন — argparse. অনেক subcommand-এর বড় টুল, যেখানে আগে থেকেই dependency আছে — typer বা click লেখা কমায়। ধারণাগুলো (positional, flag, default, help, exit code) সবখানে একই; argparse বুঝলে বাকিগুলো এক বিকেলের কাজ।

C/C++ জানলে

C-র int main(int argc, char *argv[])-ই Python-এর sys.argv-এর পূর্বপুরুষ — argv[0]-এ প্রোগ্রামের নাম, প্রথা একই। main-এর return মানই exit code, Python-এ যা sys.exit(main()). C-তে option parse করতে হয় হাতে বা POSIX-এর getopt দিয়ে; argparse সেই কাজের সাথে type রূপান্তর আর help-ও দেয়। আর subprocess.run([...]) আসলে POSIX-এ fork + exec-এর মোড়ক, যেখানে C-র system() হলো shell=True-এর মতো। বিস্তারিত: C কোর্সের command-line argument পর্ব আর process ও IPC পর্ব.

পরীক্ষা ও ইন্টারভিউতে
  • sys.argv[0]-এ কী থাকে, আর আর্গুমেন্টের টাইপ কী? — স্ক্রিপ্টের নাম; সব আর্গুমেন্টই str, সংখ্যা চাইলে রূপান্তর লাগে (argparse-এ type=int).
  • action="store_true" কী করে? — flag দিলে মান True, না দিলে False; কোনো মান লাগে না।
  • exit code 0 আর অশূন্যের অর্থ? — 0 মানে সফল, অশূন্য মানে সমস্যা; argparse ব্যবহারের ভুলে 2 দেয়।
  • subprocess.run-এ shell=True কেন বিপজ্জনক? — বাইরের ইনপুট shell-এর নির্দেশ হিসেবে চলতে পারে (command injection); list আকারে আর্গুমেন্ট দিন।
  • idempotent স্ক্রিপ্ট কী? — যা একবার আর একাধিকবার চালালে একই শেষ অবস্থা হয়; মাঝপথে থামলে আবার চালানো নিরাপদ।

১০ · AI-এর যুগে — AI-এর লেখা অটোমেশন স্ক্রিপ্ট যাচাই

“আমার ফোল্ডারের সব ছবির নাম বদলে দাও” — AI এক সেকেন্ডে একটি স্ক্রিপ্ট দেয়, আর সেটি সাধারণত চলেও। বিপদ হলো, অটোমেশন স্ক্রিপ্ট আপনার আসল ফাইল ছোঁয়, আর AI আপনার ফোল্ডারের বাস্তব অবস্থা জানে না: আগে থেকে থাকা নাম, নষ্ট ফাইল, Windows-এর path, ১০,০০০ ফাইলের পরিমাণ। AI-এর লেখা অটোমেশনে এই ভুলগুলো বারবার দেখা যায়:

AI প্রায়ই যা লেখেসমস্যাভালো উপায়
সরাসরি rename/remove, কোনো dry-run নেইপ্রথম রানেই ভুলের পূর্ণ ক্ষতি--dry-run (বা ডিফল্টে শুধু দেখানো, --apply-এ আসল কাজ)
লক্ষ্যে একই নাম আছে কি না দেখে নানিঃশব্দে ওভাররাইট, ছবি হারায়exists() যাচাই, সংঘর্ষে বাদ বা নতুন নাম
subprocess.run(f"... {name}", shell=True)ফাইলের নামে ; বা স্পেস থাকলে ভাঙে বা injectionlist আকার, check=True, timeout
"C:\\Users\\Rafi\\Pictures" কোডে লেখাঅন্য কারো কম্পিউটারে চলে নাargparse-এ positional আর্গুমেন্ট, pathlib
পুরো লুপ একটি বড় try-তে, বা except: passপ্রথম ভুলে সব থামে, বা ভুল চাপা পড়েলুপের ভেতরে প্রতিটি ফাইলের জন্য try, log, সারাংশ
সব সময় exit code 0, error print-এcron/CI ব্যর্থতা টের পায় নাstderr-এ বার্তা, ব্যর্থ হলে sys.exit(1)
নম্বর দেয় লুপের গণনা থেকেদ্বিতীয় রানে নাম সরে যায়, সংঘর্ষতথ্য থেকে নাম (পুরোনো নম্বর, তারিখ) — idempotent

AI-কে অটোমেশন স্ক্রিপ্ট লিখতে বললে নিরাপত্তার শর্তগুলো শুরুতেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ অনুযায়ী বদলে নিন:

আমি Python 3.12+ ব্যবহার করি, Windows আর Linux দুই জায়গাতেই চলবে। একটি CLI টুল লিখে দাও যা একটি ফোল্ডারের IMG_0001.jpg ধরনের ছবির নাম biye_2026_001.jpg ধরনে বদলায়। শর্ত: ১. শুধু standard library: argparse, pathlib, shutil, logging. ফোল্ডার আর prefix আর্গুমেন্ট হিসেবে নেবে, কোনো path কোডে লেখা থাকবে না; প্রতিটি আর্গুমেন্টে help দাও। ২. ডিফল্টে শুধু পরিকল্পনা দেখাবে (পুরোনো → নতুন); আসল নাম বদল হবে শুধু --apply দিলে। ৩. নতুন নাম আগে থেকে থাকলে কখনো ওভাররাইট করবে না — সেই ফাইল বাদ দিয়ে জানাবে। কিছুই মুছবে না। ৪. প্রতিটি ফাইলের error লুপের ভেতরে আলাদা করে ধরবে, শুধু OSError; শেষে সফল আর ব্যর্থের সংখ্যা দেখাবে। ৫. প্রতিটি বদল logging দিয়ে rename.log-এ লিখবে; error বার্তা stderr-এ; কিছু ব্যর্থ হলে exit code 1. ৬. দুবার চালালেও যেন নাম না সরে — নতুন নম্বর পুরোনো নামের নম্বর থেকে নেবে, লুপের গণনা থেকে নয়। ৭. main(argv) -> int কাঠামো আর if __name__ == "__main__" দাও, আর একটি অস্থায়ী ফোল্ডারে পরীক্ষার ছোট pytest টেস্ট লিখে দাও। ৮. subprocess লাগলে list আকারে, shell=True কখনো নয়।

AI-এর অটোমেশন স্ক্রিপ্ট আসল ফাইলে চালানোর আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Unix দর্শন — “একটি কাজ ভালোভাবে করুন, আর অন্যের সাথে জুড়ে যান” — কোথা থেকে এল? এটি একটি ভালো CLI টুলের নকশা কীভাবে ঠিক করে দেয়?

১৯৭০-এর দশকে AT&T-র Bell Labs-এ Unix তৈরির সময় Doug McIlroy দীর্ঘদিন ধরে একটি ধারণার পক্ষে বলছিলেন: প্রোগ্রামগুলোকে বাগানের পানির পাইপের মতো জোড়া লাগানো যাওয়া উচিত। ১৯৭৩ সালে Ken Thompson Unix-এ pipe (|) যোগ করেন, আর রাতারাতি ছোট ছোট প্রোগ্রাম জুড়ে বড় কাজ করা সম্ভব হলো। ১৯৭৮ সালে Bell System Technical Journal-এর Unix বিশেষ সংখ্যার ভূমিকায় McIlroy ও তাঁর সহলেখকেরা এই অভিজ্ঞতা কয়েকটি নীতিতে লেখেন। তার মূল কথা: প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করুক; নতুন কাজের জন্য পুরোনো প্রোগ্রামে বৈশিষ্ট্য জুড়ে জটিল না করে নতুন প্রোগ্রাম লিখুন; আর ধরে নিন প্রতিটি প্রোগ্রামের আউটপুট হবে অন্য কোনো, এখনো অজানা প্রোগ্রামের ইনপুট — তাই আউটপুটে অপ্রয়োজনীয় সাজসজ্জা দেবেন না। পরে Peter Salus সংক্ষেপে বলেন: একটি কাজ ভালোভাবে করে এমন প্রোগ্রাম লিখুন, প্রোগ্রাম লিখুন যারা একসাথে কাজ করে, আর text stream সামলায় এমন প্রোগ্রাম লিখুন, কারণ সেটাই সার্বজনীন ইন্টারফেস।

এই দর্শন সরাসরি এই পর্বের নিয়মগুলো ব্যাখ্যা করে। আসল ফল stdout-এ আর বার্তা stderr-এ কেন? কারণ stdout-এ যা যায় তা পাইপ দিয়ে পরের প্রোগ্রামে ঢোকে — সেখানে “৩টি ফাইল পাওয়া গেছে!” জাতীয় লেখা মিশলে পরের প্রোগ্রাম বিভ্রান্ত হবে। exit code কেন গুরুত্বপূর্ণ? কারণ shell-এ backup.py && upload.sh লিখলে দ্বিতীয়টি চলে শুধু প্রথমটি 0 দিলে — exit code-ই প্রোগ্রামগুলোর মধ্যে “সব ঠিক?” প্রশ্নের উত্তর। ইন্টারঅ্যাকটিভ প্রশ্নের বদলে আর্গুমেন্ট কেন? কারণ প্রশ্ন করা প্রোগ্রাম cron বা অন্য স্ক্রিপ্ট চালাতে পারে না।

দর্শনটির সমালোচনাও আছে। text stream-এ কাঠামো হারায় — কলাম কোথায় শেষ, তা প্রতিটি প্রোগ্রামকে নতুন করে অনুমান করতে হয়। এজন্য আধুনিক টুল প্রায়ই --json আউটপুটের বিকল্প দেয়, আর PowerShell টেক্সটের বদলে object পাইপ করে। আবার git-এর মতো বড় টুল “একটি কাজ” নয়, কিন্তু প্রতিটি subcommand নিজে একটি ছোট, জোড়া-যোগ্য টুলের মতো আচরণ করে।

আপনার Python টুলের জন্য বাস্তব প্রশ্নগুলো তাই: আমার টুল কি অন্য স্ক্রিপ্ট থেকে প্রশ্ন ছাড়াই চালানো যায়? এর আউটপুট কি আরেকটি প্রোগ্রাম পড়তে পারে? ব্যর্থ হলে কি অন্য প্রোগ্রাম তা জানতে পারে?

মূল উপলব্ধি: একটি CLI টুলের আসল ব্যবহারকারী প্রায়ই আরেকটি প্রোগ্রাম। ছোট, পরিষ্কার ইন্টারফেস (argv ঢোকে, stdout বের হয়, exit code বলে কী হলো) টুলকে এমন কাজেও লাগায়, যা নির্মাতা কখনো ভাবেননি।

প্র ০২Python-এ কমান্ড-লাইন parse করার তিনটি module এল কেন — getopt, optparse, তারপর argparse? PEP 389 কী সমস্যার সমাধান করেছিল?

Python-এর প্রথম দিকের getopt module ছিল C-র POSIX getopt() ফাংশনের প্রায় হুবহু অনুকরণ। আপনি option-এর অক্ষরগুলো একটি সংক্ষিপ্ত string-এ দিতেন ("hvo:" — o-র পরে কোলন মানে এর একটি মান লাগবে), আর সে ফেরত দিত (option, মান) জোড়ার একটি list. বাকি সব — মান যাচাই, টাইপ রূপান্তর, help লেখা, ভুলের বার্তা — আপনার দায়িত্ব। C প্রোগ্রামারদের কাছে চেনা, কিন্তু প্রতিটি স্ক্রিপ্টে একই বিরক্তিকর কোড।

২০০০-এর দশকের শুরুতে Greg Ward একটি তৃতীয় পক্ষের লাইব্রেরি বানান, নাম Optik, যা পরে optparse নামে Python 2.3-এ standard library-তে আসে। optparse option-কে object হিসেবে ঘোষণা করত — type, default, help, action (store_true এখান থেকেই চেনা) — আর নিজে থেকে help বানাত। বিশাল উন্নতি। কিন্তু এর দর্শন ছিল: option ঐচ্ছিক, আর বাকি সব “positional” শব্দ শুধু একটি list হিসেবে ফেরত দেওয়া হবে — সেগুলোর অর্থ, সংখ্যা, টাইপ যাচাই আবার আপনার কাজ। subcommand (git commit-এর মতো) সমর্থন ছিল না, আর একটি option-এ অনির্দিষ্ট সংখ্যক মান নেওয়াও কঠিন ছিল।

Steven Bethard এই সীমা কাটাতে argparse লেখেন — প্রথমে PyPI-তে আলাদা প্যাকেজ হিসেবে, যেখানে কয়েক বছর ধরে ব্যবহারকারীরা এটি পরীক্ষা করেন। ২০০৯ সালে তিনি PEP 389-এ প্রস্তাব দেন একে standard library-তে আনার। মূল যুক্তি: positional আর্গুমেন্টকে option-এর মতোই সমান মর্যাদা (type, help, সংখ্যা nargs), subcommand, আরও সুন্দর usage বার্তা, আর optparse-এর API এতটাই বদলাতে হতো যে নতুন module-ই ভালো। প্রস্তাবটি গৃহীত হয়, আর argparse আসে Python 2.7 ও 3.2-এ। optparse-এ আর নতুন বৈশিষ্ট্য যোগ না করার সিদ্ধান্ত হয়; পুরোনো কোড ভাঙার ভয়ে সেটি মুছে ফেলা হয়নি, আজও আছে।

এই ইতিহাস Python-এর একটি স্বভাব দেখায়: standard library-তে কিছু আনার আগে তা প্রায়ই PyPI-তে বাস্তব ব্যবহারে পরীক্ষিত হয় (pathlib, dataclasses-এর পূর্বসূরি attrs-এর ধারণাও তেমন)। আর একবার এলে সেটি সরানো খুব কঠিন — তাই একই কাজের একাধিক পুরোনো module পাশাপাশি থেকে যায়। argparse নিজেও এখন পুরোনো; click আর typer তার কিছু অসুবিধা (বড় CLI-তে লেখার পরিমাণ, টেস্ট) কমাতে এসেছে।

মূল উপলব্ধি: ভালো API ধাপে ধাপে বিবর্তিত হয় — প্রতিটি প্রজন্ম আগেরটির পুনরাবৃত্ত বিরক্তিকে লাইব্রেরির কাজে পরিণত করে। আপনি যে কোড প্রতিটি স্ক্রিপ্টে বারবার লেখেন, সেটাই পরের লাইব্রেরির জন্য প্রস্তাব।

প্র ০৩“Automate the Boring Stuff with Python” বইটি কেন এত প্রভাবশালী হলো? প্রোগ্রামার নন এমন মানুষের জন্য অটোমেশন শেখার মূল্য কী, আর তার ঝুঁকি কোথায়?

২০১৫ সালে No Starch Press থেকে প্রকাশিত হয় Al Sweigart-এর Automate the Boring Stuff with Python. বইটির লক্ষ্য পাঠক ছিলেন সফটওয়্যার ইঞ্জিনিয়ার নন — অফিসকর্মী, শিক্ষার্থী, গবেষক, যাঁরা প্রতিদিন স্প্রেডশিট, PDF, ইমেইল আর ফাইল নিয়ে একঘেয়ে কাজ করেন। প্রথম অর্ধেকে Python-এর মৌলিক বিষয়, আর দ্বিতীয় অর্ধেকে প্রতিটি অধ্যায় একটি বাস্তব কাজ: ফাইলের নাম বদল, Excel পড়া, PDF জোড়া, ওয়েব পাতা থেকে তথ্য, সময়মতো কাজ চালানো। Sweigart বইটি Creative Commons লাইসেন্সে অনলাইনে বিনামূল্যে পড়ার ব্যবস্থা রাখেন, পরে নতুন সংস্করণও বের হয়। বিনামূল্যে পাওয়া যাওয়ায় বইটি বিশ্বজুড়ে, বাংলাদেশসহ, Python শেখার জনপ্রিয় প্রথম ধাপ হয়ে ওঠে।

বইটির প্রভাবের কারণ এর দৃষ্টিভঙ্গি। প্রচলিত প্রোগ্রামিং বই শুরু করত অ্যালগরিদম বা “কম্পিউটার বিজ্ঞান” দিয়ে; Sweigart শুরু করলেন পাঠকের নিজের ব্যথা দিয়ে — “আপনি কি ঘণ্টার পর ঘণ্টা কপি-পেস্ট করেন?” প্রোগ্রামিংকে তিনি পেশা নয়, একটি সাধারণ দক্ষতা হিসেবে দেখালেন, যেমন স্প্রেডশিট চালানো। এটি একটি বড় সাংস্কৃতিক পরিবর্তনের অংশ: “citizen developer” — যিনি মূল পেশায় হিসাবরক্ষক, ব্যাংকার বা গবেষক, কিন্তু নিজের কাজ সহজ করতে ছোট স্ক্রিপ্ট লেখেন। বাংলাদেশের একজন ব্যাংক কর্মকর্তা যিনি প্রতি মাসে শাখার রিপোর্ট জোড়েন, বা একজন গবেষক যিনি শত শত ডেটা ফাইল পরিষ্কার করেন — তাঁদের জন্য এক সপ্তাহের Python শেখা বছরে বহু দিন বাঁচাতে পারে।

কিন্তু ঝুঁকিও আছে, আর এই পর্বের নিরাপত্তার অংশ সেজন্যই। পেশাদার সফটওয়্যারে code review, টেস্ট, ব্যাকআপ থাকে; অফিসের ডেস্কটপে লেখা একটি স্ক্রিপ্টে প্রায়ই কিছুই থাকে না। যিনি লিখেছেন তিনি চাকরি ছাড়লে স্ক্রিপ্টটি কেউ বোঝে না, অথচ পুরো বিভাগের মাসিক রিপোর্ট তার ওপর নির্ভর করে। স্প্রেডশিটের জগতে এমন ভুলের নামকরা উদাহরণ আছে, যেখানে একটি সূত্রের ভুল বড় সিদ্ধান্তকে প্রভাবিত করেছে — স্ক্রিপ্টও একই ঝুঁকি বহন করে। আর এখন AI যেকোনো অফিসকর্মীকে এক মিনিটে স্ক্রিপ্ট দিচ্ছে, যিনি হয়তো কোড পড়তেই পারেন না — dry-run, ব্যাকআপ আর যাচাইয়ের অভ্যাস তাই আগের চেয়ে বেশি জরুরি।

মূল উপলব্ধি: অটোমেশন প্রোগ্রামিংকে সবার দক্ষতা বানিয়েছে, আর এটাই তার শক্তি। কিন্তু যে স্ক্রিপ্ট আসল ডেটা ছোঁয়, তার দায়িত্ববোধ পেশাদার সফটওয়্যারের মতোই হতে হবে — ছোট স্ক্রিপ্ট মানে ছোট ঝুঁকি নয়।

প্র ০৪cron কোথা থেকে এল, আর প্রায় পঞ্চাশ বছর পরেও তার পাঁচ ঘরের সময়সূচির ফরম্যাট কেন টিকে আছে? systemd timer বা ক্লাউড scheduler কী নতুন দেয়?

cron নামটি এসেছে গ্রিক শব্দ “chronos” (সময়) থেকে। cron ব্যাপকভাবে পরিচিত হয় ১৯৭৯ সালের Version 7 Unix-এর মাধ্যমে, যেখানে এটি ছিল Ken Thompson-এর লেখা একটি ছোট প্রোগ্রাম। প্রথম নকশাটি ছিল খুব সরল: প্রতি মিনিটে জেগে উঠে একটি টেবিল ফাইল পড়ো, আর যে লাইনের সময় মেলে, তার কমান্ড চালাও। পরে বহু ব্যবহারকারীর সিস্টেমে প্রত্যেকের নিজের টেবিল (crontab) আর আরও দক্ষ নকশা আসে; ১৯৮৭ সালে Paul Vixie-র লেখা “Vixie cron” বহু Linux ও BSD সিস্টেমে মানক হয়ে ওঠে, আর আজকের অনেক cron তারই বংশধর।

পাঁচ ঘরের ফরম্যাট — মিনিট, ঘণ্টা, মাসের দিন, মাস, সপ্তাহের দিন — টিকে আছে কারণ এটি ছোট, পড়া যায় এমন, আর মানুষের সময়-ভাবনার সাথে মেলে। 0 22 * * * মানে “প্রতিদিন রাত ১০টা”, */15 * * * * মানে “প্রতি ১৫ মিনিটে”। এই ভাষা এত সফল যে আধুনিক ক্লাউড ও CI সিস্টেম — GitHub Actions-এর scheduled workflow, Kubernetes-এর CronJob, বহু ক্লাউড প্রদানকারীর scheduler — সময়সূচি লেখার জন্য প্রায় হুবহু cron-এর ফরম্যাটই ব্যবহার করে।

কিন্তু ক্লাসিক cron-এর কিছু দুর্বলতা আছে, যা এই পর্বে দেখেছেন। এটি ধরে নেয় কম্পিউটার সবসময় চালু — ল্যাপটপ বন্ধ থাকলে ওই সময়ের কাজ বাদ পড়ে (এজন্য anacron এসেছিল)। এর পরিবেশ খুব ছোট, তাই “আমার terminal-এ চলে, cron-এ চলে না” — এই অভিযোগ চিরকালের। আউটপুট ঐতিহ্যগতভাবে ইমেইলে পাঠানো হতো, যা আধুনিক সিস্টেমে প্রায়ই কোথাও পৌঁছায় না। একই কাজ আগের বার শেষ না হতেই আবার শুরু হতে পারে। Linux-এর systemd timer এসবের উত্তর দেয়: বাদ পড়া কাজ চালু হওয়ার পর চালানো (Persistent=true), journal-এ সব log, অন্য সার্ভিসের ওপর নির্ভরতা, সম্পদের সীমা। Windows-এর Task Scheduler-এও “বাদ পড়লে যত দ্রুত সম্ভব চালাও” ধরনের বিকল্প আছে।

যে প্রযুক্তিই হোক, আপনার স্ক্রিপ্টের দায়িত্ব একই: মানুষ ছাড়া চলতে পারা, নিজের path নিজে ঠিক করা, log রাখা, সঠিক exit code, আর আগের রান মাঝপথে থেমে গেলে নিরাপদে আবার শুরু হতে পারা।

মূল উপলব্ধি: একটি সরল, ভালো নকশার ছোট ভাষা (cron-এর পাঁচ ঘর) কয়েক প্রজন্মের প্রযুক্তি পেরিয়ে টিকে থাকে। scheduler বদলায়, কিন্তু “মানুষ ছাড়া নির্ভরযোগ্যভাবে চলো” — এই চাহিদা স্ক্রিপ্টের কাছে একই থাকে।

প্র ০৫Idempotency শব্দটি গণিত থেকে DevOps-এ এল কীভাবে? Ansible বা Terraform-এর মতো টুল এটিকে কেন কেন্দ্রীয় নীতি বানিয়েছে, আর আপনার স্ক্রিপ্টে এর অর্থ কী?

“Idempotent” শব্দটি গণিতের — উনিশ শতকে আমেরিকান গণিতবিদ Benjamin Peirce বীজগণিতে এটি ব্যবহার করেন: এমন উপাদান বা অপারেশন, যা নিজের ওপর বারবার প্রয়োগ করলেও ফল বদলায় না। যেমন পরম মান: abs(abs(x)) == abs(x); বা সংখ্যা ০ দিয়ে গুণ। প্রোগ্রামিংয়ে অর্থ দাঁড়াল: একবার করা আর কয়েকবার করার শেষ ফল একই। HTTP-র মানেও এই ধারণা স্পষ্ট লেখা আছে — GET, PUT, DELETE idempotent হওয়ার কথা, POST নয়। এজন্য নেটওয়ার্ক বিচ্ছিন্ন হলে ব্রাউজার GET নিজে থেকে আবার পাঠাতে পারে, কিন্তু একটি পেমেন্টের POST আবার পাঠানোর আগে সতর্ক করে — নইলে bKash-এর টাকা দুবার কাটতে পারে।

সার্ভার চালানোর জগতে ধারণাটি বিপ্লব আনে। ১৯৯৩ সালে Mark Burgess-এর CFEngine “convergence”-এর কথা বলে: স্ক্রিপ্ট “এই কাজগুলো করো” বলবে না, বলবে “সিস্টেমের কাঙ্ক্ষিত অবস্থা এই” — টুল দেখবে বর্তমান অবস্থা কী, আর শুধু পার্থক্যটুকু ঠিক করবে। পরে Puppet, Chef, Ansible আর অবকাঠামোর জন্য Terraform একই নীতিতে দাঁড়ায়। Ansible-এ “nginx ইনস্টল থাকুক” লিখলে প্রথম রানে ইনস্টল হয়, দ্বিতীয় রানে টুল দেখে আগেই আছে আর কিছুই করে না, রিপোর্টে লেখে “changed=0”. একশটি সার্ভারে একই playbook চালানো যায় — কোনোটি নতুন, কোনোটি অর্ধেক কনফিগার করা, কোনোটি সম্পূর্ণ — আর সবাই একই অবস্থায় পৌঁছায়।

কেন এটি এত গুরুত্বপূর্ণ? কারণ বাস্তবে ব্যর্থতা স্বাভাবিক: নেটওয়ার্ক কাটে, প্রক্রিয়া মাঝপথে মরে, বিদ্যুৎ যায়। idempotent কাজে সমাধান সরল — আবার চালান। idempotent নয় এমন কাজে প্রতিটি ব্যর্থতার পর মানুষকে খুঁজে দেখতে হয় কতটুকু হয়েছে, কতটুকু বাকি। বিতরণকৃত সিস্টেমে retry-এর নিরাপত্তার জন্য পেমেন্ট API-গুলো “idempotency key” নেয়: একই key-র অনুরোধ দুবার এলে দ্বিতীয়টি নতুন পেমেন্ট নয়, প্রথমটির ফল ফেরত দেয়।

আপনার Python স্ক্রিপ্টে এর অনুবাদ এই পর্বেই দেখেছেন: নতুন নাম আসুক তথ্য থেকে, গণনা থেকে নয়; mkdir(exist_ok=True); রিপোর্ট প্রতিবার পুরোটা নতুন করে বানানো; যা হয়ে গেছে তা চিনে বাদ দেওয়া; “যোগ করো”-র বদলে “নিশ্চিত করো এমন আছে”।

মূল উপলব্ধি: ব্যর্থতা এড়ানো যায় না, কিন্তু ব্যর্থতার পরের সমাধান সহজ করা যায়। idempotent কাজের ব্যর্থতার ওষুধ একটিই — আবার চালান। ভালো অটোমেশন “কী করতে হবে” নয়, “কী অবস্থা চাই” দিয়ে ভাবে।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — আগে অনুমান, তারপর চালিয়ে মিলিয়ে নেওয়াই আসল শেখা।

  1. Namespace-এর অনুমান: rename_args.py-এর parser-এ নিচের প্রতিটি list দিলে কী হবে, অনুমান করুন: (ক) ["--dry-run", "photos"]; (খ) ["photos", "--case", "UPPER"]; (গ) ["photos", "-v", "--start", "10"]; (ঘ) []. কোনগুলো ঠিকঠাক Namespace দেবে, আর কোনগুলোতে argparse থামবে? তারপর কোডে বদলে মিলিয়ে নিন (থামার ক্ষেত্রে বার্তা stderr-এ যাবে)।

    (ক) ঠিক — optional আর positional যেকোনো ক্রমে আসতে পারে: folder='photos', dry_run=True, বাকি default. (খ) থামবে — choices হলো lower/upper, আর মিল case-sensitive, তাই UPPER অবৈধ: error: argument --case: invalid choice: 'UPPER', exit code 2. (গ) ঠিক — verbose=True, start=10 (int). (ঘ) থামবে — বাধ্যতামূলক positional folder নেই: error: the following arguments are required: folder. শিক্ষা: যাচাইয়ের কাজ argparse আপনার কোডে পৌঁছানোর আগেই করে, তাই main()-এ আপনি ধরে নিতে পারেন আর্গুমেন্টগুলো বৈধ টাইপের।

  2. বিপর্যয় খুঁজুন: একজন লিখেছেন: for i, f in enumerate(folder.glob("*.jpg")): f.rename(folder / f"biye_{i}.jpg"). কোনো dry-run নেই। অন্তত চারটি সমস্যা খুঁজে বের করুন — বিশেষ করে স্ক্রিপ্টটি দুবার চালালে কী হবে?

    (১) glob-এর ক্রম নিশ্চিত নয় — কোন ছবি কোন নম্বর পাবে, তা কম্পিউটারভেদে বদলাতে পারে; sorted লাগবে। (২) নম্বর আসছে লুপের গণনা থেকে, তাই দ্বিতীয় রানে *.jpg আবার নতুন নামের ফাইলগুলোকেও ধরবে এবং নতুন নম্বর দেবে — biye_3.jpg হয়তো biye_0.jpg হতে চাইবে, যেখানে আরেকটি ফাইল আছে; POSIX-এ rename সেটি নিঃশব্দে ওভাররাইট করে, Windows-এ FileExistsError দিয়ে মাঝপথে থামে। দুই ক্ষেত্রেই বিশৃঙ্খলা — idempotent নয়। (৩) লক্ষ্যে নাম আছে কি না যাচাই নেই, প্রথম রানেও আগে থেকে থাকা biye_0.jpg হারাতে পারে। (৪) try নেই — একটি ফাইলের error-এ বাকি সব থামে। এছাড়া {i}-এ শূন্য-প্যাডিং নেই (biye_10 সাজানোর ক্রমে biye_2-এর আগে আসে), .JPG ধরা পড়ে না, আর কোনো log নেই। সমাধান: অংশ ৫-এর rename_photos().

  3. ছোট টুল (কোড লিখুন): find_big_old.py-কে একটি আসল CLI বানান: ফোল্ডার positional, --min-mb (int, default 100), --days (int, default 180), আর --json flag দিলে আউটপুট JSON-এ। কোনো ফাইল না মিললে exit code কী হওয়া উচিত — ভাবুন।

    একটি সমাধান:

    import argparse, json, sys, time
    from pathlib import Path
    
    def main(argv: list[str] | None = None) -> int:
        p = argparse.ArgumentParser(prog="bigold", description="বড় বা পুরোনো ফাইলের তালিকা (কিছু মোছে না)")
        p.add_argument("folder", type=Path, help="কোন ফোল্ডারে খুঁজবে")
        p.add_argument("--min-mb", type=int, default=100, help="এর চেয়ে বড় (MB)")
        p.add_argument("--days", type=int, default=180, help="এত দিনের বেশি না-ছোঁয়া")
        p.add_argument("--json", action="store_true", help="JSON আউটপুট")
        args = p.parse_args(argv)
        if not args.folder.is_dir():
            print(f"ফোল্ডার নেই: {args.folder}", file=sys.stderr)
            return 2
        cutoff = time.time() - args.days * 86400
        found = []
        for f in args.folder.rglob("*"):
            try:
                st = f.stat()
            except OSError:
                continue
            if f.is_file() and (st.st_size > args.min_mb * 1024**2 or st.st_mtime < cutoff):
                found.append({"path": str(f), "mb": round(st.st_size / 1024**2, 1)})
        found.sort(key=lambda d: (-d["mb"], d["path"]))
        if args.json:
            print(json.dumps(found, ensure_ascii=False, indent=2))
        else:
            for d in found:
                print(f"{d['mb']:9.1f} MB  {d['path']}")
        return 0
    
    if __name__ == "__main__":
        sys.exit(main())

    লক্ষ করুন: type=Path দিলে argparse সরাসরি Path object দেয়। --min-mb থেকে attribute হয় min_mb. কোনো ফাইল না মেলা সমস্যা নয় — খোঁজ সফলভাবে শেষ হয়েছে, ফল শুধু খালি; তাই exit code 0, ঠিক যেমন খালি ফলাফলে অনেক টুল 0 দেয়। (ব্যতিক্রম: grep কিছু না পেলে 1 দেয় — প্রথা টুলভেদে আলাদা, তাই help-এ লিখে দিন।) ফোল্ডার না থাকা হলো ব্যবহারের ভুল, তাই 2. --json থাকায় অন্য প্রোগ্রাম আউটপুট নির্ভরযোগ্যভাবে পড়তে পারবে — Unix দর্শনের আধুনিক রূপ।

  4. AI অডিট: যেকোনো AI-কে বলুন: “Python স্ক্রিপ্ট লেখো যা আমার Downloads ফোল্ডারের ৩০ দিনের পুরোনো সব ফাইল মুছে দেয়।” উত্তরটি এই পর্বের চেকলিস্ট দিয়ে যাচাই করুন। কোন কোন জিনিস প্রায় নিশ্চিতভাবে অনুপস্থিত থাকবে, আর AI-কে কী ফলো-আপ নির্দেশ দেবেন?

    সাধারণত যা দেখবেন: os.path.expanduser("~/Downloads") কোডে লেখা; os.remove() সরাসরি, কোনো dry-run বা নিশ্চিতকরণ নেই; পুরো লুপ একটি try-তে বা কোনো try-ই নেই; কী মোছা হলো তার log নেই; exit code নিয়ে কোনো ভাবনা নেই; আর উপফোল্ডার মুছতে গিয়ে কখনো কখনো shutil.rmtree — যা পুরো ফোল্ডার, ফেরত-আনা-যায়-না এমনভাবে, মুছে দেয়। ফলো-আপ নির্দেশ: “ফোল্ডার আর দিনের সংখ্যা argparse আর্গুমেন্ট করো; ডিফল্টে শুধু তালিকা দেখাও (মোট আকারসহ), মুছবে শুধু --apply দিলে আর তালিকা দেখিয়ে y/N জিজ্ঞেস করে; মোছার বদলে trash ফোল্ডারে সরানোর বিকল্প দাও; প্রতিটি ফাইলের error আলাদা ধরো আর log ফাইলে প্রতিটি মোছা লেখো; উপফোল্ডার কখনো মুছবে না; ব্যর্থ হলে exit code 1.” তারপর নকল ফোল্ডারে পরীক্ষা — পুরোনো তারিখের ফাইল বানাতে os.utime() দিয়ে সময় বদলানো যায়। আসল Downloads-এ চালানোর আগে অবশ্যই dry-run-এর তালিকা পড়ুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি ফোল্ডারের শত শত ফাইলের নাম নিরাপদে বদলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🎯 glob দিয়ে লক্ষ্য ফাইল বাছাই করে sorted তালিকা বানানো
  2. 👀 --dry-run দিয়ে পুরোনো → নতুন নামের পরিকল্পনা দেখা
  3. ✅ তালিকা মিলিয়ে সংঘর্ষ আর ভুল নাম নেই, নিশ্চিত হওয়া
  4. ▶️ আসল রান — প্রতিটি ফাইল আলাদা try/except-এ
  5. 📝 log আর সারাংশ দেখে সঠিক exit code দিয়ে শেষ করা

Python-এর argparse-এ parser.add_argument("--dry-run", action="store_true") লিখে কমান্ড লাইনে --dry-run না দিলে args.dry_run-এর মান কী হয়?

  • None
  • AttributeError ওঠে
  • False
  • True

terminal-এ python tool.py --count 5 চালালে sys.argv[2]-এর মান ও টাইপ কী?

  • 5, টাইপ int
  • '5', টাইপ str
  • '--count', টাইপ str
  • IndexError ওঠে

একটি ব্যাকআপ স্ক্রিপ্ট cron থেকে চলে। কিছু ফাইল কপি ব্যর্থ হলে স্ক্রিপ্টের সবচেয়ে উপযুক্ত আচরণ কোনটি?

  • প্রথম ব্যর্থ ফাইলেই পুরো স্ক্রিপ্ট থামানো
  • except Exception: pass দিয়ে চুপচাপ এগিয়ে যাওয়া, exit code 0
  • শুধু stdout-এ “কিছু সমস্যা হয়েছে” লিখে exit code 0 দেওয়া
  • প্রতিটি ফাইলের error আলাদা ধরে বাকিগুলো কপি করা, log রাখা, আর শেষে exit code 1 দেওয়া

ব্যবহারকারীর দেওয়া একটি ফাইলের নাম name দিয়ে Python থেকে একটি বাইরের প্রোগ্রাম চালাতে কোনটি সবচেয়ে নিরাপদ?

  • subprocess.run(["convert", name, "out.png"], check=True)
  • subprocess.run(f"convert {name} out.png", shell=True)
  • os.system("convert " + name + " out.png")
  • subprocess.run(f'convert "{name}" out.png', shell=True)

কোন বৈশিষ্ট্যটি একটি অটোমেশন স্ক্রিপ্টকে idempotent বানায়?

  • স্ক্রিপ্টটি এক সেকেন্ডের কম সময়ে শেষ হয়
  • একবার চালানো আর কয়েকবার চালানোর পর ফাইলগুলোর শেষ অবস্থা একই থাকে
  • স্ক্রিপ্টটি প্রতিবার আউটপুট ফাইলের শেষে নতুন সারি যোগ করে
  • স্ক্রিপ্টটি সবসময় exit code 0 দেয়

Linux-এর crontab-এ 0 22 * * * সময়সূচির অর্থ কী?

  • প্রতি ২২ মিনিট পরপর
  • প্রতি মাসের ২২ তারিখ মধ্যরাতে
  • প্রতিদিন রাত ১০টায়
  • প্রতি রবিবার রাত ১০টায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. অটোমেট করুন সেই কাজ, যা বারবার আসে, সময় খায়, আর ভুলের ঝুঁকি রাখে। একবারের কাজ হাতে করাই ভালো।
  2. CLI টুলের তিন নিয়ম: ফল stdout-এ, বার্তা stderr-এ, সফল-ব্যর্থ exit code-এ। কাঠামো: main(argv) -> int আর sys.exit(main()).
  3. argparse: positional, --flag (store_true), type, choices, default, help — -h নিজে থেকে; ভুলে exit code 2; subcommand-এর জন্য add_subparsers.
  4. ফাইলের কাজে pathlib + shutil; তালিকা সবসময় sorted; বাইরের প্রোগ্রাম subprocess.run([...], check=True) — বাইরের ইনপুটে কখনো shell=True নয়।
  5. নিরাপত্তা: আগে dry-run, নিশ্চিত না হয়ে মোছা নয়, log রাখুন, প্রতিটি ফাইলের error আলাদা ধরুন, আর দুবার চালালেও যেন ক্ষতি না হয় (idempotent). নিয়মিত কাজ দিন cron বা Task Scheduler-কে।

পরের পর্বে: এই পর্বে বারবার একটি প্রশ্ন উঠেছে — বাইরে থেকে আসা ইনপুট কতটা বিশ্বাস করা যায়? shell=True-এর injection, ফাইলের নামের ফাঁদ, কোডে লেখা পাসওয়ার্ড, আর ভুয়া প্যাকেজ — পর্ব ৫০-এ আসছে নিরাপদ Python: input যাচাই, secret সামলানো আর dependency-র নিরাপত্তা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান