پیاده‌سازی یک رابط خط فرمان بی نقص با پایتون — راهنمای کاربردی

۵۷۴ بازدید
آخرین به‌روزرسانی: ۰۵ مهر ۱۴۰۲
زمان مطالعه: ۸ دقیقه
پیاده‌سازی یک رابط خط فرمان بی نقص با پایتون — راهنمای کاربردی

در این مقاله روش نوشتن «اینترفیس» یا رابط خط فرمان در پایتون جهت ایجاد بهره‌وری و ساده‌سازی امور در تیم‌های کاری آموزش داده می‌شود. توسعه دهنگان پایتون همواره از اینترفیس‌های خط فرمان استفاده می‌کنند و بدین جهت چنین رابط‌هایی را می‌نویسند. برای نمونه در پروژه‌های «علم داده» (Data Sciense) می‌توان چندین اسکریپت از خط فرمان برای آموزش دادن مدل‌ها و محاسبه دقت الگوریتم‌ها اجرا کرد. به همین دلیل است که یکی از بهترین روش‌ها برای بهبود بهره‌وری این است که اسکریپت‌ها به خصوص زمانی که چند توسعه‌دهنده روی پروژه واحدی کار می‌کنند؛ تا حد امکان ساده و سرراست باشند.

به منظور دستیابی به چنین وضعیتی 4 مورد کلی پیشنهاد می‌شوند:

  1. در تمام موارد ممکن باید مقادیر پیش‌فرض برای آرگومان‌ها تعیین شوند.
  2. همه موارد خطا (برای نمونه نبود آرگومان، نوع نادرست یا فایل ناموجود) باید مدیریت شوند.
  3. همه آرگومان‌ها و گزینه‌ها باید مستندسازی شوند.
  4. یک نوار پیشرفت باید برای وظایف غیر آنی ارائه شود.

بررسی یک مثال ساده برای ایجاد رابط خط فرمان

مواردی که در راهنمای کلی فوق پیشنهاد کردیم را در یک مثال ساده بررسی می‌کنیم. این مثال به یک اسکریپت برای رمزنگاری و رمزگشایی از پیام‌ها با استفاده از Caesar cipher است.

تصور کنید قبلاً تابع encrypt را که در ادامه پیاده‌سازی شده است، نوشته‌اید و می‌خواهید یک اسکریپت ساده ایجاد کنید که امکان رمزنگاری و رمزگشایی پیام‌ها را فراهم سازد. ما می‌خواهیم کاربر بین حالت‌های رمزنگاری (پیش‌فرض) و رمزگشایی انتخاب کند و مقدار کلید (به طور پیش‌فرض 1 است) را نیز با استفاده از آرگومان‌های خط فرمان انتخاب نماید.

def encrypt(plaintext, key):
    cyphertext = ''
    for character in plaintext:
        if character.isalpha():
            number = ord(character)
            number += key
            if character.isupper():
                if number > ord('Z'):
                    number -= 26
                elif number < ord('A'):
                    number += 26
            elif character.islower():
                if number > ord('z'):
                    number -= 26
                elif number < ord('a'):
                    number += 26
            character = chr(number)
        cyphertext += character

return cyphertext

نخستین کاری که اسکریپت ما باید انجام دهد، این است که مقادیر آرگومان‌های خط فرمان را بگیرد. بدین منظور از sys.argv استفاده می‌کنیم.

متد مبتدیان

sys.argv یک فهرست است که شامل همه آرگومان‌های وارد شده از سوی کاربر در زمان اجرای اسکریپت است. این فهرست شامل نام خود اسکریپت نیز می‌شود. برای نمونه اگر دستور زیر را وارد کنیم:

> python caesar_script.py --key 23 --decrypt my secret message
pb vhfuhw phvvdjh

فهرست فوق‌الذکر شامل موارد زیر خواهد بود:

['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']

بنابراین باید حلقه‌ای روی این فهرست آرگومان‌ها تعریف کنیم و با در نظر گرفتن 'a '--key (یا 'k-') مقدار کلید را پیدا کنیم و با گشتن به دنبال 'decrypt--' حالت رمزنگاری یا رمزگشایی را تشخیص دهیم. در نهایت اسکریپت ما به صورت زیر خواهد بود:

import sys

from caesar_encryption import encrypt


def caesar():
    key = 1
    is_error = False

    for index, arg in enumerate(sys.argv):
        if arg in ['--key', '-k'] and len(sys.argv) > index + 1:
            key = int(sys.argv[index + 1])
            del sys.argv[index]
            del sys.argv[index]
            break

    for index, arg in enumerate(sys.argv):
        if arg in ['--encrypt', '-e']:
            del sys.argv[index]
            break
        if arg in ['--decrypt', '-d']:
            key = -key
            del sys.argv[index]
            break

    if len(sys.argv) == 1:
        is_error = True
    else:
        for arg in sys.argv:
            if arg.startswith('-'):
                is_error = True

    if is_error:
        print(f'Usage: python {sys.argv[0]} [ --key <key> ] [ --encrypt|decrypt ] <text>')
    else:
        print(encrypt(' '.join(sys.argv[1:]), key))

if __name__ == '__main__':
caesar()

این اسکریپت کمابیش توصیه‌های اشاره شده در ابتدای این نوشته را رعایت می‌کند:

  1. یک کلید پیش‌فرض (default) و یک حالت پیش‌فرض (default) وجود دارند.
  2. موارد خطاهای ابتدایی مدیریت می‌شوند (هیچ متن ورودی یا آرگومان ناشناس ارائه نشده است)
  3. مستندات خلاصه‌ای در موارد بروز این خطاها یا وقتی که اسکریپت بدون آرگومان فراخوانی می‌شود، نمایش می‌یابند:
> python caesar_script_using_sys_argv.py

Usage: python caesar.py [--key <key>] [--encrypt|decrypt] <text>

با این حال این نسخه از اسکریپت سزار کاملاً طولانی و زشت است. در واقع طول این کد 39 خط است که حتی شامل منطق خود رمزنگاری نیز نمی‌شود. روش بهتری نیز برای تجزیه آرگومان‌های خط فرمان وجود دارد.

Argparse

Argparse یک ماژول کتابخانه استاندارد پایتون برای تجزیه آرگومان‌های خط فرمان است. در ادامه روش نمایش اسکریپت سزار با استفاده از argparse را بررسی می‌کنیم:

import argparse

from caesar_encryption import encrypt


def caesar():
    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-e', '--encrypt', action='store_true')
    group.add_argument('-d', '--decrypt', action='store_true')
    parser.add_argument('text', nargs='*')
    parser.add_argument('-k', '--key', type=int, default=1)
    args = parser.parse_args()

    text_string = ' '.join(args.text)
    key = args.key
    if args.decrypt:
        key = -key
    cyphertext = encrypt(text_string, key)
    print(cyphertext)

if __name__ == '__main__':
caesar()

این کد راهنمایی‌های اولیه ما را رعایت می‌کند و مستندات دقیق‌تری ارائه کرده است. همچنین مدیریت خطا به روشی با تعامل‌پذیری بالاتر نسبت به اسکریپت دست‌نویس قبلی صورت می‌پذیرد:

> python caesar_script_using_argparse.py --encode My message

usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]

caesar_script_using_argparse.py: error: unrecognized arguments: --encode

> python caesar_script_using_argparse.py --help

usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]

positional arguments:

text

optional arguments:

-h, --help show this help message and exit

-e, --encrypt

-d, --decrypt

-k KEY, --key KEY

با این وجود، با در نظر گرفتن کد مشخص می‌شود که این کد نیز نواقصی دارد و خطوط ابتدایی تابع ما (از خط 7 تا 13) یعنی جایی که آرگومان‌ها تعریف می‌شوند، چندان مناسب نیستند. این کد تفصیل زیادی دارد و به روشی مبتنی بر برنامه‌نویسی تابعی (programmatic) نوشته شده است در حالی که می‌شد آن را به روشی خلاصه‌تر و با رویکرد اعلانی‌ (declarative) نوشت.

با یک «کلیک» (Click) کد را بهبود دهید

خوشبختانه یک کتابخانه پایتون وجود دارد که همین ویژگی‌های argparse را ارائه می‌کند و سبک کدنویسی زیباتری دارد. نام این کتابخانه Click است. در ادامه نسخه سوم اسکریپت سزار را که با استفاده از کلیک نوشته‌ایم، مشاهده می‌کنید:

import click

from caesar_encryption import encrypt

@click.command()
@click.argument('text', nargs=-1)
@click.option('--decrypt/--encrypt', '-d/-e')
@click.option('--key', '-k', default=1)
def caesar(text, decrypt, key):
    text_string = ' '.join(text)
    if decrypt:
        key = -key
    cyphertext = encrypt(text_string, key)
    click.echo(cyphertext)

if __name__ == '__main__':
caesar()

دقت کنید که آرگومان‌ها و گزینه‌ها اینک به روش دکوراتور (decorators) اعلان می‌شوند که باعث می‌شود به صورت مستقیم به عنوان پارامترهای تابع در اختیار ما باشند. برخی موارد کد فوق را در فهرست زیر توضیح داده‌ایم.

  • پارامتر nargs برای یک آرگومان اسکریپت، تعداد کلمه‌های متوالی مورد انتظار این آرگومان را تعریف می‌کند. در این روش «یک رشته درون گیومه مانند همین نوشته» به عنوان 1 کلمه شمارش می‌شود. مقدار پیش‌فرض 1 است. بدین ترتیب nargs=-1 امکان ارائه هر تعداد از کلمه‌ها را فراهم می‌سازد.
  • نمادگذاری encrypt/--decrypt-- امکان تعریف چند گزینه متقابلاً انحصاری را فراهم می‌سازد. این فرایند شبیه تابع add_mutually_exclusive_group در کتابخانه argparse است که موجب تعریف پارامترهای بولی می‌شود.
  • click.echo یک ابزار کوچک است که از سوی کتابخانه ارائه شده و همان کار print را ارائه می‌کند؛ اما با پایتون نسخه 2 و 3 سازگار است و برخی خصوصیات اضافی مانند مدیریت رنگ و غیره را دارد.

افزودن برخی کارکردهای دیگر

آرگومان‌های اسکریپت ما پیام‌های کاملاً محرمانه فرض می‌شوند که باید رمزنگاری شوند. در این شرایط آیا این که از کاربر بخواهیم پیام‌ها را به صورت متن ساده در ترمینال وارد کرده و آن‌ها را در تاریخچه دستورهای ترمینال باقی بگذارد کمی عجیب نیست؟ یک راه‌حل برای این وضعیت آن است که از روش امن‌تری به صورت یک «اعلان پنهان» (hidden prompt) استفاده کنیم. همچنین می‌توانیم متن را از یک فایل ورودی بخوانیم که در مورد متن‌های طولانی‌تر کارایی بیشتری نیز دارد. یا این که می‌توانیم انتخاب بین این حالت‌ها را بر عهده کاربر بگذاریم.

همین کار را در مورد خروجی نیز انجام می‌دهیم، یعنی کاربر می‌تواند خروجی را در یک فایل ذخیره کند یا این که آن را در ترمینال نمایش دهد. این گزینه‌ها منجر به تولید نسخه بهبودیافته‌ای از اسکریپت سزار ما می‌شود:

import click

from caesar_encryption import encrypt

@click.command()
@click.option(
    '--input_file',
    type=click.File('r'),
    help='File in which there is the text you want to encrypt/decrypt.'
         'If not provided, a prompt will allow you to type the input text.',
)
@click.option(
    '--output_file',
    type=click.File('w'),
    help='File in which the encrypted / decrypted text will be written.'
         'If not provided, the output text will just be printed.',
)
@click.option(
    '--decrypt/--encrypt',
    '-d/-e',
    help='Whether you want to encrypt the input text or decrypt it.'
)
@click.option(
    '--key',
    '-k',
    default=1,
    help='The numeric key to use for the caesar encryption / decryption.'
)
def caesar(input_file, output_file, decrypt, key):
    if input_file:
        text = input_file.read()
    else:
        text = click.prompt('Enter a text', hide_input=not decrypt)
    if decrypt:
        key = -key
    cyphertext = encrypt(text, key)
    if output_file:
        output_file.write(cyphertext)
    else:
        click.echo(cyphertext)

if __name__ == '__main__':
caesar()

مواردی که در نسخه جدید وجود دارند، ابتدا که یک پارامتر help به هر آرگومان یا گزینه اضافه کرده‌ایم. از آنجا که اسکریپت کمی پیچیده‌تر شده است، در این وضعیت می‌توانیم جزییات بیشتری در مورد رفتار به مستندات اسکریپت اضافه کنیم و از این رو به صورت زیر درمی‌آید:

> python caesar_script_v2.py --help

Usage: caesar_script_v2.py [OPTIONS]

Options:

--input_file FILENAME File in which there is the text you want to encrypt/decrypt. If not provided, a prompt will allow you to type the input text.

--output_file FILENAME File in which the encrypted/decrypted text will be written. If not provided, the output text will just be printed.

-d, --decrypt / -e, --encrypt Whether you want to encrypt the input text or decrypt it.

-k, --key INTEGER The numeric key to use for the caesar encryption / decryption.

--help Show this message and exit.

ما دو پارامتر جدید به صورت input_file, و output_file داریم که از نوع click.File هستند. کتابخانه مربوطه، باز کردن صحیح فایل‌ها را پیش از ورود به تابع تضمین کرده و خطاهایی که ممکن است رخ بدهند را مدیریت خواهد کرد. برای نمونه:

> python caesar_script_v2.py --decrypt --input_file wrong_file.txt

Usage: caesar_script_v2.py [OPTIONS]

Error: Invalid value for "--input_file": Could not open file: wrong_file.txt: No such file or directory

همان‌طور که در متن help توضیح داده شد؛ اگر input_file ارائه نشده باشد، ما باید از click.prompt استفاده کنیم تا به کاربر امکان دهیم مستقیماً متن خود را در خط اعلان وارد کند. این متن در حالت رمزنگاری پنهان خواهد بود. بنابراین اسکریپت به صورت زیر در می‌آید:

> python caesar_script_v2.py --encrypt --key 2

Enter a text: **************

yyy.ukectc.eqo

رمزگشایی

شما اینک یک هکر هستید و می‌خواهید یک متن رمز شده با کد سزار را رمزگشایی کنید؛ اما کلید را نمی‌دانید. یک راهبرد ساده می‌تواند این باشد که تابع رمزگشایی را 25 بار با همه کلیدهای ممکن فراخوانی کنید و همه متن‌های حاصل را بخوانید تا ببینید کدام یک معنی‌دار است.

اما از آنجا که ما هوشمند و البته تنبل هستیم، ترجیح می‌دهید که این فرایند را به صورت خودکار اجرا کنیم. یک روش برای انتخاب محتمل‌ترین متن ورودی از میان همه این 25 متن حاصل این است که تعداد کلمه‌های واقعی موجود در این متن‌ها را بشمارید. این کار را با استفاده از ماژول PyEnchant انجام می‌دهیم:

import click
import enchant

from caesar_encryption import encrypt

@click.command()
@click.option(
    '--input_file',
    type=click.File('r'),
    required=True,
)
@click.option(
    '--output_file',
    type=click.File('w'),
    required=True,
)
def caesar_breaker(input_file, output_file):
    cyphertext = input_file.read()
    english_dictionnary = enchant.Dict("en_US")
    max_number_of_english_words = 0
    for key in range(26):
        plaintext = encrypt(cyphertext, -key)
        number_of_english_words = 0
        for word in plaintext.split(' '):
            if word and english_dictionnary.check(word):
                number_of_english_words += 1
        if number_of_english_words > max_number_of_english_words:
            max_number_of_english_words = number_of_english_words
            best_plaintext = plaintext
            best_key = key
    click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...')
    output_file.write(best_plaintext)

if __name__ == '__main__':
caesar_breaker()

progress bar

این کد به خوبی کار می‌کند؛ اما اگر به خاطر داشته باشید، در کد فوق یکی از راهنمایی‌های اینترفیس مناسب برای خط فرمان که در ابتدای نوشته اشاره کردیم رعایت نشده است و آن مورد چهارم است.

«4- یک نوار پیشرفت در مورد اجرای وظایف غیر آنی باید ارائه شود.»

در مورد متن نمونه‌ای شامل 10،000 کلمه که استفاده کردیم، اسکریپت در طی 5 ثانیه متن رمزگشایی شده را ارائه می‌کند. این وضعیت کاملاً نرمال است، چون 25 مقدار مختلف کلید برای یک متن 10،000 کلمه‌ای بررسی شده است.

اما تصور کنید بخواهید یک متن شامل 100،000 کلمه را رمزگشایی کنید. این کار 50 ثانیه طول می‌کشد که می‌تواند برای کاربر وحشتناک باشد. به همین دلیل است که توصیه می‌کنیم نوارهای پیشرفت کار را برای هر نوع وظیفه‌ای که بی‌درنگ نیست ارائه کنید. پیاده‌سازی چنین نوارهای پیشرفتی کاملاً ساده است. در ادامه همان اسکریپت قبلی را می‌بینید که این بار در طی فرایند رمزگشایی یک نوار پیشرفت وضعیت را نیز نمایش می‌دهد:

import click
import enchant

from tqdm import tqdm

from caesar_encryption import encrypt

@click.command()
@click.option(
    '--input_file',
    type=click.File('r'),
    required=True,
)
@click.option(
    '--output_file',
    type=click.File('w'),
    required=True,
)
def caesar_breaker(input_file, output_file):
    cyphertext = input_file.read()
    english_dictionnary = enchant.Dict("en_US")
    best_number_of_english_words = 0
    for key in tqdm(range(26)):
        plaintext = encrypt(cyphertext, -key)
        number_of_english_words = 0
        for word in plaintext.split(' '):
            if word and english_dictionnary.check(word):
                number_of_english_words += 1
        if number_of_english_words > best_number_of_english_words:
            best_number_of_english_words = number_of_english_words
            best_plaintext = plaintext
            best_key = key
    click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...')
    output_file.write(best_plaintext)

if __name__ == '__main__':
caesar_breaker()

آیا تفاوت را ملاحظه کردید؟ تشخیص تفاوت این اسکریپت چندان آسان نیست، چون کلاً شامل 4 حرف به صورت TQDM است.

این نام یک کتابخانه پایتون است و ما از نام کلاس منحصر به فرد آن استفاده کرده‌ایم که با بهره‌گیری از آن می‌توانیم هر شیء تکرارپذیر را برای نمایش پیشرفت فرایند مربوطه تعیین کنیم.

for key in tqdm(range(26)):

نتیجه کار یک نوار پیشرفت زیبا است که شاید باور اجرای آن به این سادگی برای شما نیز چون ما دشوار باشد.

progress bar

کتابخانه Click نیز یک ابزار مشابه برای نمایش نوار پیشرفت به نام click.progress_bar ارائه کرده است؛ اما ظاهر آن کمی ناخوانا است و کد آن نیز به اندازه TQDM فشرده نیست.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۶ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
sicara
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *