CodeGym /Kursy /Go SELF /flag — String/Int/Bool, Parse, Args

flag — String/Int/Bool, Parse, Args

Go SELF
Poziom 48, Lekcja 1
Dostępny

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
flag.String
string
*string
flag.Int
int
*int
flag.Bool
bool
*bool

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”.

1
Zadanie
Go SELF, poziom 48, lekcja 1
Niedostępne
Głośne przywitanie
Głośne przywitanie
1
Zadanie
Go SELF, poziom 48, lekcja 1
Niedostępne
Wiadomość operatora
Wiadomość operatora
1
Zadanie
Go SELF, poziom 48, lekcja 1
Niedostępne
Przegląd argumentów
Przegląd argumentów
1
Zadanie
Go SELF, poziom 48, lekcja 1
Niedostępne
Parametry zgłoszenia
Parametry zgłoszenia
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION