نادرًا ما تكون الأدوات المساعدة لسطر الأوامر 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.
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.