דילוג לתוכן המרכזי
מודול 10 · שיעור 4 מתוך 549/50 בקורס~18 min
שיעורי מודול (4/5)

מיני-פרויקט: CLI עם דגלים

בואו נחבר את אבני הבניין של המודולים הקודמים כדי לבנות CLI אידיומטי קטן: ניתוח דגל עם החבילה flag, טיפול בארגומנטים מיקוםיים, שגיאות ב-os.Stderr וקוד היציאה הנכון.

החבילה flag

Go
package main

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

func main() {
    n := flag.Int("n", 1, "number of repetitions")
    upper := flag.Bool("u", false, "print in uppercase")
    flag.Parse()

    args := flag.Args() // remaining positional arguments
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
        os.Exit(2)
    }

    text := args[0]
    if *upper {
        text = strings.ToUpper(text)
    }
    for i := 0; i < *n; i++ {
        fmt.Println(text)
    }
}

נקודות מפתח:

  • flag.Int, flag.String, flag.Bool, flag.Duration (וכו') מחזירים מצביע: אתה משתמש ב-*n, *upper.
  • יש לקרוא ל-flag.Parse() פעם אחת, לפני קריאת הערכים ולפני flag.Args().
  • flag.Args() מחזיר את הארגומנטים מיקום (אלה שאחרי -- או אחרי הדגל האחרון).
  • טפסים מקובלים: -n 3, -n=3, --n=3. בוליאניות: -u (נכון), -u=false.

קודי יציאה קונבנציונליים

Go
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)

צדפים וסקריפטים מצפים לקודים האלה: השתמש בהם באופן עקבי. לעולם לא תצא מגורוטינה שאינה ראשית עם os.Exit: הוא מדלג על כל defer, כולל שטיפה log.

סטדר נגד סטדאוט

כלל יוניקס ישן וקדוש:

  • os.Stdout → פלט שימושי, ניתן לצינור (cli | grep ...).
  • os.Stderr → אבחון, שגיאות, פסי התקדמות, שימוש.
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

כך cli 2>/dev/null מציג רק את התוצאות, cli >out.txt מפנה רק את התוצאה והשגיאות נשארות גלויות.

log לעומת fmt

  • fmt עבור פלט משתמש.
  • log לאבחון: מוסיף חותמת זמן, כותב ל-os.Stderr כברירת מחדל, יש log.Fatal (יומנים + os.Exit(1)) ו-log.Panic.
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

עבור אפליקציות מודרניות (Go 1.21+) השתמש ב-log/slog לרישום מובנה (JSON או טקסט, רמות, שדות מוקלדים).

ארכיטקטורה: הפרד main מהלוגיקה

Go
func main() {
    if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(args []string, stdout, stderr io.Writer) error {
    // here use a new flag.FlagSet instead of the global flag.CommandLine
    fs := flag.NewFlagSet("echo", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "repetitions")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    return nil
}

יתרונות:

  • ניתן לבדיקה: אתה מעביר את bytes.Buffer בתור stdout/stderr ומאמת את הפלט.
  • אין מצב גלובלי: כל FlagSet מבודדת.
  • main נשאר זעיר: הוא מכין IO + נציגים.

פקודות משנה

עבור CLIs עם פקודות משנה (mycli serve, mycli migrate), סכימה ללא תלות חיצונית:

Go
func main() {
    if len(os.Args) < 2 {
        usage()
        os.Exit(2)
    }
    switch os.Args[1] {
    case "serve":
        serveCmd(os.Args[2:])
    case "migrate":
        migrateCmd(os.Args[2:])
    default:
        usage()
        os.Exit(2)
    }
}

לכל פקודה משנה יש flag.NewFlagSet משלה. עבור פרויקטים גדולים יותר, ספריות כמו cobra או urfave/cli בונות את הסכימה הזו בעזרת עזרה שנוצרה אוטומטית.

תרגילים

פעילות גופנית#go.m10.l4.e1
ניסיונות: 0טוען...

Definisci il flag -n di tipo int con valore di default 1 e descrizione 'ripetizioni', poi chiama flag.Parse() e stampa il valore.

טוען עורך...

הפתרון זמין לאחר 3 ניסיונות

פעילות גופנית#go.m10.l4.e2
ניסיונות: 0טוען...

Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e termina con os.Exit(1).

טוען עורך...

הפתרון זמין לאחר 3 ניסיונות

Quiz#go.m10.l4.e3
מוכן

האם ניתן להשתמש בהודעות שגיאה ב-CLI Unix ידידותי?

Go
fmt.Fprintln(???, "uso: prog ...")
אפשרויות תשובה