اذهب إلى المحتوى

كيفية استخدام الحزمة Flag في لغة جو Go


هدى جبور

نادرًا ما تكون الأدوات المساعدة لسطر الأوامر Command-line utilities مفيدةً دون ضبط configuration إضافي وذلك عندما يتطلب الأمر إجراء عمليات خارج الصندوق. تُعد الإعدادات الافتراضية للأوامر أمرًا جيدًا ومهمًا، لكن ينبغي أن تتميز بإمكانية قبول إعدادات ضبط محددة من المستخدمين أيضًا. في معظم أنظمة التشغيل يمكن تخصيص أوامر سطر الأوامر من خلال استخدام الرايات Flags؛ وهي سلاسل نصية تُضاف إلى أمر ما، بحيث تؤدي إلى سلوك خاص لهذا الأمر حسب قيمتها. تتيح لك لغة جو إنشاء أدوات مساعدة لسطر الأوامر تقبل رايات يمكن من خلالها تخصيص سلوك الأوامر، وذلك باستخدام حزمة flag من المكتبة القياسية.

ستتعلم في هذه المقالة طرقًا مختلفة لاستخدام حزمة flag بهدف إنشاء أنواع مختلفة من الأدوات المساعدة لسطر الأوامر. سنستخدم رايةً للتحكم في خرج برنامج وتقديم وسطاء موضعية Positional arguments (وهي وسطاء يجب وضعها في الموضع أو الترتيب المناسب المُحدد مسبقًا)، إذ يمكننا مزج الرايات والبيانات الأخرى وتنفيذ أوامر فرعية.

استخدام الرايات لتغيير سلوك البرنامج

يتضمن استخدام حزمة الراية ثلاث خطوات: تبدأ بتعريف متغيرات تلتقط قيم الرايات، ثم تحديد الرايات التي سيستخدمها التطبيق، وتنتهي بتحليل الرايات المُقدمة للتطبيق عند التنفيذ. تُركّز معظم الدوال داخل حزمة الراية على تعريف رايات وربطها بالمتغيرات التي تُعرّفها، وتنجز الدالة ()Parse مرحلة التحليل.

لتوضيح الأمور سنُنشئ برنامجًا بسيطًا يتضمن رايةً بوليانية تُغيّر الرسالة المطبوعة؛ فإذا كانت الراية color- موجودة، سيطبع البرنامج الرسالة باللون الأزرق؛ وإذا لم تُقدّم أي راية، فلن يكون للرسالة أي لون. انشئ ملفًا باسم boolean.go:

$ nano boolean.go

اضِف ما يلي إلى الملف:

package main

import (
    "flag"
    "fmt"
)

type Color string

const (
    ColorBlack  Color = "\u001b[30m"
    ColorRed          = "\u001b[31m"
    ColorGreen        = "\u001b[32m"
    ColorYellow       = "\u001b[33m"
    ColorBlue         = "\u001b[34m"
    ColorReset        = "\u001b[0m"
)

func colorize(color Color, message string) {
    fmt.Println(string(color), message, string(ColorReset))
}

func main() {
    useColor := flag.Bool("color", false, "display colorized output")
    flag.Parse()

    if *useColor {
        colorize(ColorBlue, "Hello, DigitalOcean!")
        return
    }
    fmt.Println("Hello, DigitalOcean!")
}

يستخدم هذا المثال سلاسل الهروب آنسي ANSI Escape Sequences لجعل الطرفية تُعطي خرجًا ملونًأ؛ وهي سلاسل خاصة من المحارف، لذا من المنطقي أن نُعرّف نوع خاص بها. في مثالنا نسمي هذا النوع Color ونعرّفه على أنه string، ثم نُعرّف لوحة ألوان لاستخدامها بكتلة نسميها const تتضمن عدة خيارات لونية اعتمادًا على النوع السابق. تستقبل الدالة colorize المُعرّفة بعد الكتلة const قيمةً لونيةً من اللوحة السابقة (أي عمليًّا متغير من النوع Color) إضافةً إلى الرسالة المطلوب تلوينها. بعد ذلك توجّه الطرفية Terminal لتغيير اللون عن طريق طباعة تسلسل الهروب للون المطلوب، ثم طباعة الرسالة. أخيرًا، يُطلب من الطرفية إعادة ضبط اللون الأساسي لها من خلال طباعة ColorReset، أي نطبع سلسلة الهروب للون المطلوب ثم رسالتنا، فتظهر باللون المطلوب، ثم نعيد ضبط اللون إلى حالته الأصلي.

نستخدم داخل الدالة main الدالة flag.Bool لتعريف راية بوليانية اسمها color. المعامل الثاني لهذه الدالة false هو القيمة الافتراضية للراية، أي عندما نُشغل البرنامج بدونها. على عكس ما قد تتوقعه، فإن ضبط هذا المعامل على true هو أمر غير صحيح، لأننا لا نريد أن يُطبق سلوك التلوين إلا عندما نُمرر الراية كما سترى بعد قليل. إذًا، تكون قيمة هذا المعامل تكون غالبًا false مع الرايات البوليانية.

المعامل الأخير هو نص توضيحي لهذه الراية، أي كأنه توثيق استخدام أو وصف. القيمة التي تعيدها الدالة هي مؤشر إلى bool، وتُضبط قيمة هذا المؤشر من خلال الدالة flag.Parse بناءً على الراية التي يُمررها المستخدم. يمكننا بعد ذلك التحقق من قيمة هذا المؤشر البولياني عن طريق تحصيل قيمته باستخدام المعامل *. إذًا باستخدام هذه القيمة المنطقية، يمكننا استدعاء colorize عند تمرير color- أو استدعاء دالة الطباعة العادية fmt.Println دون تلوين إذا لم تُمرر الراية. احفظ وأغلق الملف وشغله بدون تقديم أي راية:

$ go run boolean.go

ستحصل على الخرج التالي:

Hello, DigitalOcean!

أعد تشغيله مع تمرير الراية color-:

$ go run boolean.go -color

سيكون الناتج هو نفس النص، ولكن هذه المرة باللون الأزرق.

يمكنك أيضًا إرسال أسماء ملفات أو بيانات أخرى إلى البرنامج، وليس فقط رايات.

التعامل مع الوسطاء الموضعية

تمتلك الأوامر غالبًا بعض الوسطاء التي تحدد سلوكها. لنأخذ مثلًا الأمر head الذي يطبع الأسطر الأولى من ملف ما. غالبًا ما يُستدعى هذا الأمر بالشكل التالي head example.txt، إذ يُمثل الملف example.txt وسيطًا موضعيًّا هنا.

تستمر الدالة ()Parse في تحليل الرايات التي تصادفها ريثما تجد شيئًا آخر لا يُمثّل راية (سنرى بعد قليل أنها ستتوقف عند مصادفة وسيط موضعي). لذا سنتعرف الآن على دالة أخرى توفرها الحزمة flag تتمثل بالدالة ()Args والدالة ()Arg. لتوضيح ذلك سنعيد تنفيذ الأمر head الذي يعرض أول عدة أسطر من ملف معين.

أنشئ ملفًا جديدًا يسمى head.go وضِف الشيفرة التالية:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    var count int
    flag.IntVar(&count, "n", 5, "number of lines to read from the file")
    flag.Parse()

    var in io.Reader
    if filename := flag.Arg(0); filename != "" {
        f, err := os.Open(filename)
        if err != nil {
            fmt.Println("error opening file: err:", err)
            os.Exit(1)
        }
        defer f.Close()

        in = f
    } else {
        in = os.Stdin
    }

    buf := bufio.NewScanner(in)

    for i := 0; i < count; i++ {
        if !buf.Scan() {
            break
        }
        fmt.Println(buf.Text())
    }

    if err := buf.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "error reading: err:", err)
    }
}

نُعرّف بدايةً المتغير count الذي سيُخزّن عدد الأسطر التي يجب أن يقرأها البرنامج من الملف، كما نُعرّف أيضًا الراية n- باستخدام flag.IntVar لكي نعكس سلوك البرنامج head. تسمح لنا هذه الدالة بتمرير المؤشر الخاص بنا إلى متغير على عكس دوال حزمة الراية flag الأخرى، التي لا تحتوي على اللاحقة Var. بغض النظر عن هذا الفرق، ستكون بقية المعاملات للدالة flag.IntVar مثل نظيراتها في دوال flag.Int الأخرى التي لا تتضمن هذه اللاحقة، أي ستكون المعاملات كما يلي: اسم الراية ثم القيمة الافتراضية ثم الوصف. بعد ذلك، نستدعي دالة ()flag.Parse لتفسير دخل المستخدم.

يقرأ القسم التالي الملف، إذ نعرّف متغيرًا باسم io.Reader الذي سيُضبط إما على الملف الذي يطلبه المستخدم، أو الدخل القياسي الذي يُمرر إلى البرنامج. نستخدم الدالة flag.Arg داخل التعليمة if للوصول إلى أول وسيط موضعي يأتي بعد الرايات. ستكون قيمة filename، إما اسم ملف يُقدمه المستخدم أو سلسلة فارغة ""؛ ففي حال تقديم اسم ملف، تُستخدم الدالة os.Open لفتح الملف وضبط المتغير io.Reader سالف الذكر؛ وإذا لم يُقدم اسم ملف (أي سلسلة فارغة) فإننا نستخدم os.Stdin للقراءة من الدخل القياسي.

يستخدم القسم الأخير bufio.Scanner* الذي أنشئ باستخدام bufio.NewScanner لقراءة الأسطر من متغير io.Reader الذي يُمثله in. بعد ذلك، نكرّر حلقة عدة مرات حسب قيمة count. تُستدعى break إذا كان ناتج قراءة سطر باستخدام buf.Scan هو القيمة false، إذ يشير ذلك إلى أن عدد الأسطر أقل من الرقم الذي يطلبه المستخدم.

شغّل هذا البرنامج واعرض محتويات الملف البرمجي نفسه الذي كتبته للتو من خلال استخدام head.go مثل وسيط، أي سنشغّل البرنامج head.go ونقرأ محتوياته أيضًا:

$ go run head.go -- head.go

الفاصل -- هو راية، إذ يُفسَّر وجودها من قِبل حزمة الراية flag على أنه لن يكون هناك رايات بدءًا منها. سيكون الخرج كما يلي:

package main

import (
        "bufio"
        "flag"

دعنا نستخدم الراية n- التي عرّفناها لتحديد عدد الأسطر المقروءة:

$ go run head.go -n 1 head.go

سيكون الخرج هو أول سطر فقط من الملف:

package main

أخيرًا، عندما يكتشف البرنامج أنه لم تُقدّم أية وسطاء موضعية، سيقرأ من الدخل القياسي، تمامًا مثل الأمر head. جرّب تشغيل هذا الأمر:

$ echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3

سيكون الخرج على النحو التالي:

fish
lobsters
sharks

يقتصر سلوك دوال حزمة الراية التي رأيناها حتى الآن على فحص الأمر المُستدعى كاملًا. لا نريد دائمًا هذا السلوك، خاصةً إذا كانت الأداة التي نريدها تتضمن أوامر فرعية Sub-commands.

استخدام FlagSet لدعم إمكانية تحقيق الأوامر الفرعية

تتضمن تطبيقات سطر الأوامر الحديثة غالبًا وجود أوامر فرعية، بهدف تجميع مجموعة من الأدوات تحت أمر واحد. الأداة الأكثر شهرة التي تستخدم هذا النمط هي git، فمثلًا في git init، لدينا git هو الأمر الأساسي و init هو الأمر الفرعي التابع له. إحدى السمات البارزة للأوامر الفرعية هي أن كل أمر فرعي يمكن أن يكون له مجموعته الخاصة من الرايات.

يمكن لتطبيقات لغة جو أن تدعم الأوامر الفرعية من خلال نوع خاص يُدعى (flag.(*FlagSet. سننشئ برنامجًا يُنفذ أمرًا باستخدام أمرين فرعيين وبرايات مختلفة، وذلك لكي نجعل الأمور واضحة.

نُنشئ ملفًا جديدًا يسمى subcommand.go بالمحتويات التالية:

package main

import (
    "errors"
    "flag"
    "fmt"
    "os"
)

func NewGreetCommand() *GreetCommand {
    gc := &GreetCommand{
        fs: flag.NewFlagSet("greet", flag.ContinueOnError),
    }

    gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")

    return gc
}

type GreetCommand struct {
    fs *flag.FlagSet

    name string
}

func (g *GreetCommand) Name() string {
    return g.fs.Name()
}

func (g *GreetCommand) Init(args []string) error {
    return g.fs.Parse(args)
}

func (g *GreetCommand) Run() error {
    fmt.Println("Hello", g.name, "!")
    return nil
}

type Runner interface {
    Init([]string) error
    Run() error
    Name() string
}

func root(args []string) error {
    if len(args) < 1 {
        return errors.New("You must pass a sub-command")
    }

    cmds := []Runner{
        NewGreetCommand(),
    }

    subcommand := os.Args[1]

    for _, cmd := range cmds {
        if cmd.Name() == subcommand {
            cmd.Init(os.Args[2:])
            return cmd.Run()
        }
    }

    return fmt.Errorf("Unknown subcommand: %s", subcommand)
}

func main() {
    if err := root(os.Args[1:]); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

ينقسم هذا البرنامج إلى عدة أجزاء: الدالة main، والدالة root، وبعض الدوال لتحقيق الأمر الفرعي.

تعالج الدالة main الأخطاء التي تُعاد من الأوامر، بينما تلتقط تعليمة if الأخطاء التي قد تُعيدها الدوال وتطبع الخطأ ويُنهى البرنامج وتعاد القيمة 1 إشارةً إلى حدوث خطأ إلى نظام التشغيل. نمرر جميع المعطيات التي استُدعي البرنامج معها داخل الدالةmain إلى الدالة root مع حذف الوسيط الأول الذي يُمثل اسم البرنامج (في الأمثلة السابقة subcommand\.) من خلال استقطاع os.Args.

تعرّف الدالة الدالة root الأمر الفرعي Runner[] حيثما تُعرّف جميع الأوامر الفرعية. Runner هي واجهة الأوامر الفرعية التي تسمح لدالة root باسترداد اسم الأمر الفرعي باستخدام ()Name ومقارنته بمحتويات المتغير subcommand. بمجرد تحديد الأمر الفرعي الصحيح بعد التكرار على متغير cmds، نُهيئ الأمر الفرعي مع بقية الوسطاء ونستدعي التابع ()Run الخاص بهذا الأمر.

على الرغم من أننا نُعرّف أمرًا فرعيًّا واحدًا، إلا أن هذا الإطار سيسمح لنا بسهولة بإنشاء أوامر أخرى. نُعرّف نسخةً من النوع GreetCommand باستخدام NewGreetCommand، إذ نُنشئ flag.FlagSet* جديد باستخدام flag.NewFlagSet. تأخذ flag.NewFlagSet وسيطين، هما اسم مجموعة الرايات واستراتيجية للإبلاغ عن أخطاء التحليل. يمكن الوصول إلى اسم flag.FlagSet* باستخدام التابعflag.(*FlagSet).Name. نستخدم هذا في الطريقة ()GreetCommand).Name*) بحيث يتطابق اسم الأمر الفرعي مع الاسم الذي قدمناه إلى flag.FlagSet*.

يُعرّف NewGreetCommand أيضًا الراية name- بطريقة مشابهة للأمثلة السابقة، ولكنه بدلًا من ذلك يستدعيها مثل تابع للحقل flag.FlagSet* في GreetCommand* (نكتب gc.fs).

عندما تستدعي root التابع ()Init الذي يخص GreetCommand*، فإننا نمرر المعطيات المقدمة إلى التابع Parse الخاص بالحقل flag.FlagSet*.

سيكون من الأسهل أن ترى الأوامر الفرعية إذا بنيت هذا البرنامج ثم شغلته. اِبنِ البرنامج كما يلي:

$ go build subcommand.go

الآن، شغّل البرنامج دون وسطاء:

$ ./subcommand

سيكون الخرج على النحو التالي:

You must pass a sub-command

شغّل الآن البرنامج مع استخدام الأمر الفرعي greet:

$ ./subcommand greet

سيكون الخرج على النحو التالي:

Hello World !

استخدم الآن الراية name مع greet لتحديد اسم:

$ ./subcommand greet -name Sammy

سيكون الخرج كما يلي:

Hello Sammy !

يوضح هذا المثال بعض المبادئ الكامنة وراء كيفية هيكلة تطبيقات سطر الأوامر الأكبر حجمًا أو الأكثر تعقيدًا في لغة جو. صُمم FlagSets لمنح المطورين مزيدًا من التحكم وكيفية معالجة الرايات من خلال منطق تحليل الراية.

الخاتمة

تمنح الرايات مستخدمي برنامجك التحكم في كيفية تنفيذ البرامج، أو التحكم بسلوكه، وبالتالي تجعله أكثر مرونةً في التعامل مع سياقات التنفيذ المحتملة. من المهم أن تمنح المستخدمين إعدادات افتراضية مفيدة، ولكن يجب أن تمنحهم الفرصة لتجاوز الإعدادات التي لا تناسب حالتهم. لقد رأينا أن حزمة الراية flag توفر خيارات مرنة لتعزيز إمكانية إضافة خيارات ضبط خاصة للمستخدمين. يمكنك اختيار بعض الرايات البسيطة، أو إنشاء مجموعة من الأوامر الفرعية القابلة للتوسيع. سيساعدك استخدام حزمة الراية في كلتا الحالتين على بناء أدوات مرنة لسطر الأوامر.

ترجمة -وبتصرف- للمقال How To Use the Flag Package in Go لصاحبه Gopher Guides.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...