تطوير أدوات سطر الأوامر
الدرس 25: تطوير أدوات سطر الأوامر
تشبيه من الواقع
تخترق أنك تقف أمام آلة بيع آلية. تضغط على زر لاختيار مشروب (تمرير المعاملات)، وتوزع الآلة بناءً على اختيارك (تنفيذ الدالة المقابلة)، وإذا ضغطت الزر الخاطئ تعرض "اختيار غير صالح" (التحقق من المعاملات). أداة سطر الأوامر مثل تلك الآلة — يدخل المستخدمون الأوامر والمعاملات عبر سطر الأوامر، وينفذ البرنامج العمليات المقابلة ويخرج النتائج. أداة سطر الأوامر الجيدة مثل آلة بيع مصممة بشكل جيد: تخطيط أزرار واضح، ورسائل تشغيل صريحة، وتغذية رجعية ودية للأخطاء.
المفاهيم الأساسية
Go مناسب بشكل طبيعي لبناء أدوات سطر الأوامر — يترجم إلى ثنائي واحد، ويدعم التطوير عبر الأنظمة الأساسية، ويشتغل بسرعة. إليك المفاهيم الأساسية:
| المفهوم | الوصف |
|---|---|
os.Args |
مقطع معاملات سطر الأوامر الخام، [0] هو اسم البرنامج |
حزمة flag |
أداة تحليل المعاملات المقدمة من المكتبة القياسية |
مكتبة cobra |
إطار عمل CLI من طرف ثالث يدعم الأوامر الفرعية والمساعدة التلقائية وإكمال الصدفة |
| الأوامر الفرعية | مثل git commit وdocker run — ينفذ البرنامج منطقًا مختلفًا بناءً على الأمر الفرعي |
| التحقق من المعاملات | التحقق من صحة مدخلات المستخدم وتقديم رسائل خطأ ودية |
| الإدخال التفاعلي | قراءة مدخلات المستخدم وقت التشغيل لبناء تجارب CLI تفاعلية |
os.Args وحزمة flag
os.Args هي الطريقة الأكثر بدائية للحصول على المعاملات، مناسبة للسيناريوهات البسيطة؛ توفر حزمة flag تحليل علامات مصنفة:
هيكل سطر الأوامر:
program [flags] [args]
↑ ↑ ↑
اسم البرنامج العلامات المعاملات الموقعية
مثال:
go build -o myapp ./cmd
↑ ↑ ↑
go -o myapp ./cmd
مكتبة cobra
cobra هي إطار عمل CLI الأكثر شعبية في مجتمع Go، تستخدمه مشاريع مثل kubectl وdocker وhugo:
المكونات الأساسية لـ cobra:
- RootCmd: الأمر الجذري، نقطة دخول البرنامج
- SubCmd: الأوامر الفرعية، مثل "add" و"list" و"delete"
- Run/RunE: دوال تنفيذ الأمر
- Flags: معاملات العلامات المرتبطة بالأوامر
بنية الجملة والاستخدام الأساسي
1. أساسيات os.Args
package main
import (
"fmt"
"os"
)
func main() {
// os.Args هو مقطع سلاسل نصية، العنصر الأول هو اسم البرنامج
args := os.Args
fmt.Printf("عدد المعاملات: %d\n", len(args))
fmt.Printf("اسم البرنامج: %s\n", args[0])
// التكرار عبر جميع المعاملات
if len(args) > 1 {
fmt.Println("المعاملات الممررة:")
for i, arg := range args[1:] {
fmt.Printf(" [%d] %s\n", i, arg)
}
}
}
# تشغيل الاختبار
$ go run main.go hello world
عدد المعاملات: 3
اسم البرنامج: /tmp/go-build.../main
المعاملات الممررة:
[0] hello
[1] world
os.Args[0] ليس بالضرورة اسم البرنامج الذي كتبته — إنه مسار الملف التنفيذي الذي تمرره منظمة التشغيل. كن على علم بهذا الاختلاف عند العمل عبر الأنظمة الأساسية.
2. تحليل العلامات باستخدام حزمة flag
package main
import (
"flag"
"fmt"
)
func main() {
// تعريف معاملات العلامات
name := flag.String("name", "World", "اسمك")
age := flag.Int("age", 18, "عمرك")
verbose := flag.Bool("v", false, "إخراج مفصل")
// تحليل معاملات سطر الأوامر
flag.Parse()
// استخدام القيم المحللة (ملاحظة: هي مؤشرات، تحتاج إلى إلغاء الإشارة)
if *verbose {
fmt.Printf("[تصحيح] name=%s, age=%d\n", *name, *age)
}
fmt.Printf("مرحبًا، %s! أنت تبلغ %d سنة.\n", *name, *age)
// الحصول على المعاملات غير العلامية (المعاملات الموقعية)
fmt.Printf("المعاملات المتبقية: %v\n", flag.Args())
}
$ go run main.go -name=Alice -age=25 -v
[تصحيح] name=Alice, age=25
مرحبًا، Alice! أنت تبلغ 25 سنة.
المعاملات المتبقية: []
$ go run main.go --help
Usage of main:
-age int
عمرك (الافتراضي 18)
-name string
اسمك (الافتراضي "World")
-v إخراج مفصل
flag نمطين للتخصيص: -flag=value و-flag value. ومع ذلك، مع نمط -flag value، يجب أن يعقب -flag القيمة مباشرة ولا يمكن دمجه مع علامات أخرى.
3. استخدام ربط المتغيرات
package main
import (
"flag"
"fmt"
)
func main() {
// استخدام ربط المتغيرات للعمل مباشرة على المتغيرات بدلاً من المؤشرات
var host string
var port int
var debug bool
flag.StringVar(&host, "host", "localhost", "عنوان الخادم")
flag.IntVar(&port, "port", 8080, "رقم المنفذ")
flag.BoolVar(&debug, "debug", false, "تمكين وضع التصحيح")
flag.Parse()
fmt.Printf("الاتصال بـ %s:%d (تصحيح: %v)\n", host, port, debug)
}
StringVar/IntVar/BoolVar بمتغيرات موجودة، مناسبة للسيناريوهات التي تحتاج إلى مشاركة التكوين عبر أماكن متعددة. بينما تُرجع String/Int/Bool المقابلة مؤشرات، مناسبة للاستخدام المحلي.
4. هيكل cobra الأساسي
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
// الأمر الجذري
rootCmd := &cobra.Command{
Use: "myapp",
Short: "تطبيق CLI تجريبي",
Long: "هذا تطبيق CLI تجريبي مبني بـ cobra.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("مرحبًا بك في myapp!")
},
}
// أمر فرعي
helloCmd := &cobra.Command{
Use: "hello [name]",
Short: "قل مرحبا",
Args: cobra.MinimumNArgs(1), // مطلوب معامل واحد على الأقل
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("مرحبًا، %s!\n", args[0])
},
}
// إضافة الأمر الفرعي إلى الأمر الجذري
rootCmd.AddCommand(helloCmd)
// تنفيذ
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
$ go run main.go
مرحبًا بك في myapp!
$ go run main.go hello Go
مرحبًا، Go!
$ go run main.go --help
تطبيق CLI تجريبي
Usage:
myapp [command]
Available Commands:
hello قل مرحبا
help Help about any command
Flags:
-h, --help help for myapp
--help و-h، لا حاجة للتنفيذ يدويًا. كما تولد تلميحات استخدام تلقائية عندما تكون المعاملات غير صحيحة.
5. ربط علامات cobra
package main
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
func main() {
var name string
var count int
rootCmd := &cobra.Command{
Use: "greeter",
Short: "أداة ترحيب متكررة",
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < count; i++ {
fmt.Printf("مرحبًا، %s! (%d/%d)\n", name, i+1, count)
}
},
}
// علامات مستمرة (تنطبق على جميع الأوامر الفرعية)
rootCmd.PersistentFlags().StringVarP(&name, "name", "n", "World", "هدف الترحيب")
// علامات محلية (تنطبق فقط على الأمر الحالي)
rootCmd.Flags().IntVarP(&count, "count", "c", 1, "عدد التكرارات")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
$ go run main.go -n Alice -c 3
مرحبًا، Alice! (1/3)
مرحبًا، Alice! (2/3)
مرحبًا، Alice! (3/3)
6. الإدخال التفاعلي
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة إدخال سطر واحد
fmt.Print("أدخل اسمك: ")
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name) // إزالة حرف السطر الجديد
// قراءة إدخال مع قيمة افتراضية
fmt.Print("أدخل عمرك (الافتراضي 18): ")
ageInput, _ := reader.ReadString('\n')
ageInput = strings.TrimSpace(ageInput)
if ageInput == "" {
ageInput = "18"
}
fmt.Printf("مرحبًا، %s! أنت تبلغ %s سنة.\n", name, ageInput)
}
bufio.NewReader أكثر موثوقية من fmt.Scanln — يتعامل بشكل صحيح مع الإدخال الذي يحتوي على مسافات ولن يقطع بسبب أحرف السطر الجديد.
7. إدخال كلمة المرور (أحرف مخفية)
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/term"
)
func main() {
fmt.Print("أدخل كلمة المرور: ")
// قراءة كلمة المرور (الأحرف لا تُعرض)
password, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Fprintf(os.Stderr, "فشل في قراءة كلمة المرور: %v\n", err)
return
}
fmt.Println() // سطر جديد بعد القراءة
fmt.Printf("طول كلمة المرور: %d\n", len(password))
}
golang.org/x/term هي مكتبة الامتداد الرسمية لـ Go، توفر عمليات طرفية عبر الأنظمة الأساسية بما في ذلك قراءة كلمة المرور والتحكم بالمؤشر والمزيد.
كود المثال
مثال: آلة حاسبة أساسية لسطر الأوامر (الصعوبة ⭐)
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
func main() {
// تعريف العلامات
op := flag.String("op", "add", "نوع العملية: add, sub, mul, div")
verbose := flag.Bool("v", false, "عرض معلومات مفصلة")
flag.Parse()
// التحقق من عدد المعاملات الموقعية
args := flag.Args()
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "خطأ: مطلوب رقمان على الأقل كمعاملات")
fmt.Fprintln(os.Stderr, "الاستخدام: calc -op=add 10 20")
flag.Usage()
os.Exit(1)
}
// تحليل الأرقام
a, err := strconv.ParseFloat(args[0], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "خطأ: لا يمكن تحليل الرقم %q: %v\n", args[0], err)
os.Exit(1)
}
b, err := strconv.ParseFloat(args[1], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "خطأ: لا يمكن تحليل الرقم %q: %v\n", args[1], err)
os.Exit(1)
}
// تنفيذ العملية
var result float64
var opName string
switch *op {
case "add":
result = a + b
opName = "جمع"
case "sub":
result = a - b
opName = "طرح"
case "mul":
result = a * b
opName = "ضرب"
case "div":
if b == 0 {
fmt.Fprintln(os.Stderr, "خطأ: القاسم لا يمكن أن يكون صفرًا")
os.Exit(1)
}
result = a / b
opName = "قسمة"
default:
fmt.Fprintf(os.Stderr, "خطأ: نوع عملية غير مدعوم %q\n", *op)
os.Exit(1)
}
// إخراج النتيجة
if *verbose {
fmt.Printf("العملية: %s\n", opName)
fmt.Printf("التعبير: %g %s %g\n", a, map[string]string{
"add": "+", "sub": "-", "mul": "*", "div": "/",
}[*op], b)
}
fmt.Printf("النتيجة: %g\n", result)
}
$ go run main.go -op=add 10 20
النتيجة: 30
$ go run main.go -op=mul -v 3.5 4
العملية: ضرب
التعبير: 3.5 * 4
النتيجة: 14
$ go run main.go -op=div 10 0
خطأ: القاسم لا يمكن أن يكون صفرًا
$ go run main.go 10
خطأ: مطلوب رقمان على الأقل كمعاملات
مثال: أداة cobra متعددة الأوامر الفرعية — مدير الملفات (الصعوبة ⭐⭐)
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
)
var verbose bool
func main() {
rootCmd := &cobra.Command{
Use: "filetool",
Short: "أداة إدارة ملفات بسيطة",
Long: "filetool هي أداة CLI لعرض وإدارة الملفات.",
}
// علامات مستمرة
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "إخراج مفصل")
// إضافة أوامر فرعية
rootCmd.AddCommand(infoCmd())
rootCmd.AddCommand(listCmd())
rootCmd.AddCommand(searchCmd())
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
// أمر فرعي info: عرض تفاصيل الملف
func infoCmd() *cobra.Command {
return &cobra.Command{
Use: "info <مسار الملف>",
Short: "عرض تفاصيل الملف",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
path := args[0]
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("لا يمكن الوصول إلى %q: %w", path, err)
}
fmt.Printf("اسم الملف: %s\n", info.Name())
fmt.Printf("الحجم: %d بايت\n", info.Size())
fmt.Printf("آخر تعديل: %s\n", info.ModTime().Format("2006-01-02 15:04:05"))
fmt.Printf("مجلد: %v\n", info.IsDir())
fmt.Printf("الأذونات: %s\n", info.Mode())
if verbose {
fmt.Printf("المسار المطلق: ")
abs, err := filepath.Abs(path)
if err == nil {
fmt.Println(abs)
}
fmt.Printf("الامتداد: %s\n", filepath.Ext(path))
}
return nil
},
}
}
// أمر فرعي list: عرض محتويات المجلد
func listCmd() *cobra.Command {
var showHidden bool
cmd := &cobra.Command{
Use: "list [مسار المجلد]",
Short: "عرض محتويات المجلد",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dir := "."
if len(args) > 0 {
dir = args[0]
}
entries, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("لا يمكن قراءة المجلد %q: %w", dir, err)
}
fmt.Printf("المجلد: %s\n", dir)
fmt.Println(strings.Repeat("─", 50))
count := 0
for _, entry := range entries {
// تخطي الملفات المخفية (إلا إذا تم تحديد -a)
if !showHidden && strings.HasPrefix(entry.Name(), ".") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
// إضافة لاحقة / للمجلدات
name := entry.Name()
if entry.IsDir() {
name += "/"
}
fmt.Printf(" %-30s %8d %s\n",
name,
info.Size(),
info.ModTime().Format("2006-01-02 15:04"))
count++
}
fmt.Printf("\nالمجموع %d عنصر\n", count)
return nil
},
}
cmd.Flags().BoolVarP(&showHidden, "all", "a", false, "عرض الملفات المخفية")
return cmd
}
// أمر فرعي search: البحث عن الملفات حسب الامتداد
func searchCmd() *cobra.Command {
var maxDepth int
cmd := &cobra.Command{
Use: "search <الامتداد>",
Short: "البحث عن الملفات حسب الامتداد",
Long: "البحث عن الملفات ذات الامتداد المحدد في المجلد الحالي والمجلدات الفرعية. مثال: filetool search .go",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ext := args[0]
if !strings.HasPrefix(ext, ".") {
ext = "." + ext
}
root := "."
found := 0
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil // تخطي الملفات غير القابلة للوصول
}
// التحقق من العمق
if maxDepth > 0 {
depth := strings.Count(filepath.Clean(path), string(os.PathSeparator)) -
strings.Count(filepath.Clean(root), string(os.PathSeparator))
if depth > maxDepth {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
if !info.IsDir() && filepath.Ext(path) == ext {
fmt.Printf(" %s (%d بايت، %s)\n",
path,
info.Size(),
info.ModTime().Format("2006-01-02"))
found++
}
return nil
})
if err != nil {
return fmt.Errorf("خطأ في البحث: %w", err)
}
fmt.Printf("\nتم العثور على %d ملف %s\n", found, ext)
return nil
},
}
cmd.Flags().IntVarP(&maxDepth, "depth", "d", 0, "الحد الأقصى لعمق البحث (0 يعني غير محدود)")
return cmd
}
$ go run main.go info main.go
اسم الملف: main.go
الحجم: 2048 بايت
آخر تعديل: 2025-06-27 14:30:00
مجلد: false
الأذونات: -rw-r--r--
$ go run main.go list -a
المجلد: .
──────────────────────────────────────────────────
.git/ 4096 2025-06-27 14:00
main.go 2048 2025-06-27 14:30
go.mod 128 2025-06-27 14:00
المجموع 3 عنصر
$ go run main.go search .go -d 2
./main.go (2048 بايت، 2025-06-27)
./internal/handler.go (1024 بايت، 2025-06-26)
تم العثور على 2 ملف .go
مثال: تطبيق Todo CLI كامل (الصعوبة ⭐⭐⭐)
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
)
// هيكل عنصر Todo
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
DoneAt *time.Time `json:"done_at,omitempty"`
}
// TodoList يدعم التخزين المستمر
type TodoList struct {
Todos []Todo `json:"todos"`
NextID int `json:"next_id"`
filePath string
}
// NewTodoList ينشئ أو يحمل قائمة مهام
func NewTodoList(filePath string) (*TodoList, error) {
list := &TodoList{
Todos: []Todo{},
NextID: 1,
filePath: filePath,
}
// إذا كان الملف موجودًا، حمّل البيانات
if _, err := os.Stat(filePath); err == nil {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("فشل في قراءة ملف البيانات: %w", err)
}
if len(data) > 0 {
if err := json.Unmarshal(data, list); err != nil {
return nil, fmt.Errorf("فشل في تحليل ملف البيانات: %w", err)
}
}
}
return list, nil
}
// Save يحفظ إلى ملف
func (tl *TodoList) Save() error {
// التأكد من وجود المجلد
dir := filepath.Dir(tl.filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("فشل في إنشاء المجلد: %w", err)
}
data, err := json.MarshalIndent(tl, "", " ")
if err != nil {
return fmt.Errorf("فشل في تسلسل البيانات: %w", err)
}
return os.WriteFile(tl.filePath, data, 0644)
}
// Add يضيف مهمة جديدة
func (tl *TodoList) Add(title string) Todo {
todo := Todo{
ID: tl.NextID,
Title: title,
Done: false,
CreatedAt: time.Now(),
}
tl.Todos = append(tl.Todos, todo)
tl.NextID++
return todo
}
// Complete يعلّم المهمة كمكتملة
func (tl *TodoList) Complete(id int) error {
for i := range tl.Todos {
if tl.Todos[i].ID == id {
if tl.Todos[i].Done {
return fmt.Errorf("المهمة #%d مكتملة بالفعل", id)
}
tl.Todos[i].Done = true
now := time.Now()
tl.Todos[i].DoneAt = &now
return nil
}
}
return fmt.Errorf("المهمة #%d غير موجودة", id)
}
// Delete يحذف مهمة
func (tl *TodoList) Delete(id int) error {
for i, todo := range tl.Todos {
if todo.ID == id {
tl.Todos = append(tl.Todos[:i], tl.Todos[i+1:]...)
return nil
}
}
return fmt.Errorf("المهمة #%d غير موجودة", id)
}
// Filter يُرجع قائمة مفلترة
func (tl *TodoList) Filter(showDone bool) []Todo {
var result []Todo
for _, todo := range tl.Todos {
if showDone || !todo.Done {
result = append(result, todo)
}
}
return result
}
// dataFilePath يُرجع مسار ملف البيانات الافتراضي
func dataFilePath() string {
home, err := os.UserHomeDir()
if err != nil {
return ".todo.json"
}
return filepath.Join(home, ".todo.json")
}
func main() {
dataFile := dataFilePath()
rootCmd := &cobra.Command{
Use: "todo",
Short: "مدير مهام سطر الأوامر",
Long: `todo هو مدير مهام بسيط لسطر الأوامر.
يتم تخزين البيانات في ~/.todo.json.`,
}
// أمر فرعي add
var addCmd = &cobra.Command{
Use: "add <عنوان المهمة>",
Short: "إضافة مهمة جديدة",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
title := strings.Join(args, " ")
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
todo := list.Add(title)
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ تمت إضافة المهمة #%d: %s\n", todo.ID, todo.Title)
return nil
},
}
// أمر فرعي done
var doneCmd = &cobra.Command{
Use: "done <معرّف المهمة>",
Short: "تعليم المهمة كمكتملة",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("معرّف مهمة غير صالح: %q", args[0])
}
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
if err := list.Complete(id); err != nil {
return err
}
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ المهمة #%d مكتملة!\n", id)
return nil
},
}
// أمر فرعي list
var showAll bool
var listCmd = &cobra.Command{
Use: "list",
Short: "عرض قائمة المهام",
RunE: func(cmd *cobra.Command, args []string) error {
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
todos := list.Filter(showAll)
if len(todos) == 0 {
if showAll {
fmt.Println("لا توجد مهام بعد.")
} else {
fmt.Println("لا توجد مهام معلقة! استخدم -a لرؤية جميع المهام.")
}
return nil
}
fmt.Printf("%-4s %-8s %-30s %s\n", "المعرّف", "الحالة", "العنوان", "تاريخ الإنشاء")
fmt.Println(strings.Repeat("─", 60))
for _, todo := range todos {
status := "○"
if todo.Done {
status = "✓"
}
fmt.Printf("%-4d %-8s %-30s %s\n",
todo.ID,
status,
truncate(todo.Title, 28),
todo.CreatedAt.Format("01-02 15:04"))
}
// الإحصائيات
total := len(list.Todos)
done := 0
for _, t := range list.Todos {
if t.Done {
done++
}
}
fmt.Printf("\nالمجموع %d مهمة، %d مكتملة، %d معلقة\n", total, done, total-done)
return nil
},
}
listCmd.Flags().BoolVarP(&showAll, "all", "a", false, "عرض جميع المهام (بما في ذلك المكتملة)")
// أمر فرعي delete
var deleteCmd = &cobra.Command{
Use: "delete <معرّف المهمة>",
Short: "حذف مهمة",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("معرّف مهمة غير صالح: %q", args[0])
}
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
if err := list.Delete(id); err != nil {
return err
}
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ تم حذف المهمة #%d\n", id)
return nil
},
}
// أمر فرعي clear
var clearCmd = &cobra.Command{
Use: "clear",
Short: "مسح جميع المهام المكتملة",
RunE: func(cmd *cobra.Command, args []string) error {
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
before := len(list.Todos)
var remaining []Todo
for _, todo := range list.Todos {
if !todo.Done {
remaining = append(remaining, todo)
}
}
list.Todos = remaining
removed := before - len(list.Todos)
if removed == 0 {
fmt.Println("لا توجد مهام مكتملة للمسح.")
return nil
}
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ تم مسح %d مهمة مكتملة\n", removed)
return nil
},
}
rootCmd.AddCommand(addCmd, doneCmd, listCmd, deleteCmd, clearCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
// truncate تقطع سلسلة نصية
func truncate(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen-2]) + ".."
}
$ todo add تعلم تطوير Go CLI
✓ تمت إضافة المهمة #1: تعلم تطوير Go CLI
$ todo add إكمال التمارين
✓ تمت إضافة المهمة #2: إكمال التمارين
$ todo add كتابة مدونة تقنية
✓ تمت إضافة المهمة #3: كتابة مدونة تقنية
$ todo list
المعرّف الحالة العنوان تاريخ الإنشاء
────────────────────────────────────────────────────────────
1 ○ تعلم تطوير Go CLI 06-27 14:30
2 ○ إكمال التمارين 06-27 14:31
3 ○ كتابة مدونة تقنية 06-27 14:32
المجموع 3 مهام، 0 مكتملة، 3 معلقة
$ todo done 1
✓ المهمة #1 مكتملة!
$ todo list -a
المعرّف الحالة العنوان تاريخ الإنشاء
────────────────────────────────────────────────────────────
1 ✓ تعلم تطوير Go CLI 06-27 14:30
2 ○ إكمال التمارين 06-27 14:31
3 ○ كتابة مدونة تقنية 06-27 14:32
المجموع 3 مهام، 1 مكتملة، 2 معلقة
$ todo clear
✓ تم مسح 1 مهمة مكتملة
سيناريوهات واقعية
السيناريو 1: التحقق من المعاملات والتحقق المخصص
package main
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
)
// PortValidator يتحقق من أرقام المنافذ
func PortValidator(s string) error {
port, err := strconv.Atoi(s)
if err != nil {
return fmt.Errorf("المنفذ يجب أن يكون رقمًا، تم الاستلام: %q", s)
}
if port < 1 || port > 65535 {
return fmt.Errorf("المنفذ يجب أن يكون بين 1-65535، تم الاستلام: %d", port)
}
return nil
}
// EmailValidator تحقق بسيط من تنسيق البريد الإلكتروني
func EmailValidator(email string) error {
if !strings.Contains(email, "@") {
return fmt.Errorf("تنسيق بريد إلكتروني غير صالح، رمز @ مفقود: %q", email)
}
parts := strings.SplitN(email, "@", 2)
if len(parts[0]) == 0 || len(parts[1]) == 0 {
return fmt.Errorf("تنسيق بريد إلكتروني غير صالح: %q", email)
}
if !strings.Contains(parts[1], ".") {
return fmt.Errorf("تنسيق نطاق بريد إلكتروني غير صالح: %q", email)
}
return nil
}
// HostPortValidator يتحقق من تنسيق host:port
func HostPortValidator(addr string) error {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("التنسيق يجب أن يكون host:port، تم الاستلام: %q، الخطأ: %v", addr, err)
}
if host == "" {
return fmt.Errorf("اسم المضيف لا يمكن أن يكون فارغًا: %q", addr)
}
return PortValidator(port)
}
func main() {
var email string
var port int
var serverAddr string
rootCmd := &cobra.Command{
Use: "server",
Short: "بدء تشغيل الخادم",
PreRunE: func(cmd *cobra.Command, args []string) error {
// التحقق من جميع المعاملات قبل التنفيذ
if err := EmailValidator(email); err != nil {
return fmt.Errorf("البريد الإلكتروني للمسؤول: %w", err)
}
if err := HostPortValidator(serverAddr); err != nil {
return fmt.Errorf("عنوان الخادم: %w", err)
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("بدء تشغيل الخادم...\n")
fmt.Printf(" العنوان: %s\n", serverAddr)
fmt.Printf(" المسؤول: %s\n", email)
fmt.Printf(" المنفذ: %d\n", port)
},
}
rootCmd.Flags().StringVarP(&email, "email", "e", "admin@example.com", "البريد الإلكتروني للمسؤول")
rootCmd.Flags().IntVarP(&port, "port", "p", 8080, "رقم المنفذ")
rootCmd.Flags().StringVarP(&serverAddr, "addr", "a", "localhost:8080", "عنوان الخادم (host:port)")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "خطأ: %v\n", err)
os.Exit(1)
}
}
$ go run main.go --addr "invalid"
خطأ: عنوان الخادم: التنسيق يجب أن يكون host:port، تم الاستلام: "invalid"، الخطأ: address invalid: missing port in address
$ go run main.go --email "not-email"
خطأ: البريد الإلكتروني للمسؤول: تنسيق بريد إلكتروني غير صالح، رمز @ مفقود: "not-email"
$ go run main.go -a "0.0.0.0:9090" -e "admin@mysite.com"
بدء تشغيل الخادم...
العنوان: 0.0.0.0:9090
المسؤول: admin@mysite.com
المنفذ: 8080
السيناريو 2: شريط التقدم والإخراج الملون
package main
import (
"fmt"
"math"
"os"
"strings"
"time"
"github.com/spf13/cobra"
)
// ثوابت ألوان ANSI
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorCyan = "\033[36m"
ColorBold = "\033[1m"
)
// ProgressBar شريط التقدم
type ProgressBar struct {
total int
current int
width int
label string
}
// NewProgressBar ينشئ شريط تقدم
func NewProgressBar(total int, label string) *ProgressBar {
return &ProgressBar{
total: total,
width: 40,
label: label,
}
}
// Update يحدث التقدم ويعرضه
func (p *ProgressBar) Update(current int) {
p.current = current
percent := float64(p.current) / float64(p.total)
filled := int(math.Round(percent * float64(p.width)))
bar := strings.Repeat("█", filled) + strings.Repeat("░", p.width-filled)
// تغيير اللون مع التقدم
color := ColorYellow
if percent >= 0.8 {
color = ColorGreen
} else if percent >= 0.5 {
color = ColorCyan
}
fmt.Fprintf(os.Stderr, "\r%s %s[%s%s%s] %s%d/%d%s (%.0f%%)",
p.label,
ColorBold,
color, bar, ColorReset,
ColorBold, p.current, p.total, ColorReset,
percent*100)
if p.current >= p.total {
fmt.Fprintf(os.Stderr, "\n")
}
}
func main() {
rootCmd := &cobra.Command{
Use: "downloader",
Short: "محاكاة تحميل ملفات (عرض شريط التقدم)",
Run: func(cmd *cobra.Command, args []string) {
files := []string{
"go1.21.0.linux-amd64.tar.gz",
"docs.tar.gz",
"examples.zip",
}
totalSteps := 100
for _, file := range files {
fmt.Printf("\n%sجاري التحميل: %s%s\n", ColorBlue, file, ColorReset)
bar := NewProgressBar(totalSteps, " التقدم")
for i := 0; i <= totalSteps; i++ {
bar.Update(i)
// محاكاة تأخير التحميل
time.Sleep(20 * time.Millisecond)
}
fmt.Printf(" %s✓ اكتمل التحميل%s\n", ColorGreen, ColorReset)
}
fmt.Printf("\n%s%sاكتملت جميع التنزيلات!%s\n", ColorBold, ColorGreen, ColorReset)
},
}
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
$ go run main.go
جاري التحميل: go1.21.0.linux-amd64.tar.gz
التقدم [████████████████████████████████████████] 100/100 (100%)
✓ اكتمل التحميل
جاري التحميل: docs.tar.gz
التقدم [████████████████████████████████████████] 100/100 (100%)
✓ اكتمل التحميل
اكتملت جميع التنزيلات!
❓ أسئلة شائعة
س1: كيف تتعامل حزمة flag مع أسماء العلامات المكررة؟
// لا تسمح حزمة flag بتعريف أسماء علامات مكررة، ستتسببت في panic
// لكن يمكنك تعريف علامات مسمى نفسها في أوامر فرعية مختلفة (إذا كنت تنفذ منطق الأوامر الفرعية بنفسك)
// عند استخدام cobra، تكون علامات كل أمر مستقلة ولن تتعارض
cmd1.Flags().StringVar(&name, "name", "", "اسم الأمر1")
cmd2.Flags().StringVar(&name, "name", "", "اسم الأمر2") // تمامًا OK
س2: كيف نجعل cobra يدعم ملفات التكوين المستمرة (مثل ~/.config/myapp.yaml)؟
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func initConfig() {
viper.SetConfigName("config") // اسم ملف التكوين (بدون امتداد)
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME/.config/myapp")
viper.AddConfigPath(".")
// تجاوز متغيرات البيئة
viper.AutomaticEnv()
// قراءة ملف التكوين (السماح بعدم الوجود)
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "استخدام ملف التكوين:", viper.ConfigFileUsed())
}
}
func main() {
rootCmd := &cobra.Command{
Use: "myapp",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConfig()
},
}
// ...
}
viper هو أفضل رفيق لـ cobra، يوفر قراءة موحدة لملفات التكوين ومتغيرات البيئة وعلامات سطر الأوامر. الأولوية: علامات سطر الأوامر > متغيرات البيئة > ملف التكوين > الافتراضات.
س3: ما الفرق بين os.Args وflag.Args()؟
// os.Args تحتوي على جميع المعاملات الخام، بما في ذلك اسم البرنامج
os.Args // ["myapp", "-v", "hello", "world"]
// بعد flag.Parse():
// flag.Args() تحتوي فقط على المعاملات غير العلامية (المعاملات الموقعية)
flag.Args() // ["hello", "world"]
// flag استهلكت -v، لن تظهر في Args()
س4: كيفية التعامل مع إكمال Tab للأوامر الفرعية؟
# تحتوي cobra على إنشاء إكمال صدفة مدمج
$ todo completion bash > /etc/bash_completion.d/todo
$ todo completion zsh > "${fpath[1]}/_todo"
$ todo completion fish > ~/.config/fish/completions/todo.fish
# للعلامات المخصصة، يمكنك تسجيل دوال الإكمال
cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json", "yaml", "table"}, cobra.ShellCompDirectiveNoFileComp
})
📖 ملخص
في هذا الدرس تعلمنا المحتوى الأساسي لتطوير أدوات سطر الأوامر في Go:
| الموضوع | الأداة | النقاط الرئيسية |
|---|---|---|
| المعاملات الخام | os.Args |
مقطع سلاسل نصية، [0] هو اسم البرنامج |
| تحليل العلامات | حزمة flag |
علامات مصنفة، مساعدة تلقائية، معاملات موقعية |
| إطار عمل CLI | cobra |
أوامر فرعية، علامات مستمرة، إكمال تلقائي |
| التحقق من المعاملات | دوال مخصصة | مدققات PreRunE/Args |
| الإدخال التفاعلي | bufio/term |
قراءة إدخال الأسطر، إدخال كلمة مرور مخفي |
النقاط الأساسية:
- حزمة
flagكافية للأدوات البسيطة، استخدمcobraللأدوات المعقدة PersistentFlagsفي cobra تنطبق على جميع الأوامر الفرعية، بينماFlagsتنطبق فقط على الأمر الحالي- يجب معالجة التحقق من المعاملات بشكل موحد في
PreRunE، مع إبقاء دالةRunنظيفة - استخدم ملفات JSON للحفاظ على البيانات، مخزنة في دليل المستخدم المنزل
- شريط التقدم والإخراج الملون يمكن أن يحسنا بشكل كبير تجربة مستخدم أداة سطر الأوامر
📝 تمارين
التمرين 1: عارض متغيرات البيئة
اكتب أداة CLI envtool تدعم الميزات التالية:
# عرض جميع متغيرات البيئة
$ envtool list
# البحث في متغيرات البيئة تحتوي على كلمة مفتاحية
$ envtool search PATH
# الحصول على قيمة متغير بيئة محدد
$ envtool get GOPATH
# تعيين متغير بيئة (العملية الحالية فقط)
$ envtool set MY_VAR=hello
المتطلب: التنفيذ باستخدام cobra، مع دعم علامة --format=json|table للتحكم في تنسيق الإخراج.
التمرين 2: مولّد كلمات المرور
اكتب مولّد كلمات مرور CLI passgen:
# توليد كلمة مرور افتراضية (16 حرفًا، تتضمن أحرف كبيرة وصغيرة وأرقام)
$ passgen
# تحديد الطول ومجموعة الأحرف
$ passgen -l 32 -s "abc123!@#"
# توليد دفعي
$ passgen -n 5
# استبعاد الأحرف الغامضة (0/O، 1/l/I)
$ passgen --no-ambiguous
المتطلبات:
- التنفيذ باستخدام حزمة
flag - يجب تقييم قوة كلمة المرور وعرضها (ضعيفة/متوسطة/قوية)
- يمكن نسخ النتائج المولدة إلى الحافظة (اختياري)
التمرين 3: أداة معالجة الملفات الدفعية
اكتب أداة batch تدعم عمليات الملفات الدفعية:
# إعادة تسمية دفعية: إضافة بادئة
$ batch rename --prefix "2025_" *.jpg
# تحويل حالة الأحرف دفعيًا
$ batch case --to lower *.TXT
# إحصائيات دفعية
$ batch stats ./docs
# بحث واستبدال دفعي (محاكاة)
$ batch replace --old "foo" --new "bar" *.go
المتطلب: التنفيذ باستخدام أوامر cobra الفرعية، كل عملية كأمر فرعي مستقل.
الدرس التالي
في الدرس التالي سنتعلم عن تطوير REST API،covering كيفية بناء خدمات RESTful API باستخدام Go، بما في ذلك تصميم المسارات والوسائط ومعالجة JSON وتكامل قواعد البيانات وغيرها من المهارات العملية.



