تطوير أدوات سطر الأوامر

الدرس 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

GO
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)
        }
    }
}
BASH
# تشغيل الاختبار
$ go run main.go hello world
عدد المعاملات: 3
اسم البرنامج: /tmp/go-build.../main
المعاملات الممررة:
  [0] hello
  [1] world
💡 نصيحة: os.Args[0] ليس بالضرورة اسم البرنامج الذي كتبته — إنه مسار الملف التنفيذي الذي تمرره منظمة التشغيل. كن على علم بهذا الاختلاف عند العمل عبر الأنظمة الأساسية.

2. تحليل العلامات باستخدام حزمة flag

GO
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())
}
BASH
$ 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. استخدام ربط المتغيرات

GO
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 الأساسي

GO
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)
    }
}
BASH
$ 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
💡 نصيحة: تعالج cobra تلقائيًا --help و-h، لا حاجة للتنفيذ يدويًا. كما تولد تلميحات استخدام تلقائية عندما تكون المعاملات غير صحيحة.

5. ربط علامات cobra

GO
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)
    }
}
BASH
$ go run main.go -n Alice -c 3
مرحبًا، Alice! (1/3)
مرحبًا، Alice! (2/3)
مرحبًا، Alice! (3/3)

6. الإدخال التفاعلي

GO
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. إدخال كلمة المرور (أحرف مخفية)

GO
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، توفر عمليات طرفية عبر الأنظمة الأساسية بما في ذلك قراءة كلمة المرور والتحكم بالمؤشر والمزيد.


كود المثال

مثال: آلة حاسبة أساسية لسطر الأوامر (الصعوبة ⭐)

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)
}
▶ جرّب الكود
BASH
$ 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 متعددة الأوامر الفرعية — مدير الملفات (الصعوبة ⭐⭐)

GO
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
}
▶ جرّب الكود
BASH
$ 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 كامل (الصعوبة ⭐⭐⭐)

GO
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]) + ".."
}
▶ جرّب الكود
BASH
$ 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: التحقق من المعاملات والتحقق المخصص

GO
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)
    }
}
BASH
$ 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: شريط التقدم والإخراج الملون

GO
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)
    }
}
BASH
$ go run main.go

جاري التحميل: go1.21.0.linux-amd64.tar.gz
  التقدم [████████████████████████████████████████] 100/100 (100%)
  ✓ اكتمل التحميل

جاري التحميل: docs.tar.gz
  التقدم [████████████████████████████████████████] 100/100 (100%)
  ✓ اكتمل التحميل

اكتملت جميع التنزيلات!

❓ أسئلة شائعة

س1: كيف تتعامل حزمة flag مع أسماء العلامات المكررة؟

GO
// لا تسمح حزمة flag بتعريف أسماء علامات مكررة، ستتسببت في panic
// لكن يمكنك تعريف علامات مسمى نفسها في أوامر فرعية مختلفة (إذا كنت تنفذ منطق الأوامر الفرعية بنفسك)

// عند استخدام cobra، تكون علامات كل أمر مستقلة ولن تتعارض
cmd1.Flags().StringVar(&name, "name", "", "اسم الأمر1")
cmd2.Flags().StringVar(&name, "name", "", "اسم الأمر2") // تمامًا OK

س2: كيف نجعل cobra يدعم ملفات التكوين المستمرة (مثل ~/.config/myapp.yaml

GO
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()؟

GO
// os.Args تحتوي على جميع المعاملات الخام، بما في ذلك اسم البرنامج
os.Args     // ["myapp", "-v", "hello", "world"]

// بعد flag.Parse():
// flag.Args() تحتوي فقط على المعاملات غير العلامية (المعاملات الموقعية)
flag.Args() // ["hello", "world"]

// flag استهلكت -v، لن تظهر في Args()

س4: كيفية التعامل مع إكمال Tab للأوامر الفرعية؟

BASH
# تحتوي 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 قراءة إدخال الأسطر، إدخال كلمة مرور مخفي

النقاط الأساسية:


📝 تمارين

التمرين 1: عارض متغيرات البيئة

اكتب أداة CLI envtool تدعم الميزات التالية:

BASH
# عرض جميع متغيرات البيئة
$ envtool list

# البحث في متغيرات البيئة تحتوي على كلمة مفتاحية
$ envtool search PATH

# الحصول على قيمة متغير بيئة محدد
$ envtool get GOPATH

# تعيين متغير بيئة (العملية الحالية فقط)
$ envtool set MY_VAR=hello

المتطلب: التنفيذ باستخدام cobra، مع دعم علامة --format=json|table للتحكم في تنسيق الإخراج.

التمرين 2: مولّد كلمات المرور

اكتب مولّد كلمات مرور CLI passgen:

BASH
# توليد كلمة مرور افتراضية (16 حرفًا، تتضمن أحرف كبيرة وصغيرة وأرقام)
$ passgen

# تحديد الطول ومجموعة الأحرف
$ passgen -l 32 -s "abc123!@#"

# توليد دفعي
$ passgen -n 5

# استبعاد الأحرف الغامضة (0/O، 1/l/I)
$ passgen --no-ambiguous

المتطلبات:

التمرين 3: أداة معالجة الملفات الدفعية

اكتب أداة batch تدعم عمليات الملفات الدفعية:

BASH
# إعادة تسمية دفعية: إضافة بادئة
$ 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 وتكامل قواعد البيانات وغيرها من المهارات العملية.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%