1. Po co pakiet flag, skoro jest os.Args
Kiedy piszesz program CLI, prawie zawsze chcesz, aby użytkownik mógł dostosować zachowanie: włączyć tryb debugowania, ustawić limit, wybrać format wyjścia. Oczywiście można ręcznie analizować os.Args, ale bardzo szybko zaczyna to przypominać rozpakowywanie walizki po urlopie: „skąd tu jeszcze trzy paczki i dlaczego skarpetki są w lodówce?”. Pakiet flag to standardowy sposób obsługi flag w Go, który daje jednolity składniowy format i usuwa część rutyny.
Podstawowa idea jest taka: os.Args to po prostu surowe []string „tak, jak przekazał je system operacyjny”, a flag potrafi zamienić je w zrozumiałe wartości odpowiednich typów. W efekcie dostajesz zgrabne string/int/bool, a nie wieczne święto parsowania: „czy to było -limit=10, czy -limit 10, a może -limit ten?”.
Wewnętrznie flag i tak patrzy na argumenty uruchomienia (domyślnie to os.Args[1:]), ale robi to centralnie, tak samo dla wszystkich flag i według standardowych zasad.
Można to zobrazować tak:
flowchart LR
A["os.Args (surowe []string)"] --> B["flag.Parse()"]
B --> C["wartości flag: string/int/bool"]
B --> D["argumenty pozycyjne: flag.Args()"]
2. Deklarowanie flag
Gdy po raz pierwszy widzisz API flag, wydaje się dziwne: dlaczego flag.String deklaruje flagę, a nie czyta? Dlaczego zwraca wskaźnik? I dlaczego nie można po prostu napisać „odczytaj mi flagi” jednym poleceniem? To wszystko są cechy modelu flag: najpierw opisujemy, jakie flagi obsługuje program, potem wywołujemy parsowanie, a dopiero potem czytamy wyniki.
flag.String, flag.Int, flag.Bool
Flagi deklaruje się funkcjami z rodziny:
- flag.String(name, defaultValue, usage)
- flag.Int(name, defaultValue, usage)
- flag.Bool(name, defaultValue, usage)
Każda z nich zwraca wskaźnik do wartości (*string, *int, *bool). Dlaczego tak jest — wyjaśnimy niżej, a na razie zapamiętaj praktyczną zasadę: po Parse() odczytujemy wartość przez *.
Mała tabela, żeby łatwiej było to ogarnąć:
| Funkcja | Typ wartości | Co zwraca |
|---|---|---|
|
|
|
|
|
|
|
|
|
Mini-przykład: zadeklarujmy trzy flagi dla naszego szkoleniowego „pseudo-todo” (na razie niczego nie przechowuje — po prostu wypisujemy, co byśmy zrobili).
package main
import (
"flag"
"fmt"
)
func main() {
title := flag.String("title", "", "task title")
priority := flag.Int("priority", 1, "priority 1..5")
done := flag.Bool("done", false, "mark task as done")
flag.Parse()
fmt.Println(*title, *priority, *done)
// (przykład wyjścia zależy od argumentów uruchomienia)
}
Zwróć uwagę: w ogóle nie dotykamy ręcznie os.Args. I to właśnie jest pierwszy „zwycięski” krok nad chaosem.
Dlaczego zwracane jest *T
Kiedy początkujący widzi *title, często myśli: „Prosiłem tylko o odczytanie stringa, dlaczego dostałem… adres stringa?”. To normalne pytanie. Odpowiedź jest taka, że flag najpierw tworzy „kontener” na wartość, a potem podczas Parse() zapisuje do niego wynik parsowania argumentów.
Można to wyobrazić sobie jak karteczkę na biurku:
- title — to kartka, na której zapisano, gdzie leży prawdziwa wartość
- *title — to sama wartość znajdująca się pod tym adresem
Czyli flag.String(...) zwraca nie string, lecz „miejsce, do którego potem włożą string”. A po flag.Parse() to miejsce jest wypełnione albo tym, co podał użytkownik, albo wartością domyślną.
Mała demonstracja, gdzie wypisujemy typy, żeby mózgowi było spokojniej:
package main
import (
"flag"
"fmt"
)
func main() {
title := flag.String("title", "untitled", "task title")
fmt.Printf("%T\n", title) // *string
flag.Parse()
fmt.Printf("%T\n", *title) // string
}
Nie zagłębiamy się tu w wskaźniki jako temat sam w sobie (to będzie osobna duża historia), ale warto przyzwyczaić się do jednego nawyku: wartość flagi zawsze czytamy przez *.
3. Parsowanie argumentów: flag.Parse()
W flag istnieje prawie rytualna kolejność działań: „zadeklarowaliśmy → sparsowaliśmy → odczytaliśmy”. A jeśli tę kolejność złamiesz, program będzie działał równie pewnie jak budzik bez baterii: niby znajoma rzecz, ale pożytku niewiele.
flag.Parse() robi kilka rzeczy naraz. Bierze argumenty uruchomienia, rozpoznaje wśród nich znane flagi, przekształca wartości do odpowiednich typów (na przykład string "10" w int(10) dla flag.Int) i zapisuje wynik do tych samych zmiennych-wskaźników, które otrzymałeś przy deklarowaniu flag. Wszystko, co nie należy do flag, zostawia jako „resztę” — argumenty pozycyjne.
Prosty przykład tego, co się stanie, jeśli zapomnisz o Parse(). (To typowy błąd, więc warto zobaczyć go raz.)
package main
import (
"flag"
"fmt"
)
func main() {
limit := flag.Int("limit", 10, "max items")
fmt.Println(*limit) // 10 (zawsze 10, dopóki nie wywołamy Parse)
}
A oto poprawna wersja:
package main
import (
"flag"
"fmt"
)
func main() {
limit := flag.Int("limit", 10, "max items")
flag.Parse()
fmt.Println(*limit) // zależy od -limit ...
}
Ważny szczegół: flag obsługuje oba popularne sposoby przekazywania wartości: -limit=5 i -limit 5. Nie musisz pisać dwóch parserów i zgadywać, którą wersję wybrał użytkownik.
4. Argumenty pozycyjne
Flagi to nazwane ustawienia („włącz to”, „ustaw taki limit”). Ale w wielu komendach są jeszcze argumenty pozycyjne: plik, ścieżka, nazwa encji, tekst zadania. Nie mają nazwy i są określane przez kolejność.
Po flag.Parse() możesz pobrać argumenty pozycyjne na trzy sposoby:
- flag.Args() zwróci []string ze wszystkimi argumentami pozycyjnymi
- flag.NArg() zwróci liczbę argumentów pozycyjnych
- flag.Arg(i) zwróci i-ty argument pozycyjny (od zera)
Zróbmy naszą komendę „todo” trochę bardziej żywą: niech nagłówek zadania będzie argumentem pozycyjnym, a priorytet — flagą. Czyli uruchomienie wygląda mniej więcej tak:
- todo -priority 3 "Buy milk"
Kod:
package main
import (
"flag"
"fmt"
)
func main() {
priority := flag.Int("priority", 1, "priority 1..5")
flag.Parse()
fmt.Println("args:", flag.Args()) // args: [...]
fmt.Println("priority:", *priority) // priority: ...
}
Sprawdzaj flag.NArg() przed flag.Arg(0)
Teraz dodajmy odczyt „nagłówka” jako pierwszego argumentu pozycyjnego, ale ostrożnie: najpierw sprawdzimy, czy on w ogóle istnieje.
package main
import (
"flag"
"fmt"
)
func main() {
flag.Parse()
if flag.NArg() < 1 {
fmt.Println("missing TITLE")
return
}
fmt.Println("title:", flag.Arg(0))
}
To bardzo ważny nawyk: nie czytać flag.Arg(0) w ciemno. Jeśli argumentów nie ma, dostaniesz pusty ciąg, a program może „jakby działać”, ale robić bezsensowne rzeczy. To gorsze niż jawny błąd, bo takie usterki żyją długo i karmią się twoją pewnością siebie.
5. Przydatne niuanse CLI
Flagi logiczne: flag.Bool
Flagi logiczne to osobna przyjemność CLI. W przeciwieństwie do stringów i liczb często służą jako „przełącznik trybu”: włącz/wyłącz coś. A dobry CLI zwykle pozwala pisać je skrótowo.
W flag.Bool jest praktyczny moment UX: flagę można podać jako -done=true, ale zazwyczaj można też po prostu -done, co oznacza true. To sprawia, że komendy są krótsze i przyjemniejsze.
Niech w naszym „todo” będzie flaga -done, która oznacza zadanie jako wykonane (tak, dziwnie jest tworzyć zadanie już wykonane — ale CLI, podobnie jak życie, nie musi być logiczne).
package main
import (
"flag"
"fmt"
)
func main() {
done := flag.Bool("done", false, "mark task as done")
flag.Parse()
fmt.Println("done:", *done) // done: true/false
}
Jeśli użytkownik nie podał -done, wartość pozostanie domyślna (false). Jeśli podał — stanie się true. I, co ważne, nie musisz ręcznie analizować ciągu "true"/"false" i zastanawiać się, co zrobić z "yes".
Terminator --: „dalej to już nie są flagi”
Jest jeden mały problem CLI, który pojawia się nagle. Wyobraź sobie, że argument pozycyjny sam zaczyna się od -. Na przykład chcesz przekazać tekst zadania -buy milk (może to taki styl) albo plik o nazwie -data.txt (tak, to się zdarza).
Bez dodatkowej zasady flag spróbuje zinterpretować to jako flagę. Aby wyraźnie powiedzieć „koniec, dalej nie ma flag, dalej są po prostu argumenty”, używa się terminatora --.
Z punktu widzenia użytkownika komenda wygląda tak:
- todo -priority 2 -- -buy milk
Kod się przy tym nie zmienia: nadal wywołujesz flag.Parse(), a potem czytasz flag.Args(). Po prostu -- pomaga parserowi poprawnie rozdzielić części.
Mini-przykład pokazujący sam fakt, że „po -- wszystko trafia do Args”:
package main
import (
"flag"
"fmt"
)
func main() {
flag.Parse()
fmt.Println(flag.Args()) // wszystko po -- będzie tutaj
}
Mini-przykład: składamy „konfigurację uruchomienia”
Teraz złożymy mały spójny fragment: flagi + argument pozycyjny i wypiszemy, co dokładnie program zrozumiał. To jeszcze nie jest „prawdziwa aplikacja todo”, ale już poprawna warstwa „interfejsu uruchomienia”.
Warunki są proste: priority i done to flagi, TITLE to argument pozycyjny.
package main
import (
"flag"
"fmt"
)
func main() {
priority := flag.Int("priority", 1, "priority 1..5")
done := flag.Bool("done", false, "mark as done")
flag.Parse()
title := ""
if flag.NArg() >= 1 {
title = flag.Arg(0)
}
fmt.Println("title:", title, "priority:", *priority, "done:", *done)
}
Tak, na razie nie walidujemy zakresu priorytetu i nie wypisujemy ładnego usage — to osobne umiejętności. Na tym etapie ważne jest doprowadzenie do automatyzmu mechaniki: zadeklaruj → Parse → odczytaj → sprawdź Args.
6. Typowe błędy
Błąd nr 1: zapomniano wywołać flag.Parse() i dziwią się, że „flagi nie działają”.
To najczęstsza historia. Program się uruchamia, nic się nie psuje, ale każdy -priority 5 wydaje się ignorowany. Powód jest prosty: dopóki nie wywołasz Parse(), w zmiennych masz tylko wartości domyślne.
Błąd nr 2: próbują używać title zamiast *title i dostają dziwne błędy kompilacji.
flag.String zwraca *string, a nie string. Zrobiono tak po to, aby Parse() mógł zapisać wynik w przygotowanym wcześniej miejscu. Leczenie jest proste: po Parse() zawsze dereferencjuj *title, *limit, *done.
Błąd nr 3: deklarują flagi po flag.Parse() i oczekują, że zostaną rozpoznane.
Parse() analizuje argumenty na podstawie listy już zadeklarowanych flag. Jeśli zadeklarowałeś flagę później, Parse() jej nie „zna”, bo parsowanie już się odbyło. Zasada jest bardzo sztywna: najpierw deklaracje, potem Parse().
Błąd nr 4: czytają argumenty pozycyjne przed Parse() i dostają „pusto/coś nie tak”.
Przed Parse() pakiet flag jeszcze nie oddzielił flag od argumentów pozycyjnych. Dlatego flag.Args() przed parsowaniem będzie albo puste, albo zawierało niespodziewane rzeczy. Prawidłowa kolejność: flag.Parse(), a dopiero potem flag.Args()/flag.NArg()/flag.Arg(i).
Błąd nr 5: czytają flag.Arg(0) bez sprawdzania flag.NArg() i dostają „cichą” błędną logikę.
W przeciwieństwie do odczytu os.Args[i], tutaj zwykle nie dostaniesz paniki, ale dostaniesz pusty ciąg, a program może później utworzyć zadanie bez tytułu i udawać, że tak miało być. Przed odczytem argumentów pozycyjnych warto sprawdzać ich liczbę, nawet jeśli bardzo chcesz „zaoszczędzić dwie linie”.
GO TO FULL VERSION