Go: Na prática: Desenvolvimento de ferramentas de CLI
Última atualização: 2026-08-26
A criação de ferramentas de linha de comando (CLI) é um dos pontos fortes do Go — elas são compiladas em um único binário, são multiplataforma e não têm nenhuma dependência. Escrever CLIs em Go tornou-se o padrão na era do cloud-native.
De docker a kubectl e gh, quase todas as principais ferramentas de CLI são escritas em Go. Nesta aula, você vai dominar todas as habilidades essenciais do desenvolvimento de CLI e criar um gerenciador de senhas completo.
1. Você aprenderá
- O pacote
flaganalisa os argumentos da linha de comando os.ArgsAcesso a argumentos em formato bruto- Fluxos padrão
os.Stdin/os.Stdout - A biblioteca Cobra e o padrão de subcomandos
- Integração da CLI com a E/S de arquivos
- Projeto: Gerenciador de senhas via CLI (Gerar/Armazenar/Recuperar/Exportar senhas)
2. Uma história real de um engenheiro de operações
(1) Problema: Ter que gerar manualmente uma senha todas as vezes
Bob é um engenheiro de operações que frequentemente precisa gerar senhas complexas para diversos serviços:
“Hoje eu gerar uma senha para o PostgreSQL, amanhã para o Redis e depois de amanhã para uma conta de administrador. Toda vez, tenho que abrir um site gerador de senhas, copiar e colar no 1Password — cinco vezes por dia. É muito chato. Além disso, sites diferentes têm requisitos diferentes: 12 caracteres com caracteres especiais, 16 caracteres sem caracteres ambíguos...”
Bob está procurando uma ferramenta de CLI como esta:
# Desired CLI
$ passgen generate --length 16 --special
Generated: Kd9#mP2$xL7qR!vB
$ passgen save mydb -p Kd9#mP2$xL7qR!vB
Saved: mydb
$ passgen list
mydb (created: 2026-07-08)
admin (created: 2026-07-05)
api-key (created: 2026-07-01)
$ passgen get mydb
mydb: Kd9#mP2$xL7qR!vB
Mas não havia nenhuma ferramenta no mercado que atendesse plenamente às suas necessidades. Por isso, ele decidiu criar uma ele mesmo usando Go.
(2) Solução em Go: pacote de sinalizadores + E/S de arquivos
// passgen.go — Version 1: Basic password generation
package main
import (
"crypto/rand"
"flag"
"fmt"
"math/big"
)
var (
length = flag.Int("length", 12, "password length")
special = flag.Bool("special", false, "include special characters")
count = flag.Int("count", 1, "number of passwords to generate")
)
const (
lowerChars = "abcdefghijklmnopqrstuvwxyz"
upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
digitChars = "0123456789"
specialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
)
func generatePassword(length int, includeSpecial bool) string {
charset := lowerChars + upperChars + digitChars
if includeSpecial {
charset += specialChars
}
pwd := make([]byte, length)
for i := range pwd {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
pwd[i] = charset[n.Int64()]
}
return string(pwd)
}
func main() {
flag.Parse()
for i := 0; i < *count; i++ {
fmt.Println(generatePassword(*length, *special))
}
}
$ go run passgen.go --length 16 --special
Kd9#mP2$xL7qR!vB
$ go run passgen.go --length 8
aB3xK9mP
(3) Desempenho: Modo manual x CLI
| Método | Velocidade | Repetibilidade | Integração com CI/CD | Segurança |
|---|---|---|---|---|
| Geração de sites | 30s/vez | ❌ | ❌ | ❌ (Risco à privacidade) |
| Criar manualmente | 60 segundos por instância | ✅ Pode ser duplicado | ❌ | ✅ |
| Go CLI | 0,5 s por operação | ✅ Programável | ✅ Integrável | ✅ Executa-se localmente |
go build, basta copiá-lo para qualquer servidor Linux, Mac ou Windows para executá-lo. Essa é a principal vantagem de usar o Go para aplicativos de CLI.
3. Pacote flag: Análise de argumentos da linha de comando
(1) Noções básicas sobre bandeiras
package main
import (
"flag"
"fmt"
)
func main() {
// Register parameters (returns pointers)
name := flag.String("name", "World", "name to greet")
age := flag.Int("age", 0, "age")
verbose := flag.Bool("verbose", false, "verbose output")
// Parse (must be called before reading values)
flag.Parse()
if *verbose {
fmt.Printf("name=%s, age=%d\n", *name, *age)
}
fmt.Printf("Hello, %s!\n", *name)
}
$ go run main.go --name Alice --age 28 --verbose
name=Alice, age=28
Hello, Alice!
$ go run main.go
Hello, World!
(2) Referência rápida para o tipo flag
| Função | Tipo | Exemplo |
|---|---|---|
flag.String(name, default, usage) |
*string |
--name Alice |
flag.Int(name, default, usage) |
*int |
--port 8080 |
flag.Float64(name, default, usage) |
*float64 |
--ratio 0.5 |
flag.Bool(name, default, usage) |
*bool |
--verbose |
flag.Duration(name, default, usage) |
*time.Duration |
--timeout 5s |
(3) ▶ Exemplo: parâmetro com sinalizador curto + parâmetro sem sinalizador
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Custom Usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: mytool [options] <file path>\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
}
// Register parameters
output := flag.String("o", "output.txt", "output file path")
limit := flag.Int("n", 0, "lines to process (0=all)")
debug := flag.Bool("debug", false, "enable debug mode")
// Parse
flag.Parse()
// Access non-flag arguments (remaining args)
args := flag.Args()
if len(args) < 1 {
flag.Usage()
os.Exit(1)
}
filePath := args[0]
fmt.Printf("Processing file: %s\n", filePath)
fmt.Printf("Output: %s\n", *output)
fmt.Printf("Lines: %d\n", *limit)
fmt.Printf("Debug: %v\n", *debug)
// Supports both -o and --o forms
// Supports both -o=file and -o file syntax
}
$ go run main.go -o result.txt -n 100 data.csv
Processing file: data.csv
Output: result.txt
Lines: 100
Debug: false
4. os.Args e os.Stdin/Stdout
(1) os.Args: Argumentos em formato bruto
package main
import (
"fmt"
"os"
)
func main() {
fmt.Printf("Program: %s\n", os.Args[0])
fmt.Printf("Arg count: %d\n", len(os.Args)-1)
for i, arg := range os.Args[1:] {
fmt.Printf("Arg %d: %s\n", i+1, arg)
}
}
$ go run main.go hello world --debug
Program: /tmp/main
Arg count: 3
Arg 1: hello
Arg 2: world
Arg 3: --debug
(2) ▶ Exemplo: Atribuição da variável flag (IntVar / StringVar)
package main
import (
"flag"
"fmt"
)
func main() {
// flag.Int returns a pointer vs flag.IntVar binds to an existing variable
var (
name string
port int
debug bool
)
flag.StringVar(&name, "name", "World", "name")
flag.IntVar(&port, "port", 8080, "port")
flag.BoolVar(&debug, "debug", false, "debug mode")
flag.Parse()
fmt.Printf("name=%s, port=%d, debug=%v\n", name, port, debug)
}
flag.IntVar(&dest, ...) se vincula a uma variável existente, enquanto flag.Int(...) retorna um ponteiro. Ambos são totalmente equivalentes — escolha o estilo que preferir. IntVar é mais adequado para declarar variáveis em um local centralizado.
(3) os.Stdin: Entrada por pipe
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
stat, _ := os.Stdin.Stat()
hasStdin := (stat.Mode() & os.ModeCharDevice) == 0
if hasStdin {
// Read from pipe (echo "hello" | go run main.go)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := strings.ToUpper(scanner.Text())
fmt.Println(line)
}
} else {
// Read from arguments (go run main.go hello)
if len(os.Args) > 1 {
fmt.Println(strings.ToUpper(strings.Join(os.Args[1:], " ")))
} else {
fmt.Println("Please provide input (pipe or arguments)")
}
}
}
$ echo "hello world" | go run main.go
HELLO WORLD
$ go run main.go hello world
HELLO WORLD
(4) ▶ Exemplo: Entrada interativa (fmt.Scanf)
package main
import "fmt"
func main() {
var name string
var age int
fmt.Print("Enter name: ")
fmt.Scanf("%s", &name)
fmt.Print("Enter age: ")
fmt.Scanf("%d", &age)
fmt.Printf("Hello %s, you are %d years old\n", name, age)
}
(5) ▶ Exemplo: Redirecionando os.Stdout
package main
import (
"fmt"
"os"
)
func main() {
// Write directly to stdout
fmt.Fprintln(os.Stdout, "Standard output")
fmt.Fprintln(os.Stderr, "Error output")
// Redirect to file
file, _ := os.Create("output.log")
defer file.Close()
// fmt.Fprintln can write to any io.Writer
fmt.Fprintln(file, "This line was written to the file")
fmt.Fprintln(file, "Second line")
}
(6) Comparação das fontes de entrada
| Método de entrada | Exemplo de comando | Caso de uso |
|---|---|---|
| Argumentos da linha de comando | mytool --name Alice |
Opções de configuração, nome do arquivo |
| canal stdin | cat data | mytool |
Grandes volumes de dados, processamento em cadeia |
| Entrada interativa | mytool aguarda a entrada do usuário |
Entrada que requer orientação |
| Variável de ambiente | MYTOOL_DEBUG=1 mytool |
Configuração sensível |
5. Biblioteca cobra: modo de subcomandos
(1) Introdução ao Cobra
O cobra é o framework de CLI mais popular para Go (criado pelo criador do Kubernetes) e oferece suporte a subcomandos, mensagens de ajuda e autocompletar.
# Install cobra-cli
$ go install github.com/spf13/cobra-cli@latest
# Initialize project
$ cobra-cli init passgen
$ cobra-cli add generate
$ cobra-cli add save
$ cobra-cli add list
(2) ▶ Exemplo: Estrutura do subcomando “cobra”
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "passgen",
Short: "Password Manager",
Long: `A CLI tool for generating, storing, and managing passwords.`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// cmd/generate.go
package cmd
import (
"fmt"
"crypto/rand"
"math/big"
"github.com/spf13/cobra"
)
var (
genLength int
genSpecial bool
genCount int
)
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a random password",
Long: `Generate a random password with specified length and complexity.`,
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < genCount; i++ {
fmt.Println(generatePassword(genLength, genSpecial))
}
},
}
func init() {
rootCmd.AddCommand(generateCmd)
generateCmd.Flags().IntVarP(&genLength, "length", "l", 12, "password length")
generateCmd.Flags().BoolVarP(&genSpecial, "special", "s", false, "include special characters")
generateCmd.Flags().IntVarP(&genCount, "count", "c", 1, "number to generate")
}
func generatePassword(length int, special bool) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if special {
charset += "!@#$%^&*()-_=+[]{}|;:,.<>?"
}
pwd := make([]byte, length)
for i := range pwd {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
pwd[i] = charset[n.Int64()]
}
return string(pwd)
}
// main.go
package main
import "passgen/cmd"
func main() {
cmd.Execute()
}
$ go build -o passgen
$ ./passgen generate --length 16 --special
Kd9#mP2$xL7qR!vB
$ ./passgen generate -l 8
aB3xK9mP
(3) Flag x Cobra
| Dimensão | pacote de bandeiras | cobra |
|---|---|---|
| Integrado | ✅ Biblioteca padrão | ❌ De terceiros |
| Subcomando | ❌ Não compatível | ✅ Compatível nativamente |
| Ajuda sobre carros | ✅ Noções básicas | ✅ Estética |
| Autocompletar | ❌ | ✅ bash/zsh/fish |
| Casos de uso | Scripts simples | Ferramentas complexas com vários comandos |
6. Exemplo completo: CLI do Password Manager
Reúna todo esse conhecimento em um gerenciador de senhas completo:
// main.go
package main
import (
"bufio"
"crypto/rand"
"encoding/json"
"flag"
"fmt"
"math/big"
"os"
"strings"
"time"
)
// ---------- Data Structures ----------
type PasswordEntry struct {
Name string `json:"name"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
type PasswordStore struct {
entries map[string]PasswordEntry
filePath string
}
const defaultStoreFile = "passwords.json"
// ---------- Storage Layer ----------
func NewPasswordStore(filePath string) *PasswordStore {
store := &PasswordStore{
entries: make(map[string]PasswordEntry),
filePath: filePath,
}
store.load()
return store
}
func (s *PasswordStore) load() {
data, err := os.ReadFile(s.filePath)
if err != nil {
return
}
json.Unmarshal(data, &s.entries)
}
func (s *PasswordStore) save() error {
data, err := json.MarshalIndent(s.entries, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.filePath, data, 0600)
}
func (s *PasswordStore) Add(name, password string) {
now := time.Now()
if existing, ok := s.entries[name]; ok {
existing.Password = password
existing.UpdatedAt = now
s.entries[name] = existing
} else {
s.entries[name] = PasswordEntry{
Name: name,
Password: password,
CreatedAt: now,
}
}
s.save()
}
func (s *PasswordStore) Get(name string) (PasswordEntry, bool) {
entry, ok := s.entries[name]
return entry, ok
}
func (s *PasswordStore) Delete(name string) bool {
_, ok := s.entries[name]
if ok {
Excluir(s.entries, name)
s.save()
}
return ok
}
func (s *PasswordStore) List() []PasswordEntry {
result := make([]PasswordEntry, 0, len(s.entries))
for _, entry := range s.entries {
result = append(result, entry)
}
return result
}
// ---------- Password Generation ----------
const (
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
digits = "0123456789"
specials = "!@#$%^&*()-_=+[]{}|;:,.<>?"
noAmbigu = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
)
func generatePassword(length int, useSpecial bool) (string, error) {
if length < 4 {
return "", fmt.Errorf("password length must be at least 4")
}
charset := letters + digits
if useSpecial {
charset += specials
}
pwd := make([]byte, length)
for i := range pwd {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
pwd[i] = charset[n.Int64()]
}
return string(pwd), nil
}
// ---------- CLI Commands ----------
func cmdGenerate(args []string) {
cmd := flag.NewFlagSet("generate", flag.ExitOnError)
length := cmd.Int("length", 12, "password length")
special := cmd.Bool("special", false, "include special characters")
count := cmd.Int("count", 1, "Valor to generate")
_ = cmd.Bool("no-ambiguous", false, "exclude ambiguous characters (0OIl1)")
cmd.Parse(args)
for i := 0; i < *count; i++ {
pwd, err := generatePassword(*length, *special)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(pwd)
}
}
func cmdSave(args []string, store *PasswordStore) {
cmd := flag.NewFlagSet("save", flag.ExitOnError)
password := cmd.String("password", "", "password (leave empty to auto-generate)")
length := cmd.Int("length", 16, "auto-generated password length")
cmd.Parse(args)
name := cmd.Arg(0)
if name == "" {
fmt.Fprintln(os.Stderr, "Usage: passgen save <name> [--password <pwd>]")
os.Exit(1)
}
pwd := *password
if pwd == "" {
var err error
pwd, err = generatePassword(*length, true)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
store.Add(name, pwd)
fmt.Printf("Saved: %s = %s\n", name, pwd)
}
func cmdGet(args []string, store *PasswordStore) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Usage: passgen get <name>")
os.Exit(1)
}
entry, ok := store.Get(args[0])
if !ok {
fmt.Fprintf(os.Stderr, "Not found: %s\n", args[0])
os.Exit(1)
}
fmt.Printf("%s: %s\n", entry.Name, entry.Password)
}
func cmdList(args []string, store *PasswordStore) {
entries := store.List()
if len(entries) == 0 {
fmt.Println("(no saved passwords)")
return
}
fmt.Printf("%-20s %-20s %s\n", "Name", "Password", "Created")
fmt.Println(strings.Repeat("-", 60))
for _, e := range entries {
pwd := e.Password
if len(pwd) > 16 {
pwd = pwd[:16] + "..."
}
fmt.Printf("%-20s %-20s %s\n",
e.Name, pwd, e.CreatedAt.Format("2006-01-02"))
}
}
func cmdDelete(args []string, store *PasswordStore) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Usage: passgen Excluir <name>")
os.Exit(1)
}
if store.Delete(args[0]) {
fmt.Printf("Deleted: %s\n", args[0])
} else {
fmt.Fprintf(os.Stderr, "Not found: %s\n", args[0])
}
}
func cmdExport(args []string, store *PasswordStore) {
output := "passwords_export.json"
if len(args) > 0 {
output = args[0]
}
data, _ := json.MarshalIndent(store.List(), "", " ")
os.WriteFile(output, data, 0600)
fmt.Printf("Exported to: %s\n", output)
}
// ---------- Main Function ----------
func printUsage() {
fmt.Println(`Password Manager CLI
Usage:
passgen <command> [options]
Commands:
generate Generate a random password
save Save a password
get Retrieve a password
list List all passwords
Excluir Delete a password
export Export passwords to a file
Options:
--store Specify storage file path (default: passwords.json)
Examples:
passgen generate --length 16 --special
passgen save mydb --length 20
passgen get mydb
passgen list
passgen Excluir mydb`)
}
func main() {
// Global --store parameter
storeFile := defaultStoreFile
if len(os.Args) > 1 {
for i := 1; i < len(os.Args); i++ {
if os.Args[i] == "--store" && i+1 < len(os.Args) {
storeFile = os.Args[i+1]
// Remove the store parameter
os.Args = append(os.Args[:i], os.Args[i+2:]...)
break
}
}
}
store := NewPasswordStore(storeFile)
if len(os.Args) < 2 {
printUsage()
return
}
command := os.Args[1]
args := os.Args[2:]
switch command {
case "generate":
cmdGenerate(args)
case "save":
cmdSave(args, store)
case "get":
cmdGet(args, store)
case "list":
cmdList(args, store)
case "Excluir":
cmdDelete(args, store)
case "export":
cmdExport(args, store)
case "help", "--help", "-h":
printUsage()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
fmt.Fprintf(os.Stderr, "Run 'passgen help' to see available commands\n")
os.Exit(1)
}
}
Uso pretendido:
# Generate password
$ go run main.go generate --length 16 --special
Kd9#mP2$xL7qR!vB
# Save password (auto-generated)
$ go run main.go save mydb --length 20
Saved: mydb = aB3xK9mP$L7qR!vD2nFg
# Save password (manually specified)
$ go run main.go save admin --password "MyStr0ng!Pass"
Saved: admin = MyStr0ng!Pass
# Retrieve password
$ go run main.go get mydb
mydb: aB3xK9mP$L7qR!vD2nFg
# List
$ go run main.go list
Name Password Created
------------------------------------------------------------
mydb aB3xK9mP$L7qR!v... 2026-07-08
admin MyStr0ng!Pass 2026-07-08
# Export
$ go run main.go export
Exported to: passwords_export.json
# Specify storage file
$ go run main.go --store vault.json generate -l 10
flowchart TD
A[passgen command] --> B{switch command}
B -->|generate| C[flag.NewFlagSet parse<br/>--length --special --count]
B -->|save| D[Parse name + --password<br/>Auto-generate or specify password]
B -->|get| E[Look up by name from map]
B -->|list| F[Iterate map, output table]
B -->|delete| G[delete(map, name)]
B -->|export| H[json.Marshal + WriteFile]
C --> I[rand.Int secure random]
D --> J[store.Add → json → save]
E --> K[store.Get → output]
F --> L[store.List → format]
G --> M[store.Delete → json → save]
H --> N[store.List → json.Marshal]
flag.NewFlagSet é usado para subcomandos, e cada subcomando possui seu próprio conjunto de sinalizadores. flag.FlagSet.Parse analisa apenas seus próprios argumentos e não modifica os.Args. O comando principal deve atribuir valores manualmente usando os.Args.
❓ Perguntas Frequentes
P: Como faço para escolher entre
flagecobra? R: Scripts simples (1 comando, alguns parâmetros) →flag; ferramentas com vários comandos (estilo git:tool <command> [options]) →cobra. Oflagfaz parte da biblioteca padrão, enquanto ocobraoferece mais recursos, mas requer a adição de uma dependência.
P: Como faço para analisar subcomandos? R: Método manual: use
os.Args[1]para identificar o comando eflag.NewFlagSet("sub", ...)para analisar os argumentos do subcomando. Método Cobra: userootCmd.AddCommand(subCmd), e a estrutura cuida disso automaticamente.
P: Como faço para percorrer
os.Args? R:os.Args[0]é o caminho do programa, eos.Args[1:]são os argumentos. Você pode percorrê-los usandoos.Args[i]ourange os.Args[1:].
P: Como a E/S de arquivos é utilizada na CLI? R: Existem três modos: (1) Salvar em um caminho fixo (
~/.config/passgen.json); (2) Especificar um caminho usando o parâmetro--store; (3) Variáveis de ambiente (comoPASSGEN_STORE).
P: Como faço para publicar uma CLI no GitHub? R: (1) Compile com
go build -o mytool; (2) Envie para o GitHub Releases; (3) Instale usandogo install github.com/user/mytool@latest. Os binários estáticos do Go tornam a publicação extremamente simples.
P: Como o gerador de senhas garante a segurança? R: Use
crypto/randem vez demath/rand—crypto/randutiliza o CSPRNG do sistema operacional (como/dev/urandom), que é imprevisível.math/randé pseudoaleatório e não é adequado para fins criptográficos.
P: Como uma ferramenta de CLI pode lidar com entradas simples do usuário? R: Use
bufio.NewScanner(os.Stdin)para ler linha por linha oufmt.Scanfpara ler entradas formatadas. Para ferramentas de produção, recomendamos a bibliotecagopass(que não exibe senhas na tela).
P: Como as mensagens de erro devem ser tratadas na CLI? R: As mensagens de erro devem ser gravadas em
os.Stderr(em vez deos.Stdout) para quemytool 2>/dev/nullseja executado silenciosamente. Usefmt.Fprintln(os.Stderr, "error").
📖 Resumo
- O pacote
flaganalisa os argumentos da linha de comando:flag.String/flag.Int/flag.Bool os.Argsacessa os argumentos em formato bruto, permitindo uma análise manual flexívelos.Stdin/os.Stdout/os.Stderrfluxos de E/S padrãoflag.NewFlagSetimplementa uma interface de linha de comando (CLI) no estilo de subcomandos- A biblioteca cobra oferece uma estrutura completa para subcomandos
crypto/randgera números aleatórios criptograficamente seguros- Persistência de dados para ferramentas de CLI: arquivos JSON + cache de mapa na memória
go buildé compilado em um único binário para distribuição multiplataforma
📝 Exercícios
-
Problema básico (Dificuldade ⭐): Use o pacote
flagpara escrever um clone dowc: omywcsuporta três opções —--lines,--wordse--chars—, lê a entrada do stdin ou de um arquivo e exibe as estatísticas. -
Exercício avançado (Dificuldade ⭐⭐): Adicione suporte à variável de ambiente
--storeao gerenciador de senhas nesta lição: se a variável de ambientePASSGEN_STOREestiver definida, use-a como caminho de armazenamento em primeiro lugar; caso contrário, use o parâmetro--store; e, por fim, use o caminho padrão. -
Desafio (Dificuldade: ⭐⭐⭐): Adicione um comando
statsao gerenciador de senhas: conte o número total de senhas, a senha mais longa e a mais curta, os caracteres mais usados e as estatísticas agrupadas por data de criação (hoje/esta semana/este mês/antes). Você deve usarsort.Slice+strings.Count+time.Time.