├── 1.1 ├── go.mod └── main.go ├── 1.2 ├── go.mod ├── hello.go └── hello_test.go ├── 10.1 ├── go.mod ├── log └── main.go ├── 10.2 ├── go.mod └── main.go ├── 10.3 ├── go.mod └── main.go ├── 10.4 ├── go.mod ├── go.sum ├── pipeline.go └── pipeline_test.go ├── 10.5 ├── cmd │ └── cat │ │ └── main.go ├── go.mod ├── go.sum ├── pipeline.go ├── pipeline_test.go └── testdata │ └── hello.txt ├── 11.1 ├── cmd │ ├── load │ │ └── main.go │ └── store │ │ └── main.go ├── go.mod ├── go.sum ├── store.go └── store_test.go ├── 11.2 ├── battery.go ├── battery_test.go ├── go.mod ├── go.sum └── testdata │ └── battery.json ├── 11.3 ├── go.mod ├── go.sum ├── prom.go ├── prom_test.go └── testdata │ └── config.yaml ├── 12.1 ├── go.mod └── main.go ├── 12.2 ├── go.mod └── main.go ├── 12.3 ├── cmd │ └── weather │ │ └── main.go ├── go.mod ├── go.sum ├── testdata │ ├── weather.json │ └── weather_invalid.json ├── weather.go └── weather_test.go ├── 12.4 ├── cmd │ └── weather │ │ └── main.go ├── go.mod ├── go.sum ├── testdata │ ├── weather.json │ └── weather_invalid.json ├── weather.go └── weather_test.go ├── 12.5 ├── cmd │ └── weather │ │ └── main.go ├── go.mod ├── go.sum ├── testdata │ ├── weather.json │ └── weather_invalid.json ├── weather.go └── weather_test.go ├── 2.1 ├── cmd │ └── hello │ │ └── main.go ├── go.mod ├── hello.go └── hello_test.go ├── 2.2 ├── cmd │ └── hello │ │ └── main.go ├── go.mod ├── hello.go └── hello_test.go ├── 3.1 ├── go.mod └── main.go ├── 3.2 ├── cmd │ └── count │ │ └── main.go ├── count.go ├── count_test.go └── go.mod ├── 3.3 ├── cmd │ └── count │ │ └── main.go ├── count.go ├── count_test.go └── go.mod ├── 4.1 ├── cmd │ └── count │ │ └── main.go ├── count.go ├── count_test.go ├── go.mod └── testdata │ └── three_lines.txt ├── 5.1 ├── cmd │ ├── lines │ │ └── main.go │ └── words │ │ └── main.go ├── count.go ├── count_test.go ├── go.mod └── testdata │ └── three_lines.txt ├── 5.2 ├── go.mod └── main.go ├── 5.3 ├── go.mod └── main.go ├── 5.4 ├── cmd │ └── count │ │ └── main.go ├── count.go ├── count_test.go ├── go.mod └── testdata │ └── three_lines.txt ├── 6.1 ├── go.mod ├── go.sum ├── writer.go └── writer_test.go ├── 7.1 ├── findgo │ ├── file.go │ ├── subfolder │ │ └── subfolder.go │ └── subfolder2 │ │ ├── another.go │ │ └── file.go ├── go.mod └── main.go ├── 7.2 ├── findgo │ ├── file.go │ ├── subfolder │ │ └── subfolder.go │ └── subfolder2 │ │ ├── another.go │ │ └── file.go ├── go.mod └── main.go ├── 7.3 ├── findgo │ ├── file.go │ ├── subfolder │ │ └── subfolder.go │ └── subfolder2 │ │ ├── another.go │ │ └── file.go ├── go.mod └── main.go ├── 7.4 ├── cmd │ └── findgo │ │ └── main.go ├── findgo.go ├── findgo_test.go ├── go.mod └── testdata │ └── findgo │ ├── file.go │ ├── subfolder │ └── subfolder.go │ └── subfolder2 │ ├── another.go │ └── file.go ├── 7.5 ├── cmd │ └── findgo │ │ └── main.go ├── findgo.go ├── findgo_test.go ├── go.mod └── testdata │ ├── findgo.zip │ └── findgo │ ├── file.go │ ├── subfolder │ └── subfolder.go │ └── subfolder2 │ ├── another.go │ └── file.go ├── 8.1 ├── go.mod └── main.go ├── 8.2 ├── battery.go ├── battery_integration_test.go ├── battery_test.go ├── cmd │ └── battery │ │ └── main.go ├── go.mod ├── go.sum └── testdata │ └── pmset.txt ├── 9.1 ├── cmd │ └── shell │ │ └── main.go ├── go.mod ├── go.sum ├── shell.go └── shell_test.go ├── 9.2 ├── cmd │ └── shell │ │ └── main.go ├── go.mod ├── go.sum ├── shell.go └── shell_test.go ├── LICENSE ├── README.md ├── img └── cover.png └── run_tests.sh /1.1/go.mod: -------------------------------------------------------------------------------- 1 | module hello 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /1.1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Hello, world") 9 | } 10 | -------------------------------------------------------------------------------- /1.2/go.mod: -------------------------------------------------------------------------------- 1 | module hello 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /1.2/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | func PrintTo(w io.Writer) { 9 | fmt.Fprint(w, "Hello, world") 10 | } 11 | -------------------------------------------------------------------------------- /1.2/hello_test.go: -------------------------------------------------------------------------------- 1 | package hello_test 2 | 3 | import ( 4 | "bytes" 5 | "hello" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestPrintsHelloMessageToWriter(t *testing.T) { 11 | t.Parallel() 12 | fakeTerminal := &bytes.Buffer{} 13 | hello.PrintTo(io.Writer(fakeTerminal)) 14 | want := "Hello, world" 15 | got := fakeTerminal.String() 16 | if want != got { 17 | t.Errorf("want %q, got %q", want, got) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /10.1/go.mod: -------------------------------------------------------------------------------- 1 | module visitors 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /10.1/log: -------------------------------------------------------------------------------- 1 | 203.0.113.17 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 2028 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 2 | 203.0.113.1 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 162544 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 3 | 203.0.113.1 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 9419 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 4 | 203.0.113.10 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 2058 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 5 | 203.0.113.10 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 343743 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 6 | 203.0.113.19 - - [30/Jun/2019:17:06:16 +0000] "GET / HTTP/1.1" 200 1150 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 7 | 203.0.113.250 - - [30/Jun/2019:17:06:16 +0000] "GET / HTTP/1.1" 200 2946 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36" 8 | 203.0.113.250 - - [30/Jun/2019:17:06:17 +0000] "GET / HTTP/1.1" 200 13278 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 9 | 203.0.113.14 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 29474 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 10 | 203.0.113.16 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 29349 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 11 | 203.0.113.19 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 48271 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 12 | 203.0.113.2 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 1380 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 13 | 203.0.113.250 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 2028 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 14 | 203.0.113.19 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 91819 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 15 | 203.0.113.250 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 305667 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 16 | 203.0.113.250 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 13194 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 17 | 203.0.113.10 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 12935 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 18 | 203.0.113.2 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 14598 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 19 | 203.0.113.2 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 22458 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 20 | 203.0.113.2 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 15737 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 21 | 203.0.113.253 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 404 17679 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 22 | 203.0.113.86 - - [30/Jun/2019:17:06:23 +0000] "GET / HTTP/1.1" 200 5995 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 23 | 203.0.113.20 - - [30/Jun/2019:17:06:23 +0000] "GET / HTTP/1.1" 200 8809 "-" "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-J415FN Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.2 Chrome/67.0.3396.87 Mobile Safari/537.36" 24 | 203.0.113.86 - - [30/Jun/2019:17:06:24 +0000] "GET / HTTP/1.1" 200 162544 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1" 25 | 203.0.113.9 - - [30/Jun/2019:17:06:24 +0000] "GET / HTTP/1.1" 200 - "https://example.com/ "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-J415FN Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.2 Chrome/67.0.3396.87 Mobile Safari/537.36" -------------------------------------------------------------------------------- /10.1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | f, err := os.Open("log") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | defer f.Close() 18 | scanner := bufio.NewScanner(f) 19 | uniques := map[string]int{} 20 | for scanner.Scan() { 21 | fields := strings.Fields(scanner.Text()) 22 | if len(fields) > 0 { 23 | uniques[fields[0]]++ 24 | } 25 | } 26 | type freq struct { 27 | addr string 28 | count int 29 | } 30 | freqs := make([]freq, 0, len(uniques)) 31 | for addr, count := range uniques { 32 | freqs = append(freqs, freq{addr, count}) 33 | } 34 | sort.Slice(freqs, func(i, j int) bool { 35 | return freqs[i].count > freqs[j].count 36 | }) 37 | fmt.Printf("%-16s%s\n", "Address", "Requests") 38 | for i, freq := range freqs { 39 | if i > 9 { 40 | break 41 | } 42 | fmt.Printf("%-16s%d\n", freq.addr, freq.count) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /10.2/go.mod: -------------------------------------------------------------------------------- 1 | module writer 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /10.2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | f, err := os.Create("output.dat") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | defer f.Close() 15 | err = write(f) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | 21 | func write(w io.Writer) error { 22 | metadata := []byte{1, 2, 3} 23 | _, err := w.Write(metadata) 24 | if err != nil { 25 | return err 26 | } 27 | _, err = w.Write(metadata) 28 | if err != nil { 29 | return err 30 | } 31 | _, err = w.Write(metadata) 32 | if err != nil { 33 | return err 34 | } 35 | _, err = w.Write(metadata) 36 | if err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /10.3/go.mod: -------------------------------------------------------------------------------- 1 | module safewriter 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /10.3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | f, err := os.Create("output.dat") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | defer f.Close() 15 | err = write(f) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | 21 | type safeWriter struct { 22 | w io.Writer 23 | Error error 24 | } 25 | 26 | func (sw *safeWriter) Write(data []byte) { 27 | if sw.Error != nil { 28 | return 29 | } 30 | _, err := sw.w.Write(data) 31 | if err != nil { 32 | sw.Error = err 33 | } 34 | } 35 | 36 | func write(w io.Writer) error { 37 | metadata := []byte{1, 2, 3} 38 | sw := safeWriter{w: w} 39 | sw.Write(metadata) 40 | sw.Write(metadata) 41 | sw.Write(metadata) 42 | sw.Write(metadata) 43 | return sw.Error 44 | } 45 | -------------------------------------------------------------------------------- /10.4/go.mod: -------------------------------------------------------------------------------- 1 | module pipeline 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /10.4/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /10.4/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | ) 7 | 8 | type Pipeline struct { 9 | Reader io.Reader 10 | Output io.Writer 11 | Error error 12 | } 13 | 14 | func FromString(s string) *Pipeline { 15 | return &Pipeline{ 16 | Reader: strings.NewReader(s), 17 | } 18 | } 19 | 20 | func (p *Pipeline) Stdout() { 21 | io.Copy(p.Output, p.Reader) 22 | } 23 | -------------------------------------------------------------------------------- /10.4/pipeline_test.go: -------------------------------------------------------------------------------- 1 | package pipeline_test 2 | 3 | import ( 4 | "bytes" 5 | "pipeline" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestStdout(t *testing.T) { 12 | t.Parallel() 13 | want := "Hello, world\n" 14 | p := pipeline.FromString(want) 15 | buf := &bytes.Buffer{} 16 | p.Output = buf 17 | p.Stdout() 18 | if p.Error != nil { 19 | t.Fatal(p.Error) 20 | } 21 | got := buf.String() 22 | if !cmp.Equal(want, got) { 23 | t.Errorf("want %q, got %q", want, got) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /10.5/cmd/cat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "pipeline" 4 | 5 | func main() { 6 | pipeline.FromFile("testdata/hello.txt").Stdout() 7 | } 8 | -------------------------------------------------------------------------------- /10.5/go.mod: -------------------------------------------------------------------------------- 1 | module pipeline 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /10.5/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /10.5/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type Pipeline struct { 13 | Reader io.Reader 14 | Output io.Writer 15 | Error error 16 | } 17 | 18 | func New() *Pipeline { 19 | return &Pipeline{ 20 | Output: os.Stdout, 21 | } 22 | } 23 | 24 | func FromString(s string) *Pipeline { 25 | p := New() 26 | p.Reader = strings.NewReader(s) 27 | return p 28 | } 29 | 30 | func FromFile(pathname string) *Pipeline { 31 | f, err := os.Open(pathname) 32 | if err != nil { 33 | return &Pipeline{Error: err} 34 | } 35 | p := New() 36 | p.Reader = f 37 | return p 38 | } 39 | 40 | func (p *Pipeline) Column(col int) *Pipeline { 41 | if p.Error != nil { 42 | p.Reader = strings.NewReader("") 43 | return p 44 | } 45 | if col < 1 { 46 | p.Error = fmt.Errorf("bad column %d: must be positive", col) 47 | return p 48 | } 49 | result := &bytes.Buffer{} 50 | scanner := bufio.NewScanner(p.Reader) 51 | for scanner.Scan() { 52 | fields := strings.Fields(scanner.Text()) 53 | if len(fields) < col { 54 | continue 55 | } 56 | fmt.Fprintln(result, fields[col-1]) 57 | } 58 | return &Pipeline{ 59 | Reader: result, 60 | } 61 | } 62 | 63 | func (p *Pipeline) Stdout() { 64 | if p.Error != nil { 65 | return 66 | } 67 | io.Copy(p.Output, p.Reader) 68 | } 69 | 70 | func (p *Pipeline) String() (string, error) { 71 | if p.Error != nil { 72 | return "", p.Error 73 | } 74 | data, err := io.ReadAll(p.Reader) 75 | if err != nil { 76 | return "", err 77 | } 78 | return string(data), nil 79 | } 80 | -------------------------------------------------------------------------------- /10.5/pipeline_test.go: -------------------------------------------------------------------------------- 1 | package pipeline_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "pipeline" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestFromFile(t *testing.T) { 14 | t.Parallel() 15 | want := []byte("Hello, world\n") 16 | p := pipeline.FromFile("testdata/hello.txt") 17 | if p.Error != nil { 18 | t.Fatal(p.Error) 19 | } 20 | got, err := io.ReadAll(p.Reader) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if !cmp.Equal(want, got) { 25 | t.Errorf("want %q, got %q", want, got) 26 | } 27 | } 28 | 29 | func TestFromFileInvalid(t *testing.T) { 30 | t.Parallel() 31 | p := pipeline.FromFile("doesnt-exist.txt") 32 | if p.Error == nil { 33 | t.Fatal("want error opening non-existent file, but got nil") 34 | } 35 | } 36 | 37 | func TestColumn(t *testing.T) { 38 | t.Parallel() 39 | input := "1 2 3\n1 2 3\n1 2 3\n" 40 | p := pipeline.FromString(input) 41 | want := "2\n2\n2\n" 42 | got, err := p.Column(2).String() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | if !cmp.Equal(want, got) { 47 | t.Error(cmp.Diff(want, got)) 48 | } 49 | } 50 | 51 | func TestColumnError(t *testing.T) { 52 | t.Parallel() 53 | p := pipeline.FromString("1 2 3\n") 54 | p.Error = errors.New("oh no") 55 | data, err := io.ReadAll(p.Column(1).Reader) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | if len(data) > 0 { 60 | t.Errorf("want no output from Column after error, but got %q", data) 61 | } 62 | } 63 | 64 | func TestColumnInvalid(t *testing.T) { 65 | t.Parallel() 66 | p := pipeline.FromString("1 2 3\n1 2 3\n1 2 3\n") 67 | p.Column(-1) 68 | if p.Error == nil { 69 | t.Error("want error on non-positive Column, but got nil") 70 | } 71 | data, err := io.ReadAll(p.Column(1).Reader) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | if len(data) > 0 { 76 | t.Errorf("want no output from Column with invalid col, but got %q", data) 77 | } 78 | } 79 | 80 | func TestStdout(t *testing.T) { 81 | t.Parallel() 82 | want := "Hello, world\n" 83 | p := pipeline.FromString(want) 84 | buf := &bytes.Buffer{} 85 | p.Output = buf 86 | p.Stdout() 87 | if p.Error != nil { 88 | t.Fatal(p.Error) 89 | } 90 | got := buf.String() 91 | if !cmp.Equal(want, got) { 92 | t.Errorf("want %q, got %q", want, got) 93 | } 94 | } 95 | 96 | func TestStdoutError(t *testing.T) { 97 | t.Parallel() 98 | p := pipeline.FromString("Hello, world\n") 99 | p.Error = errors.New("oh no") 100 | buf := &bytes.Buffer{} 101 | p.Output = buf 102 | p.Stdout() 103 | got := buf.String() 104 | if got != "" { 105 | t.Errorf("want no output from Stdout after error, but got %q", got) 106 | } 107 | } 108 | 109 | func TestString(t *testing.T) { 110 | t.Parallel() 111 | want := "Hello, world\n" 112 | p := pipeline.FromString(want) 113 | got, err := p.String() 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | if !cmp.Equal(want, got) { 118 | t.Errorf("want %q, got %q", want, got) 119 | } 120 | } 121 | 122 | func TestStringError(t *testing.T) { 123 | t.Parallel() 124 | p := pipeline.FromString("Hello, world\n") 125 | p.Error = errors.New("oh no") 126 | _, err := p.String() 127 | if err == nil { 128 | t.Error("want error from String when pipeline has error, but got nil") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /10.5/testdata/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world 2 | -------------------------------------------------------------------------------- /11.1/cmd/load/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "store" 7 | ) 8 | 9 | func main() { 10 | s := store.Open("test.bin") 11 | defer s.Close() 12 | var result int 13 | err := s.Load(&result) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | fmt.Println("Data:", result) 18 | } 19 | -------------------------------------------------------------------------------- /11.1/cmd/store/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "store" 7 | ) 8 | 9 | func main() { 10 | s := store.Open("test.bin") 11 | defer s.Close() 12 | err := s.Save(42) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | fmt.Println("Data stored in test.bin") 17 | } 18 | -------------------------------------------------------------------------------- /11.1/go.mod: -------------------------------------------------------------------------------- 1 | module store 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /11.1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /11.1/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/gob" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type Store struct { 10 | path string 11 | stream io.ReadWriteCloser 12 | } 13 | 14 | func Open(path string) *Store { 15 | return &Store{ 16 | path: path, 17 | } 18 | } 19 | 20 | func (s *Store) Load(v interface{}) error { 21 | if s.stream == nil { 22 | f, err := os.Open(s.path) 23 | if err != nil { 24 | return err 25 | } 26 | s.stream = f 27 | } 28 | return gob.NewDecoder(s.stream).Decode(v) 29 | } 30 | 31 | func (s *Store) Save(v interface{}) error { 32 | if s.stream == nil { 33 | f, err := os.Create(s.path) 34 | if err != nil { 35 | return err 36 | } 37 | s.stream = f 38 | } 39 | return gob.NewEncoder(s.stream).Encode(v) 40 | } 41 | 42 | func (s *Store) Close() error { 43 | if s.stream == nil { 44 | return nil 45 | } 46 | return s.stream.Close() 47 | } 48 | -------------------------------------------------------------------------------- /11.1/store_test.go: -------------------------------------------------------------------------------- 1 | package store_test 2 | 3 | import ( 4 | "store" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestStoreFile(t *testing.T) { 11 | t.Parallel() 12 | path := t.TempDir() + "/store.bin" 13 | output := store.Open(path) 14 | want := []int{2, 3, 5, 7, 11} 15 | err := output.Save(want) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | output.Close() 20 | input := store.Open(path) 21 | var got []int 22 | err = input.Load(&got) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | input.Close() 27 | if !cmp.Equal(want, got) { 28 | t.Error(cmp.Diff(want, got)) 29 | } 30 | } 31 | 32 | func TestClose(t *testing.T) { 33 | t.Parallel() 34 | path := t.TempDir() + "/store.bin" 35 | output := store.Open(path) 36 | err := output.Save("some data") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | err = output.Close() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | err = output.Close() 45 | if err == nil { 46 | t.Fatal("want error closing closed Store, but got nil") 47 | } 48 | } 49 | 50 | func TestCloseNilStream(t *testing.T) { 51 | t.Parallel() 52 | path := t.TempDir() + "/store.bin" 53 | output := store.Open(path) 54 | err := output.Close() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /11.2/battery.go: -------------------------------------------------------------------------------- 1 | package battery 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Battery struct { 8 | Name string 9 | ID int64 10 | ChargePercent int 11 | TimeToFullCharge string 12 | Present bool 13 | } 14 | 15 | func (b Battery) ToJSON() string { 16 | output, err := json.MarshalIndent(b, "", " ") 17 | if err != nil { 18 | panic(err) 19 | } 20 | return string(output) 21 | } 22 | -------------------------------------------------------------------------------- /11.2/battery_test.go: -------------------------------------------------------------------------------- 1 | package battery_test 2 | 3 | import ( 4 | "battery" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestToJSON(t *testing.T) { 12 | t.Parallel() 13 | batt := battery.Battery{ 14 | Name: "InternalBattery-0", 15 | ID: 10813539, 16 | ChargePercent: 100, 17 | TimeToFullCharge: "0:00", 18 | Present: true, 19 | } 20 | wantBytes, err := os.ReadFile("testdata/battery.json") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | want := string(wantBytes) 25 | got := batt.ToJSON() 26 | if !cmp.Equal(want, got) { 27 | t.Error(cmp.Diff(want, got)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /11.2/go.mod: -------------------------------------------------------------------------------- 1 | module battery 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /11.2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /11.2/testdata/battery.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "InternalBattery-0", 3 | "ID": 10813539, 4 | "ChargePercent": 100, 5 | "TimeToFullCharge": "0:00", 6 | "Present": true 7 | } -------------------------------------------------------------------------------- /11.3/go.mod: -------------------------------------------------------------------------------- 1 | module prom 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.6 7 | gopkg.in/yaml.v2 v2.4.0 8 | ) 9 | -------------------------------------------------------------------------------- /11.3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 5 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 6 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 7 | -------------------------------------------------------------------------------- /11.3/prom.go: -------------------------------------------------------------------------------- 1 | package prom 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type Config struct { 11 | Global GlobalConfig 12 | } 13 | 14 | type GlobalConfig struct { 15 | ScrapeInterval time.Duration `yaml:"scrape_interval"` 16 | EvaluationInterval time.Duration `yaml:"evaluation_interval"` 17 | ScrapeTimeout time.Duration `yaml:"scrape_timeout"` 18 | ExternalLabels map[string]string `yaml:"external_labels"` 19 | } 20 | 21 | func ConfigFromYAML(path string) (Config, error) { 22 | f, err := os.Open(path) 23 | if err != nil { 24 | return Config{}, err 25 | } 26 | defer f.Close() 27 | config := Config{ 28 | GlobalConfig{ 29 | ScrapeTimeout: 10 * time.Second, 30 | }, 31 | } 32 | err = yaml.NewDecoder(f).Decode(&config) 33 | if err != nil { 34 | return Config{}, err 35 | } 36 | return config, nil 37 | } 38 | -------------------------------------------------------------------------------- /11.3/prom_test.go: -------------------------------------------------------------------------------- 1 | package prom_test 2 | 3 | import ( 4 | "prom" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestConfigFromYAML(t *testing.T) { 12 | t.Parallel() 13 | want := prom.Config{ 14 | Global: prom.GlobalConfig{ 15 | ScrapeInterval: 15 * time.Second, 16 | EvaluationInterval: 30 * time.Second, 17 | ScrapeTimeout: 10 * time.Second, 18 | ExternalLabels: map[string]string{ 19 | "monitor": "codelab", 20 | "foo": "bar", 21 | }, 22 | }, 23 | } 24 | got, err := prom.ConfigFromYAML("testdata/config.yaml") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | if !cmp.Equal(want, got) { 29 | t.Error(cmp.Diff(want, got)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /11.3/testdata/config.yaml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s 4 | evaluation_interval: 30s 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | external_labels: 8 | monitor: codelab 9 | foo: bar 10 | -------------------------------------------------------------------------------- /12.1/go.mod: -------------------------------------------------------------------------------- 1 | module weather 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /12.1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | const BaseURL = "https://api.openweathermap.org" 12 | 13 | func main() { 14 | key := os.Getenv("OPENWEATHERMAP_API_KEY") 15 | URL := fmt.Sprintf("%s/data/2.5/weather?q=London,UK&appid=%s", BaseURL, key) 16 | resp, err := http.Get(URL) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer resp.Body.Close() 21 | io.Copy(os.Stdout, resp.Body) 22 | } 23 | -------------------------------------------------------------------------------- /12.2/go.mod: -------------------------------------------------------------------------------- 1 | module weather 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /12.2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | const BaseURL = "https://api.openweathermap.org" 12 | 13 | func main() { 14 | key := os.Getenv("OPENWEATHERMAP_API_KEY") 15 | if key == "" { 16 | log.Fatal("Please set the environment variable OPENWEATHERMAP_API_KEY.") 17 | } 18 | URL := fmt.Sprintf("%s/data/2.5/weather?q=London,UK&appid=%s", BaseURL, key) 19 | resp, err := http.Get(URL) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | defer resp.Body.Close() 24 | if resp.StatusCode != http.StatusOK { 25 | log.Fatal("unexpected response status ", resp.Status) 26 | } 27 | io.Copy(os.Stdout, resp.Body) 28 | } 29 | -------------------------------------------------------------------------------- /12.3/cmd/weather/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "weather" 10 | ) 11 | 12 | func main() { 13 | key := os.Getenv("OPENWEATHERMAP_API_KEY") 14 | if key == "" { 15 | log.Fatal("Please set the environment variable OPENWEATHERMAP_API_KEY.") 16 | } 17 | if len(os.Args) < 2 { 18 | log.Fatalf("Usage: %s LOCATION\n\nExample: %[1]s London,UK", os.Args[0]) 19 | } 20 | location := os.Args[1] 21 | URL := weather.FormatURL(weather.BaseURL, location, key) 22 | resp, err := http.Get(URL) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | defer resp.Body.Close() 27 | if resp.StatusCode != http.StatusOK { 28 | log.Fatal("unexpected response status ", resp.Status) 29 | } 30 | data, err := io.ReadAll(resp.Body) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | conditions, err := weather.ParseResponse(data) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | fmt.Println(conditions) 39 | } 40 | -------------------------------------------------------------------------------- /12.3/go.mod: -------------------------------------------------------------------------------- 1 | module weather 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /12.3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /12.3/testdata/weather.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -0.1257, 4 | "lat": 51.5085 5 | }, 6 | "weather": [ 7 | { 8 | "id": 801, 9 | "main": "Clouds", 10 | "description": "few clouds", 11 | "icon": "02d" 12 | } 13 | ], 14 | "base": "stations", 15 | "main": { 16 | "temp": 284.1, 17 | "feels_like": 283.13, 18 | "temp_min": 282.2, 19 | "temp_max": 285.42, 20 | "pressure": 996, 21 | "humidity": 72 22 | }, 23 | "visibility": 10000, 24 | "wind": { 25 | "speed": 4.12, 26 | "deg": 240 27 | }, 28 | "clouds": { 29 | "all": 15 30 | }, 31 | "dt": 1635783703, 32 | "sys": { 33 | "type": 2, 34 | "id": 2019646, 35 | "country": "GB", 36 | "sunrise": 1635749645, 37 | "sunset": 1635784437 38 | }, 39 | "timezone": 0, 40 | "id": 2643743, 41 | "name": "London", 42 | "cod": 200 43 | } -------------------------------------------------------------------------------- /12.3/testdata/weather_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -0.1257, 4 | "lat": 51.5085 5 | }, 6 | "weather": [], 7 | "base": "stations", 8 | "main": { 9 | "temp": 284.1, 10 | "feels_like": 283.13, 11 | "temp_min": 282.2, 12 | "temp_max": 285.42, 13 | "pressure": 996, 14 | "humidity": 72 15 | }, 16 | "visibility": 10000, 17 | "wind": { 18 | "speed": 4.12, 19 | "deg": 240 20 | }, 21 | "clouds": { 22 | "all": 15 23 | }, 24 | "dt": 1635783703, 25 | "sys": { 26 | "type": 2, 27 | "id": 2019646, 28 | "country": "GB", 29 | "sunrise": 1635749645, 30 | "sunset": 1635784437 31 | }, 32 | "timezone": 0, 33 | "id": 2643743, 34 | "name": "London", 35 | "cod": 200 36 | } -------------------------------------------------------------------------------- /12.3/weather.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const BaseURL = "https://api.openweathermap.org" 9 | 10 | type Conditions struct { 11 | Summary string 12 | } 13 | 14 | type OWMResponse struct { 15 | Weather []struct { 16 | Main string 17 | } 18 | } 19 | 20 | func ParseResponse(data []byte) (Conditions, error) { 21 | var resp OWMResponse 22 | err := json.Unmarshal(data, &resp) 23 | if err != nil { 24 | return Conditions{}, fmt.Errorf("invalid API response %q: %w", data, err) 25 | } 26 | if len(resp.Weather) < 1 { 27 | return Conditions{}, fmt.Errorf("invalid API response %q: want at least one Weather element", data) 28 | } 29 | conditions := Conditions{ 30 | Summary: resp.Weather[0].Main, 31 | } 32 | return conditions, nil 33 | } 34 | 35 | func FormatURL(baseURL, location, key string) string { 36 | return fmt.Sprintf("%s/data/2.5/weather?q=%s&appid=%s", baseURL, location, key) 37 | } 38 | -------------------------------------------------------------------------------- /12.3/weather_test.go: -------------------------------------------------------------------------------- 1 | package weather_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | "weather" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestParseResponse(t *testing.T) { 15 | t.Parallel() 16 | data, err := os.ReadFile("testdata/weather.json") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | want := weather.Conditions{ 21 | Summary: "Clouds", 22 | } 23 | got, err := weather.ParseResponse(data) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if !cmp.Equal(want, got) { 28 | t.Error(cmp.Diff(want, got)) 29 | } 30 | } 31 | 32 | func TestParseResponseEmpty(t *testing.T) { 33 | t.Parallel() 34 | _, err := weather.ParseResponse([]byte{}) 35 | if err == nil { 36 | t.Fatal("want error parsing empty response, got nil") 37 | } 38 | } 39 | 40 | func TestParseResponseInvalid(t *testing.T) { 41 | t.Parallel() 42 | data, err := os.ReadFile("testdata/weather_invalid.json") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | _, err = weather.ParseResponse(data) 47 | if err == nil { 48 | t.Fatal("want error parsing invalid response, got nil") 49 | } 50 | } 51 | 52 | func TestFormatURL(t *testing.T) { 53 | t.Parallel() 54 | baseURL := weather.BaseURL 55 | location := "Paris,FR" 56 | key := "dummyAPIKey" 57 | want := "https://api.openweathermap.org/data/2.5/weather?q=Paris,FR&appid=dummyAPIKey" 58 | got := weather.FormatURL(baseURL, location, key) 59 | if !cmp.Equal(want, got) { 60 | t.Error(cmp.Diff(want, got)) 61 | } 62 | } 63 | 64 | func TestSimpleHTTP(t *testing.T) { 65 | t.Parallel() 66 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | fmt.Fprintln(w, "Hello, client") 68 | })) 69 | defer ts.Close() 70 | client := ts.Client() 71 | resp, err := client.Get(ts.URL) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | defer resp.Body.Close() 76 | want := http.StatusOK 77 | got := resp.StatusCode 78 | if !cmp.Equal(want, got) { 79 | t.Error(cmp.Diff(want, got)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /12.4/cmd/weather/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "weather" 5 | ) 6 | 7 | func main() { 8 | weather.RunCLI() 9 | } 10 | -------------------------------------------------------------------------------- /12.4/go.mod: -------------------------------------------------------------------------------- 1 | module weather 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /12.4/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /12.4/testdata/weather.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -0.1257, 4 | "lat": 51.5085 5 | }, 6 | "weather": [ 7 | { 8 | "id": 801, 9 | "main": "Clouds", 10 | "description": "few clouds", 11 | "icon": "02d" 12 | } 13 | ], 14 | "base": "stations", 15 | "main": { 16 | "temp": 284.1, 17 | "feels_like": 283.13, 18 | "temp_min": 282.2, 19 | "temp_max": 285.42, 20 | "pressure": 996, 21 | "humidity": 72 22 | }, 23 | "visibility": 10000, 24 | "wind": { 25 | "speed": 4.12, 26 | "deg": 240 27 | }, 28 | "clouds": { 29 | "all": 15 30 | }, 31 | "dt": 1635783703, 32 | "sys": { 33 | "type": 2, 34 | "id": 2019646, 35 | "country": "GB", 36 | "sunrise": 1635749645, 37 | "sunset": 1635784437 38 | }, 39 | "timezone": 0, 40 | "id": 2643743, 41 | "name": "London", 42 | "cod": 200 43 | } -------------------------------------------------------------------------------- /12.4/testdata/weather_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -0.1257, 4 | "lat": 51.5085 5 | }, 6 | "weather": [], 7 | "base": "stations", 8 | "main": { 9 | "temp": 284.1, 10 | "feels_like": 283.13, 11 | "temp_min": 282.2, 12 | "temp_max": 285.42, 13 | "pressure": 996, 14 | "humidity": 72 15 | }, 16 | "visibility": 10000, 17 | "wind": { 18 | "speed": 4.12, 19 | "deg": 240 20 | }, 21 | "clouds": { 22 | "all": 15 23 | }, 24 | "dt": 1635783703, 25 | "sys": { 26 | "type": 2, 27 | "id": 2019646, 28 | "country": "GB", 29 | "sunrise": 1635749645, 30 | "sunset": 1635784437 31 | }, 32 | "timezone": 0, 33 | "id": 2643743, 34 | "name": "London", 35 | "cod": 200 36 | } -------------------------------------------------------------------------------- /12.4/weather.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "time" 10 | ) 11 | 12 | type Conditions struct { 13 | Summary string 14 | } 15 | 16 | type OWMResponse struct { 17 | Weather []struct { 18 | Main string 19 | } 20 | } 21 | 22 | func ParseResponse(data []byte) (Conditions, error) { 23 | var resp OWMResponse 24 | err := json.Unmarshal(data, &resp) 25 | if err != nil { 26 | return Conditions{}, fmt.Errorf("invalid API response %s: %w", data, err) 27 | } 28 | if len(resp.Weather) < 1 { 29 | return Conditions{}, fmt.Errorf("invalid API response %s: want at least one Weather element", data) 30 | } 31 | conditions := Conditions{ 32 | Summary: resp.Weather[0].Main, 33 | } 34 | return conditions, nil 35 | } 36 | 37 | type Client struct { 38 | APIKey string 39 | BaseURL string 40 | HTTPClient *http.Client 41 | } 42 | 43 | func NewClient(apiKey string) *Client { 44 | return &Client{ 45 | APIKey: apiKey, 46 | BaseURL: "https://api.openweathermap.org", 47 | HTTPClient: &http.Client{ 48 | Timeout: 10 * time.Second, 49 | }, 50 | } 51 | } 52 | 53 | func (c Client) FormatURL(location string) string { 54 | return fmt.Sprintf("%s/data/2.5/weather?q=%s&appid=%s", c.BaseURL, location, c.APIKey) 55 | } 56 | 57 | func (c *Client) GetWeather(location string) (Conditions, error) { 58 | URL := c.FormatURL(location) 59 | resp, err := c.HTTPClient.Get(URL) 60 | if err != nil { 61 | return Conditions{}, err 62 | } 63 | defer resp.Body.Close() 64 | if resp.StatusCode != http.StatusOK { 65 | return Conditions{}, fmt.Errorf("unexpected response status %q", resp.Status) 66 | } 67 | data, err := io.ReadAll(resp.Body) 68 | if err != nil { 69 | return Conditions{}, err 70 | } 71 | conditions, err := ParseResponse(data) 72 | if err != nil { 73 | return Conditions{}, fmt.Errorf("invalid API response %s: %w", data, err) 74 | } 75 | return conditions, nil 76 | } 77 | 78 | func Get(location, key string) (Conditions, error) { 79 | c := NewClient(key) 80 | conditions, err := c.GetWeather(location) 81 | if err != nil { 82 | return Conditions{}, err 83 | } 84 | return conditions, nil 85 | } 86 | 87 | func RunCLI() { 88 | key := os.Getenv("OPENWEATHERMAP_API_KEY") 89 | if key == "" { 90 | fmt.Fprintln(os.Stderr, "Please set the environment variable OPENWEATHERMAP_API_KEY.") 91 | os.Exit(1) 92 | } 93 | if len(os.Args) < 2 { 94 | fmt.Fprintf(os.Stderr, "Usage: %s LOCATION\n\nExample: %[1]s London,UK\n", os.Args[0]) 95 | os.Exit(1) 96 | } 97 | location := os.Args[1] 98 | conditions, err := Get(location, key) 99 | if err != nil { 100 | fmt.Fprintln(os.Stderr, err) 101 | os.Exit(1) 102 | } 103 | fmt.Println(conditions) 104 | } 105 | -------------------------------------------------------------------------------- /12.4/weather_test.go: -------------------------------------------------------------------------------- 1 | package weather_test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | "weather" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestParseResponse(t *testing.T) { 15 | t.Parallel() 16 | data, err := os.ReadFile("testdata/weather.json") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | want := weather.Conditions{ 21 | Summary: "Clouds", 22 | } 23 | got, err := weather.ParseResponse(data) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if !cmp.Equal(want, got) { 28 | t.Error(cmp.Diff(want, got)) 29 | } 30 | } 31 | 32 | func TestParseResponseEmpty(t *testing.T) { 33 | t.Parallel() 34 | _, err := weather.ParseResponse([]byte{}) 35 | if err == nil { 36 | t.Fatal("want error parsing empty response, got nil") 37 | } 38 | } 39 | 40 | func TestParseResponseInvalid(t *testing.T) { 41 | t.Parallel() 42 | data, err := os.ReadFile("testdata/weather_invalid.json") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | _, err = weather.ParseResponse(data) 47 | if err == nil { 48 | t.Fatal("want error parsing invalid response, got nil") 49 | } 50 | } 51 | 52 | func TestFormatURL(t *testing.T) { 53 | t.Parallel() 54 | c := weather.NewClient("dummyAPIKey") 55 | location := "Paris,FR" 56 | want := "https://api.openweathermap.org/data/2.5/weather?q=Paris,FR&appid=dummyAPIKey" 57 | got := c.FormatURL(location) 58 | if !cmp.Equal(want, got) { 59 | t.Error(cmp.Diff(want, got)) 60 | } 61 | } 62 | 63 | func TestGetWeather(t *testing.T) { 64 | t.Parallel() 65 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 | f, err := os.Open("testdata/weather.json") 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | defer f.Close() 71 | io.Copy(w, f) 72 | })) 73 | defer ts.Close() 74 | c := weather.NewClient("dummyAPIKey") 75 | c.BaseURL = ts.URL 76 | c.HTTPClient = ts.Client() 77 | want := weather.Conditions{ 78 | Summary: "Clouds", 79 | } 80 | got, err := c.GetWeather("Paris,FR") 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | if !cmp.Equal(want, got) { 85 | t.Error(cmp.Diff(want, got)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /12.5/cmd/weather/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "weather" 5 | ) 6 | 7 | func main() { 8 | weather.RunCLI() 9 | } 10 | -------------------------------------------------------------------------------- /12.5/go.mod: -------------------------------------------------------------------------------- 1 | module weather 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /12.5/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /12.5/testdata/weather.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -0.1257, 4 | "lat": 51.5085 5 | }, 6 | "weather": [ 7 | { 8 | "id": 801, 9 | "main": "Clouds", 10 | "description": "few clouds", 11 | "icon": "02d" 12 | } 13 | ], 14 | "base": "stations", 15 | "main": { 16 | "temp": 284.1, 17 | "feels_like": 283.13, 18 | "temp_min": 282.2, 19 | "temp_max": 285.42, 20 | "pressure": 996, 21 | "humidity": 72 22 | }, 23 | "visibility": 10000, 24 | "wind": { 25 | "speed": 4.12, 26 | "deg": 240 27 | }, 28 | "clouds": { 29 | "all": 15 30 | }, 31 | "dt": 1635783703, 32 | "sys": { 33 | "type": 2, 34 | "id": 2019646, 35 | "country": "GB", 36 | "sunrise": 1635749645, 37 | "sunset": 1635784437 38 | }, 39 | "timezone": 0, 40 | "id": 2643743, 41 | "name": "London", 42 | "cod": 200 43 | } -------------------------------------------------------------------------------- /12.5/testdata/weather_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -0.1257, 4 | "lat": 51.5085 5 | }, 6 | "weather": [], 7 | "base": "stations", 8 | "main": { 9 | "temp": 284.1, 10 | "feels_like": 283.13, 11 | "temp_min": 282.2, 12 | "temp_max": 285.42, 13 | "pressure": 996, 14 | "humidity": 72 15 | }, 16 | "visibility": 10000, 17 | "wind": { 18 | "speed": 4.12, 19 | "deg": 240 20 | }, 21 | "clouds": { 22 | "all": 15 23 | }, 24 | "dt": 1635783703, 25 | "sys": { 26 | "type": 2, 27 | "id": 2019646, 28 | "country": "GB", 29 | "sunrise": 1635749645, 30 | "sunset": 1635784437 31 | }, 32 | "timezone": 0, 33 | "id": 2643743, 34 | "name": "London", 35 | "cod": 200 36 | } -------------------------------------------------------------------------------- /12.5/weather.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | type Conditions struct { 12 | Summary string 13 | Temperature Temperature 14 | } 15 | 16 | type Temperature float64 17 | 18 | func (t Temperature) Celsius() float64 { 19 | return float64(t) - 273.15 20 | } 21 | 22 | type OWMResponse struct { 23 | Weather []struct { 24 | Main string 25 | } 26 | Main struct { 27 | Temp float64 28 | } 29 | } 30 | 31 | func ParseResponse(data []byte) (Conditions, error) { 32 | var resp OWMResponse 33 | err := json.Unmarshal(data, &resp) 34 | if err != nil { 35 | return Conditions{}, fmt.Errorf("invalid API response %s: %w", data, err) 36 | } 37 | if len(resp.Weather) < 1 { 38 | return Conditions{}, fmt.Errorf("invalid API response %s: want at least one Weather element", data) 39 | } 40 | conditions := Conditions{ 41 | Summary: resp.Weather[0].Main, 42 | Temperature: Temperature(resp.Main.Temp), 43 | } 44 | return conditions, nil 45 | } 46 | 47 | type Client struct { 48 | APIKey string 49 | BaseURL string 50 | HTTPClient *http.Client 51 | } 52 | 53 | func NewClient(apiKey string) *Client { 54 | return &Client{ 55 | APIKey: apiKey, 56 | BaseURL: "https://api.openweathermap.org", 57 | HTTPClient: http.DefaultClient, 58 | } 59 | } 60 | 61 | func (c Client) FormatURL(location string) string { 62 | return fmt.Sprintf("%s/data/2.5/weather?q=%s&appid=%s", c.BaseURL, location, c.APIKey) 63 | } 64 | 65 | func (c *Client) GetWeather(location string) (Conditions, error) { 66 | URL := c.FormatURL(location) 67 | resp, err := c.HTTPClient.Get(URL) 68 | if err != nil { 69 | return Conditions{}, err 70 | } 71 | defer resp.Body.Close() 72 | if resp.StatusCode != http.StatusOK { 73 | return Conditions{}, fmt.Errorf("unexpected response status %q", resp.Status) 74 | } 75 | data, err := io.ReadAll(resp.Body) 76 | if err != nil { 77 | return Conditions{}, err 78 | } 79 | conditions, err := ParseResponse(data) 80 | if err != nil { 81 | return Conditions{}, fmt.Errorf("invalid API response %s: %w", data, err) 82 | } 83 | return conditions, nil 84 | } 85 | 86 | func Get(location, key string) (Conditions, error) { 87 | c := NewClient(key) 88 | conditions, err := c.GetWeather(location) 89 | if err != nil { 90 | return Conditions{}, err 91 | } 92 | return conditions, nil 93 | } 94 | 95 | func RunCLI() { 96 | key := os.Getenv("OPENWEATHERMAP_API_KEY") 97 | if key == "" { 98 | fmt.Fprintln(os.Stderr, "Please set the environment variable OPENWEATHERMAP_API_KEY.") 99 | os.Exit(1) 100 | } 101 | if len(os.Args) < 2 { 102 | fmt.Fprintf(os.Stderr, "Usage: %s LOCATION\n\nExample: %[1]s London,UK\n", os.Args[0]) 103 | os.Exit(1) 104 | } 105 | location := os.Args[1] 106 | conditions, err := Get(location, key) 107 | if err != nil { 108 | fmt.Fprintln(os.Stderr, err) 109 | os.Exit(1) 110 | } 111 | fmt.Printf("%s %.1fºC\n", conditions.Summary, conditions.Temperature.Celsius()) 112 | } 113 | -------------------------------------------------------------------------------- /12.5/weather_test.go: -------------------------------------------------------------------------------- 1 | package weather_test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | "weather" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestParseResponse(t *testing.T) { 15 | t.Parallel() 16 | data, err := os.ReadFile("testdata/weather.json") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | want := weather.Conditions{ 21 | Summary: "Clouds", 22 | Temperature: 284.1, 23 | } 24 | got, err := weather.ParseResponse(data) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | if !cmp.Equal(want, got) { 29 | t.Error(cmp.Diff(want, got)) 30 | } 31 | } 32 | 33 | func TestParseResponseEmpty(t *testing.T) { 34 | t.Parallel() 35 | _, err := weather.ParseResponse([]byte{}) 36 | if err == nil { 37 | t.Fatal("want error parsing empty response, got nil") 38 | } 39 | } 40 | 41 | func TestParseResponseInvalid(t *testing.T) { 42 | t.Parallel() 43 | data, err := os.ReadFile("testdata/weather_invalid.json") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | _, err = weather.ParseResponse(data) 48 | if err == nil { 49 | t.Fatal("want error parsing invalid response, got nil") 50 | } 51 | } 52 | 53 | func TestFormatURL(t *testing.T) { 54 | t.Parallel() 55 | c := weather.NewClient("dummyAPIKey") 56 | location := "Paris,FR" 57 | want := "https://api.openweathermap.org/data/2.5/weather?q=Paris,FR&appid=dummyAPIKey" 58 | got := c.FormatURL(location) 59 | if !cmp.Equal(want, got) { 60 | t.Error(cmp.Diff(want, got)) 61 | } 62 | } 63 | 64 | func TestGetWeather(t *testing.T) { 65 | t.Parallel() 66 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | f, err := os.Open("testdata/weather.json") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | defer f.Close() 72 | io.Copy(w, f) 73 | })) 74 | defer ts.Close() 75 | c := weather.NewClient("dummyAPIKey") 76 | c.BaseURL = ts.URL 77 | c.HTTPClient = ts.Client() 78 | want := weather.Conditions{ 79 | Summary: "Clouds", 80 | Temperature: 284.1, 81 | } 82 | got, err := c.GetWeather("Paris,FR") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | if !cmp.Equal(want, got) { 87 | t.Error(cmp.Diff(want, got)) 88 | } 89 | } 90 | 91 | func TestCelsius(t *testing.T) { 92 | t.Parallel() 93 | input := weather.Temperature(274.15) 94 | want := 1.0 95 | got := input.Celsius() 96 | if !cmp.Equal(want, got) { 97 | t.Error(cmp.Diff(want, got)) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /2.1/cmd/hello/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "hello" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | hello.PrintTo(os.Stdout) 10 | } 11 | -------------------------------------------------------------------------------- /2.1/go.mod: -------------------------------------------------------------------------------- 1 | module hello 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /2.1/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | func PrintTo(w io.Writer) { 9 | fmt.Fprintln(w, "Hello, world") 10 | } 11 | -------------------------------------------------------------------------------- /2.1/hello_test.go: -------------------------------------------------------------------------------- 1 | package hello_test 2 | 3 | import ( 4 | "bytes" 5 | "hello" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestPrintsHelloMessageToWriter(t *testing.T) { 11 | t.Parallel() 12 | fakeTerminal := &bytes.Buffer{} 13 | hello.PrintTo(io.Writer(fakeTerminal)) 14 | want := "Hello, world\n" 15 | got := fakeTerminal.String() 16 | if want != got { 17 | t.Errorf("want %q, got %q", want, got) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /2.2/cmd/hello/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "hello" 5 | ) 6 | 7 | func main() { 8 | hello.Print() 9 | } 10 | -------------------------------------------------------------------------------- /2.2/go.mod: -------------------------------------------------------------------------------- 1 | module hello 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /2.2/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type Printer struct { 10 | Output io.Writer 11 | } 12 | 13 | func NewPrinter() *Printer { 14 | return &Printer{ 15 | Output: os.Stdout, 16 | } 17 | } 18 | 19 | func (p *Printer) Print() { 20 | fmt.Fprintln(p.Output, "Hello, world") 21 | } 22 | 23 | func Print() { 24 | NewPrinter().Print() 25 | } 26 | -------------------------------------------------------------------------------- /2.2/hello_test.go: -------------------------------------------------------------------------------- 1 | package hello_test 2 | 3 | import ( 4 | "bytes" 5 | "hello" 6 | "testing" 7 | ) 8 | 9 | func TestPrintsHelloMessageToWriter(t *testing.T) { 10 | fakeTerminal := &bytes.Buffer{} 11 | p := &hello.Printer{ 12 | Output: fakeTerminal, 13 | } 14 | p.Print() 15 | want := "Hello, world\n" 16 | got := fakeTerminal.String() 17 | if want != got { 18 | t.Errorf("want %q, got %q", want, got) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /3.1/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /3.1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | lines := 0 11 | scanner := bufio.NewScanner(os.Stdin) 12 | for scanner.Scan() { 13 | lines++ 14 | } 15 | fmt.Println(lines) 16 | } -------------------------------------------------------------------------------- /3.2/cmd/count/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "count" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println(count.Lines()) 10 | } 11 | -------------------------------------------------------------------------------- /3.2/count.go: -------------------------------------------------------------------------------- 1 | package count 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type counter struct { 10 | Input io.Reader 11 | } 12 | 13 | func NewCounter() counter { 14 | return counter{ 15 | Input: os.Stdin, 16 | } 17 | } 18 | 19 | func (c counter) Lines() int { 20 | lines := 0 21 | scanner := bufio.NewScanner(c.Input) 22 | for scanner.Scan() { 23 | lines++ 24 | } 25 | return lines 26 | } 27 | 28 | func Lines() int { 29 | return NewCounter().Lines() 30 | } 31 | -------------------------------------------------------------------------------- /3.2/count_test.go: -------------------------------------------------------------------------------- 1 | package count_test 2 | 3 | import ( 4 | "bytes" 5 | "count" 6 | "testing" 7 | ) 8 | 9 | func TestLines(t *testing.T) { 10 | t.Parallel() 11 | c := count.NewCounter() 12 | c.Input = bytes.NewBufferString("1\n2\n3") 13 | want := 3 14 | got := c.Lines() 15 | if want != got { 16 | t.Errorf("want %d, got %d", want, got) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /3.2/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /3.3/cmd/count/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "count" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println(count.Lines()) 10 | } 11 | -------------------------------------------------------------------------------- /3.3/count.go: -------------------------------------------------------------------------------- 1 | package count 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "os" 8 | ) 9 | 10 | type counter struct { 11 | input io.Reader 12 | output io.Writer 13 | } 14 | 15 | type option func(*counter) error 16 | 17 | func WithInput(input io.Reader) option { 18 | return func(c *counter) error { 19 | if input == nil { 20 | return errors.New("nil input reader") 21 | } 22 | c.input = input 23 | return nil 24 | } 25 | } 26 | 27 | func WithOutput(output io.Writer) option { 28 | return func(c *counter) error { 29 | if output == nil { 30 | return errors.New("nil output writer") 31 | } 32 | c.output = output 33 | return nil 34 | } 35 | } 36 | 37 | func NewCounter(opts ...option) (counter, error) { 38 | c := counter{ 39 | input: os.Stdin, 40 | output: os.Stdout, 41 | } 42 | for _, opt := range opts { 43 | err := opt(&c) 44 | if err != nil { 45 | return counter{}, err 46 | } 47 | } 48 | return c, nil 49 | } 50 | 51 | func (c counter) Lines() int { 52 | lines := 0 53 | scanner := bufio.NewScanner(c.input) 54 | for scanner.Scan() { 55 | lines++ 56 | } 57 | return lines 58 | } 59 | 60 | func Lines() int { 61 | c, err := NewCounter() 62 | if err != nil { 63 | panic("internal error") 64 | } 65 | return c.Lines() 66 | } 67 | -------------------------------------------------------------------------------- /3.3/count_test.go: -------------------------------------------------------------------------------- 1 | package count_test 2 | 3 | import ( 4 | "bytes" 5 | "count" 6 | "testing" 7 | ) 8 | 9 | func TestLines(t *testing.T) { 10 | t.Parallel() 11 | inputBuf := bytes.NewBufferString("1\n2\n3") 12 | c, err := count.NewCounter( 13 | count.WithInput(inputBuf), 14 | ) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | want := 3 19 | got := c.Lines() 20 | if want != got { 21 | t.Errorf("want %d, got %d", want, got) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /3.3/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /4.1/cmd/count/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "count" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println(count.Lines()) 10 | } 11 | -------------------------------------------------------------------------------- /4.1/count.go: -------------------------------------------------------------------------------- 1 | package count 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | type counter struct { 12 | input io.Reader 13 | output io.Writer 14 | } 15 | 16 | type option func(*counter) error 17 | 18 | func WithInput(input io.Reader) option { 19 | return func(c *counter) error { 20 | if input == nil { 21 | return errors.New("nil input reader") 22 | } 23 | c.input = input 24 | return nil 25 | } 26 | } 27 | 28 | func WithInputFromArgs(args []string) option { 29 | return func(c *counter) error { 30 | if len(args) < 1 { 31 | return nil 32 | } 33 | f, err := os.Open(args[0]) 34 | if err != nil { 35 | return err 36 | } 37 | c.input = f 38 | return nil 39 | } 40 | } 41 | 42 | func WithOutput(output io.Writer) option { 43 | return func(c *counter) error { 44 | if output == nil { 45 | return errors.New("nil output writer") 46 | } 47 | c.output = output 48 | return nil 49 | } 50 | } 51 | 52 | func NewCounter(opts ...option) (counter, error) { 53 | c := counter{ 54 | input: os.Stdin, 55 | output: os.Stdout, 56 | } 57 | for _, opt := range opts { 58 | err := opt(&c) 59 | if err != nil { 60 | return counter{}, err 61 | } 62 | } 63 | return c, nil 64 | } 65 | 66 | func (c counter) Lines() int { 67 | lines := 0 68 | scanner := bufio.NewScanner(c.input) 69 | for scanner.Scan() { 70 | lines++ 71 | } 72 | return lines 73 | } 74 | 75 | func Lines() int { 76 | c, err := NewCounter( 77 | WithInputFromArgs(os.Args[1:]), 78 | ) 79 | if err != nil { 80 | fmt.Fprintln(os.Stderr, err) 81 | os.Exit(1) 82 | } 83 | return c.Lines() 84 | } 85 | -------------------------------------------------------------------------------- /4.1/count_test.go: -------------------------------------------------------------------------------- 1 | package count_test 2 | 3 | import ( 4 | "bytes" 5 | "count" 6 | "testing" 7 | ) 8 | 9 | func TestLines(t *testing.T) { 10 | t.Parallel() 11 | inputBuf := bytes.NewBufferString("1\n2\n3") 12 | c, err := count.NewCounter( 13 | count.WithInput(inputBuf), 14 | ) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | want := 3 19 | got := c.Lines() 20 | if want != got { 21 | t.Errorf("want %d, got %d", want, got) 22 | } 23 | } 24 | 25 | func TestWithInputFromArgs(t *testing.T) { 26 | t.Parallel() 27 | args := []string{"testdata/three_lines.txt"} 28 | c, err := count.NewCounter( 29 | count.WithInputFromArgs(args), 30 | ) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | want := 3 35 | got := c.Lines() 36 | if want != got { 37 | t.Errorf("want %d, got %d", want, got) 38 | } 39 | } 40 | 41 | func TestWithInputFromArgsEmpty(t *testing.T) { 42 | t.Parallel() 43 | inputBuf := bytes.NewBufferString("1\n2\n3") 44 | c, err := count.NewCounter( 45 | count.WithInput(inputBuf), 46 | count.WithInputFromArgs([]string{}), 47 | ) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | want := 3 52 | got := c.Lines() 53 | if want != got { 54 | t.Errorf("want %d, got %d", want, got) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /4.1/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /4.1/testdata/three_lines.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 -------------------------------------------------------------------------------- /5.1/cmd/lines/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "count" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println(count.Lines()) 10 | } 11 | -------------------------------------------------------------------------------- /5.1/cmd/words/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "count" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fmt.Println(count.Words()) 10 | } 11 | -------------------------------------------------------------------------------- /5.1/count.go: -------------------------------------------------------------------------------- 1 | package count 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | type counter struct { 12 | input io.Reader 13 | output io.Writer 14 | } 15 | 16 | type option func(*counter) error 17 | 18 | func WithInput(input io.Reader) option { 19 | return func(c *counter) error { 20 | if input == nil { 21 | return errors.New("nil input reader") 22 | } 23 | c.input = input 24 | return nil 25 | } 26 | } 27 | 28 | func WithInputFromArgs(args []string) option { 29 | return func(c *counter) error { 30 | if len(args) < 1 { 31 | return nil 32 | } 33 | f, err := os.Open(args[0]) 34 | if err != nil { 35 | return err 36 | } 37 | c.input = f 38 | return nil 39 | } 40 | } 41 | 42 | func WithOutput(output io.Writer) option { 43 | return func(c *counter) error { 44 | if output == nil { 45 | return errors.New("nil output writer") 46 | } 47 | c.output = output 48 | return nil 49 | } 50 | } 51 | 52 | func NewCounter(opts ...option) (counter, error) { 53 | c := counter{ 54 | input: os.Stdin, 55 | output: os.Stdout, 56 | } 57 | for _, opt := range opts { 58 | err := opt(&c) 59 | if err != nil { 60 | return counter{}, err 61 | } 62 | } 63 | return c, nil 64 | } 65 | 66 | func (c counter) Lines() int { 67 | lines := 0 68 | scanner := bufio.NewScanner(c.input) 69 | for scanner.Scan() { 70 | lines++ 71 | } 72 | return lines 73 | } 74 | 75 | func (c counter) Words() int { 76 | words := 0 77 | scanner := bufio.NewScanner(c.input) 78 | scanner.Split(bufio.ScanWords) 79 | for scanner.Scan() { 80 | words++ 81 | } 82 | return words 83 | } 84 | 85 | func Lines() int { 86 | c, err := NewCounter( 87 | WithInputFromArgs(os.Args[1:]), 88 | ) 89 | if err != nil { 90 | fmt.Fprintln(os.Stderr, err) 91 | os.Exit(1) 92 | } 93 | return c.Lines() 94 | } 95 | 96 | func Words() int { 97 | c, err := NewCounter( 98 | WithInputFromArgs(os.Args[1:]), 99 | ) 100 | if err != nil { 101 | fmt.Fprintln(os.Stderr, err) 102 | os.Exit(1) 103 | } 104 | return c.Words() 105 | } 106 | -------------------------------------------------------------------------------- /5.1/count_test.go: -------------------------------------------------------------------------------- 1 | package count_test 2 | 3 | import ( 4 | "bytes" 5 | "count" 6 | "testing" 7 | ) 8 | 9 | func TestWithInputFromArgs(t *testing.T) { 10 | t.Parallel() 11 | args := []string{"testdata/three_lines.txt"} 12 | c, err := count.NewCounter( 13 | count.WithInputFromArgs(args), 14 | ) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | want := 3 19 | got := c.Lines() 20 | if want != got { 21 | t.Errorf("want %d, got %d", want, got) 22 | } 23 | } 24 | 25 | func TestWithInputFromArgsEmpty(t *testing.T) { 26 | t.Parallel() 27 | inputBuf := bytes.NewBufferString("1\n2\n3") 28 | c, err := count.NewCounter( 29 | count.WithInput(inputBuf), 30 | count.WithInputFromArgs([]string{}), 31 | ) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | want := 3 36 | got := c.Lines() 37 | if want != got { 38 | t.Errorf("want %d, got %d", want, got) 39 | } 40 | } 41 | 42 | func TestLines(t *testing.T) { 43 | t.Parallel() 44 | inputBuf := bytes.NewBufferString("1\n2\n3") 45 | c, err := count.NewCounter( 46 | count.WithInput(inputBuf), 47 | ) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | want := 3 52 | got := c.Lines() 53 | if want != got { 54 | t.Errorf("want %d, got %d", want, got) 55 | } 56 | } 57 | 58 | func TestWords(t *testing.T) { 59 | t.Parallel() 60 | inputBuf := bytes.NewBufferString("1\n2 words\n3 this time") 61 | c, err := count.NewCounter( 62 | count.WithInput(inputBuf), 63 | ) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | want := 6 68 | got := c.Words() 69 | if want != got { 70 | t.Errorf("want %d, got %d", want, got) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /5.1/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /5.1/testdata/three_lines.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 -------------------------------------------------------------------------------- /5.2/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /5.2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | countWords := flag.Bool("w", false, "Count words instead of lines") 10 | flag.Parse() 11 | if *countWords { 12 | fmt.Println("We're counting words!") 13 | } 14 | } -------------------------------------------------------------------------------- /5.3/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /5.3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | fset := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 11 | countWords := fset.Bool("w", false, "Count words instead of lines") 12 | fset.Parse(os.Args[1:]) 13 | if *countWords { 14 | fmt.Println("We're counting words!") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /5.4/cmd/count/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "count" 5 | ) 6 | 7 | func main() { 8 | count.RunCLI() 9 | } 10 | -------------------------------------------------------------------------------- /5.4/count.go: -------------------------------------------------------------------------------- 1 | package count 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | type counter struct { 13 | wordCount bool 14 | input io.Reader 15 | output io.Writer 16 | } 17 | 18 | type option func(*counter) error 19 | 20 | func WithInput(input io.Reader) option { 21 | return func(c *counter) error { 22 | if input == nil { 23 | return errors.New("nil input reader") 24 | } 25 | c.input = input 26 | return nil 27 | } 28 | } 29 | 30 | func FromArgs(args []string) option { 31 | return func(c *counter) error { 32 | fset := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 33 | wordCount := fset.Bool("w", false, "Count words instead of lines") 34 | fset.SetOutput(c.output) 35 | err := fset.Parse(args) 36 | if err != nil { 37 | return err 38 | } 39 | c.wordCount = *wordCount 40 | args = fset.Args() 41 | if len(args) < 1 { 42 | return nil 43 | } 44 | f, err := os.Open(args[0]) 45 | if err != nil { 46 | return err 47 | } 48 | c.input = f 49 | return nil 50 | } 51 | } 52 | 53 | func WithOutput(output io.Writer) option { 54 | return func(c *counter) error { 55 | if output == nil { 56 | return errors.New("nil output writer") 57 | } 58 | c.output = output 59 | return nil 60 | } 61 | } 62 | 63 | func NewCounter(opts ...option) (counter, error) { 64 | c := counter{ 65 | input: os.Stdin, 66 | output: os.Stdout, 67 | } 68 | for _, opt := range opts { 69 | err := opt(&c) 70 | if err != nil { 71 | return counter{}, err 72 | } 73 | } 74 | return c, nil 75 | } 76 | 77 | func (c counter) Lines() int { 78 | count := 0 79 | scanner := bufio.NewScanner(c.input) 80 | for scanner.Scan() { 81 | count++ 82 | } 83 | return count 84 | } 85 | 86 | func (c counter) Words() int { 87 | count := 0 88 | scanner := bufio.NewScanner(c.input) 89 | scanner.Split(bufio.ScanWords) 90 | for scanner.Scan() { 91 | count++ 92 | } 93 | return count 94 | } 95 | 96 | func RunCLI() { 97 | c, err := NewCounter( 98 | FromArgs(os.Args[1:]), 99 | ) 100 | if err != nil { 101 | fmt.Fprintln(os.Stderr, err) 102 | os.Exit(1) 103 | } 104 | if c.wordCount { 105 | fmt.Println(c.Words()) 106 | } else { 107 | fmt.Println(c.Lines()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /5.4/count_test.go: -------------------------------------------------------------------------------- 1 | package count_test 2 | 3 | import ( 4 | "bytes" 5 | "count" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestFromArgs(t *testing.T) { 11 | t.Parallel() 12 | args := []string{"testdata/three_lines.txt"} 13 | c, err := count.NewCounter( 14 | count.FromArgs(args), 15 | ) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | want := 3 20 | got := c.Lines() 21 | if want != got { 22 | t.Errorf("want %d, got %d", want, got) 23 | } 24 | } 25 | 26 | func TestFromArgsEmpty(t *testing.T) { 27 | t.Parallel() 28 | inputBuf := bytes.NewBufferString("1\n2\n3") 29 | c, err := count.NewCounter( 30 | count.WithInput(inputBuf), 31 | count.FromArgs([]string{}), 32 | ) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | want := 3 37 | got := c.Lines() 38 | if want != got { 39 | t.Errorf("want %d, got %d", want, got) 40 | } 41 | } 42 | 43 | func TestFromArgsErrorsOnBogusFlag(t *testing.T) { 44 | t.Parallel() 45 | args := []string{"-bogus"} 46 | _, err := count.NewCounter( 47 | count.WithOutput(io.Discard), 48 | count.FromArgs(args), 49 | ) 50 | if err == nil { 51 | t.Fatal("want error on bogus flag, got nil") 52 | } 53 | } 54 | 55 | func TestWordCount(t *testing.T) { 56 | t.Parallel() 57 | args := []string{"-w", "testdata/three_lines.txt"} 58 | c, err := count.NewCounter( 59 | count.FromArgs(args), 60 | ) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | want := 6 65 | got := c.Words() 66 | if want != got { 67 | t.Errorf("want %d, got %d", want, got) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /5.4/go.mod: -------------------------------------------------------------------------------- 1 | module count 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /5.4/testdata/three_lines.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 words 3 | 3 this time -------------------------------------------------------------------------------- /6.1/go.mod: -------------------------------------------------------------------------------- 1 | module writer 2 | 3 | go 1.16 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /6.1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /6.1/writer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import "os" 4 | 5 | func WriteToFile(path string, data []byte) error { 6 | err := os.WriteFile(path, data, 0600) 7 | if err != nil { 8 | return err 9 | } 10 | return os.Chmod(path, 0600) 11 | } 12 | -------------------------------------------------------------------------------- /6.1/writer_test.go: -------------------------------------------------------------------------------- 1 | package writer_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "writer" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestWriteToFile(t *testing.T) { 12 | t.Parallel() 13 | path := t.TempDir() + "/write_test.txt" 14 | want := []byte{1, 2, 3} 15 | err := writer.WriteToFile(path, want) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | stat, err := os.Stat(path) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | perm := stat.Mode().Perm() 24 | if perm != 0600 { 25 | t.Errorf("want file mode 0600, got 0%o", perm) 26 | } 27 | got, err := os.ReadFile(path) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if !cmp.Equal(want, got) { 32 | t.Fatal(cmp.Diff(want, got)) 33 | } 34 | } 35 | 36 | func TestWriteToFileClobbers(t *testing.T) { 37 | t.Parallel() 38 | path := t.TempDir() + "/clobber_test.txt" 39 | err := os.WriteFile(path, []byte{4, 5, 6}, 0600) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | want := []byte{1, 2, 3} 44 | err = writer.WriteToFile(path, want) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | got, err := os.ReadFile(path) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | if !cmp.Equal(want, got) { 53 | t.Fatal(cmp.Diff(want, got)) 54 | } 55 | } 56 | 57 | func TestPermsClosed(t *testing.T) { 58 | t.Parallel() 59 | path := t.TempDir() + "/perms_test.txt" 60 | // Pre-create empty file with open perms 61 | err := os.WriteFile(path, []byte{}, 0644) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | err = writer.WriteToFile(path, []byte{}) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | stat, err := os.Stat(path) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | perm := stat.Mode().Perm() 74 | if perm != 0600 { 75 | t.Errorf("want file mode 0600, got 0%o", perm) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /7.1/findgo/file.go: -------------------------------------------------------------------------------- 1 | Some lines 2 | of code 3 | could go here 4 | -------------------------------------------------------------------------------- /7.1/findgo/subfolder/subfolder.go: -------------------------------------------------------------------------------- 1 | Some more code 2 | here. -------------------------------------------------------------------------------- /7.1/findgo/subfolder2/another.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.1/findgo/subfolder2/another.go -------------------------------------------------------------------------------- /7.1/findgo/subfolder2/file.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.1/findgo/subfolder2/file.go -------------------------------------------------------------------------------- /7.1/go.mod: -------------------------------------------------------------------------------- 1 | module findgo 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /7.1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | ) 8 | 9 | func main() { 10 | fmt.Println(countGoFiles("findgo", 0)) 11 | } 12 | 13 | func countGoFiles(folder string, count int) int { 14 | files, err := os.ReadDir(folder) 15 | if err != nil { 16 | // skip 17 | return count 18 | } 19 | for _, f := range files { 20 | if f.IsDir() { 21 | count = countGoFiles(folder+"/"+f.Name(), count) 22 | } 23 | if path.Ext(f.Name()) == ".go" { 24 | count++ 25 | } 26 | } 27 | return count 28 | } 29 | -------------------------------------------------------------------------------- /7.2/findgo/file.go: -------------------------------------------------------------------------------- 1 | Some lines 2 | of code 3 | could go here 4 | -------------------------------------------------------------------------------- /7.2/findgo/subfolder/subfolder.go: -------------------------------------------------------------------------------- 1 | Some more code 2 | here. -------------------------------------------------------------------------------- /7.2/findgo/subfolder2/another.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.2/findgo/subfolder2/another.go -------------------------------------------------------------------------------- /7.2/findgo/subfolder2/file.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.2/findgo/subfolder2/file.go -------------------------------------------------------------------------------- /7.2/go.mod: -------------------------------------------------------------------------------- 1 | module findgo 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /7.2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | fsys := os.DirFS("findgo") 12 | matches, err := fs.Glob(fsys, "*.go") 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | fmt.Println(len(matches)) 17 | } 18 | -------------------------------------------------------------------------------- /7.3/findgo/file.go: -------------------------------------------------------------------------------- 1 | Some lines 2 | of code 3 | could go here 4 | -------------------------------------------------------------------------------- /7.3/findgo/subfolder/subfolder.go: -------------------------------------------------------------------------------- 1 | Some more code 2 | here. -------------------------------------------------------------------------------- /7.3/findgo/subfolder2/another.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.3/findgo/subfolder2/another.go -------------------------------------------------------------------------------- /7.3/findgo/subfolder2/file.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.3/findgo/subfolder2/file.go -------------------------------------------------------------------------------- /7.3/go.mod: -------------------------------------------------------------------------------- 1 | module findgo 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /7.3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func main() { 11 | var count int 12 | fsys := os.DirFS("findgo") 13 | fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { 14 | if filepath.Ext(p) == ".go" { 15 | count++ 16 | } 17 | return nil 18 | }) 19 | fmt.Println(count) 20 | } 21 | -------------------------------------------------------------------------------- /7.4/cmd/findgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "findgo" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | fmt.Println(findgo.Files(os.Args[1])) 11 | } 12 | -------------------------------------------------------------------------------- /7.4/findgo.go: -------------------------------------------------------------------------------- 1 | package findgo 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func Files(path string) (count int) { 10 | fsys := os.DirFS(path) 11 | fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { 12 | if filepath.Ext(p) == ".go" { 13 | count++ 14 | } 15 | return nil 16 | }) 17 | return count 18 | } 19 | -------------------------------------------------------------------------------- /7.4/findgo_test.go: -------------------------------------------------------------------------------- 1 | package findgo_test 2 | 3 | import ( 4 | "findgo" 5 | "testing" 6 | ) 7 | 8 | func TestFiles(t *testing.T) { 9 | t.Parallel() 10 | want := 4 11 | got := findgo.Files("testdata/findgo") 12 | if want != got { 13 | t.Errorf("want %d, got %d", want, got) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /7.4/go.mod: -------------------------------------------------------------------------------- 1 | module findgo 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /7.4/testdata/findgo/file.go: -------------------------------------------------------------------------------- 1 | Some lines 2 | of code 3 | could go here 4 | -------------------------------------------------------------------------------- /7.4/testdata/findgo/subfolder/subfolder.go: -------------------------------------------------------------------------------- 1 | Some more code 2 | here. -------------------------------------------------------------------------------- /7.4/testdata/findgo/subfolder2/another.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.4/testdata/findgo/subfolder2/another.go -------------------------------------------------------------------------------- /7.4/testdata/findgo/subfolder2/file.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.4/testdata/findgo/subfolder2/file.go -------------------------------------------------------------------------------- /7.5/cmd/findgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "findgo" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | fmt.Println(findgo.Files(os.DirFS(os.Args[1]))) 11 | } 12 | -------------------------------------------------------------------------------- /7.5/findgo.go: -------------------------------------------------------------------------------- 1 | package findgo 2 | 3 | import ( 4 | "io/fs" 5 | "path/filepath" 6 | ) 7 | 8 | func Files(fsys fs.FS) (count int) { 9 | fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { 10 | if filepath.Ext(p) == ".go" { 11 | count++ 12 | } 13 | return nil 14 | }) 15 | return count 16 | } 17 | -------------------------------------------------------------------------------- /7.5/findgo_test.go: -------------------------------------------------------------------------------- 1 | package findgo_test 2 | 3 | import ( 4 | "archive/zip" 5 | "findgo" 6 | "os" 7 | "testing" 8 | "testing/fstest" 9 | ) 10 | 11 | func TestFilesOnDisk(t *testing.T) { 12 | t.Parallel() 13 | fsys := os.DirFS("testdata/findgo") 14 | want := 4 15 | got := findgo.Files(fsys) 16 | if want != got { 17 | t.Errorf("want %d, got %d", want, got) 18 | } 19 | } 20 | 21 | func TestFilesInMemory(t *testing.T) { 22 | t.Parallel() 23 | fsys := fstest.MapFS{ 24 | "file.go": {}, 25 | "subfolder/subfolder.go": {}, 26 | "subfolder2/another.go": {}, 27 | "subfolder2/file.go": {}, 28 | } 29 | want := 4 30 | got := findgo.Files(fsys) 31 | if want != got { 32 | t.Errorf("want %d, got %d", want, got) 33 | } 34 | } 35 | 36 | func TestFilesInZIP(t *testing.T) { 37 | t.Parallel() 38 | fsys, err := zip.OpenReader("testdata/findgo.zip") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | want := 4 43 | got := findgo.Files(fsys) 44 | if want != got { 45 | t.Errorf("want %d, got %d", want, got) 46 | } 47 | } 48 | 49 | func BenchmarkFilesOnDisk(b *testing.B) { 50 | fsys := os.DirFS("testdata/findgo") 51 | b.ResetTimer() 52 | for i := 0; i < b.N; i++ { 53 | findgo.Files(fsys) 54 | } 55 | } 56 | 57 | func BenchmarkFilesInMemory(b *testing.B) { 58 | fsys := fstest.MapFS{ 59 | "file.go": {}, 60 | "subfolder/subfolder.go": {}, 61 | "subfolder2/another.go": {}, 62 | "subfolder2/file.go": {}, 63 | } 64 | b.ResetTimer() 65 | for i := 0; i < b.N; i++ { 66 | findgo.Files(fsys) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /7.5/go.mod: -------------------------------------------------------------------------------- 1 | module findgo 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /7.5/testdata/findgo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.5/testdata/findgo.zip -------------------------------------------------------------------------------- /7.5/testdata/findgo/file.go: -------------------------------------------------------------------------------- 1 | Some lines 2 | of code 3 | could go here 4 | -------------------------------------------------------------------------------- /7.5/testdata/findgo/subfolder/subfolder.go: -------------------------------------------------------------------------------- 1 | Some more code 2 | here. -------------------------------------------------------------------------------- /7.5/testdata/findgo/subfolder2/another.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.5/testdata/findgo/subfolder2/another.go -------------------------------------------------------------------------------- /7.5/testdata/findgo/subfolder2/file.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/7.5/testdata/findgo/subfolder2/file.go -------------------------------------------------------------------------------- /8.1/go.mod: -------------------------------------------------------------------------------- 1 | module ls 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /8.1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | func main() { 9 | cmd := exec.Command("/bin/ls", "-l", "main.go") 10 | cmd.Stdout = os.Stdout 11 | cmd.Run() 12 | } 13 | -------------------------------------------------------------------------------- /8.2/battery.go: -------------------------------------------------------------------------------- 1 | package battery 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | type Status struct { 11 | ChargePercent int 12 | } 13 | 14 | func GetStatus() (Status, error) { 15 | text, err := GetPmsetOutput() 16 | if err != nil { 17 | return Status{}, err 18 | } 19 | return ParsePmsetOutput(text) 20 | } 21 | 22 | func GetPmsetOutput() (string, error) { 23 | data, err := exec.Command("/usr/bin/pmset", "-g", "ps").CombinedOutput() 24 | if err != nil { 25 | return "", err 26 | } 27 | return string(data), nil 28 | } 29 | 30 | var pmsetOutput = regexp.MustCompile("([0-9]+)%") 31 | 32 | func ParsePmsetOutput(text string) (Status, error) { 33 | matches := pmsetOutput.FindStringSubmatch(text) 34 | if len(matches) < 2 { 35 | return Status{}, fmt.Errorf("failed to parse pmset output: %q", text) 36 | } 37 | charge, err := strconv.Atoi(matches[1]) 38 | if err != nil { 39 | return Status{}, fmt.Errorf("failed to parse charge percentage: %q", matches[1]) 40 | } 41 | return Status{ChargePercent: charge}, nil 42 | } 43 | -------------------------------------------------------------------------------- /8.2/battery_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package battery_test 4 | 5 | import ( 6 | "battery" 7 | "testing" 8 | ) 9 | 10 | func TestGetPmsetOutput(t *testing.T) { 11 | t.Parallel() 12 | text, err := battery.GetPmsetOutput() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | status, err := battery.ParsePmsetOutput(text) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | t.Logf("Charge: %d%%", status.ChargePercent) 21 | } 22 | -------------------------------------------------------------------------------- /8.2/battery_test.go: -------------------------------------------------------------------------------- 1 | package battery_test 2 | 3 | import ( 4 | "battery" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestParsePmsetOutput(t *testing.T) { 12 | t.Parallel() 13 | data, err := os.ReadFile("testdata/pmset.txt") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | want := battery.Status{ 18 | ChargePercent: 98, 19 | } 20 | got, err := battery.ParsePmsetOutput(string(data)) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if !cmp.Equal(want, got) { 25 | t.Error(cmp.Diff(want, got)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /8.2/cmd/battery/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "battery" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | status, err := battery.GetStatus() 11 | if err != nil { 12 | fmt.Fprintf(os.Stderr, "couldn't read battery status: %v", err) 13 | } 14 | fmt.Printf("Battery %d%% charged\n", status.ChargePercent) 15 | } 16 | -------------------------------------------------------------------------------- /8.2/go.mod: -------------------------------------------------------------------------------- 1 | module battery 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /8.2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /8.2/testdata/pmset.txt: -------------------------------------------------------------------------------- 1 | Now drawing from 'AC Power' 2 | -InternalBattery-0 (id=10879075) 98%; charging; 0:42 remaining present: true 3 | -------------------------------------------------------------------------------- /9.1/cmd/shell/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "shell" 8 | ) 9 | 10 | func main() { 11 | input := bufio.NewReader(os.Stdin) 12 | for { 13 | fmt.Print("> ") 14 | line, err := input.ReadString('\n') 15 | if err != nil { 16 | fmt.Println("\nBe seeing you!") 17 | break 18 | } 19 | cmd, err := shell.CmdFromString(line) 20 | if err != nil { 21 | continue 22 | } 23 | out, err := cmd.CombinedOutput() 24 | if err != nil { 25 | fmt.Println("error:", err) 26 | } 27 | fmt.Printf("%s", out) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /9.1/go.mod: -------------------------------------------------------------------------------- 1 | module shell 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /9.1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /9.1/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | func CmdFromString(cmdLine string) (*exec.Cmd, error) { 10 | args := strings.Fields(cmdLine) 11 | if len(args) < 1 { 12 | return nil, errors.New("empty input") 13 | } 14 | return exec.Command(args[0], args[1:]...), nil 15 | } 16 | -------------------------------------------------------------------------------- /9.1/shell_test.go: -------------------------------------------------------------------------------- 1 | package shell_test 2 | 3 | import ( 4 | "shell" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestCmdFromStringErrorsOnEmptyInput(t *testing.T) { 11 | t.Parallel() 12 | _, err := shell.CmdFromString("") 13 | if err == nil { 14 | t.Fatal("want error on empty input, got nil") 15 | } 16 | } 17 | 18 | func TestCmdFromString(t *testing.T) { 19 | t.Parallel() 20 | cmd, err := shell.CmdFromString("/bin/ls -l main.go") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | want := []string{"/bin/ls", "-l", "main.go"} 25 | got := cmd.Args 26 | if !cmp.Equal(want, got) { 27 | t.Error(cmp.Diff(want, got)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /9.2/cmd/shell/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "shell" 5 | ) 6 | 7 | func main() { 8 | shell.RunCLI() 9 | } 10 | -------------------------------------------------------------------------------- /9.2/go.mod: -------------------------------------------------------------------------------- 1 | module shell 2 | 3 | go 1.17 4 | 5 | require github.com/google/go-cmp v0.5.6 6 | -------------------------------------------------------------------------------- /9.2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | -------------------------------------------------------------------------------- /9.2/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | type Session struct { 14 | Stdin io.Reader 15 | Stdout, Stderr io.Writer 16 | DryRun bool 17 | } 18 | 19 | func NewSession(stdin io.Reader, stdout, stderr io.Writer) *Session { 20 | return &Session{ 21 | Stdin: stdin, 22 | Stdout: stdout, 23 | Stderr: stderr, 24 | DryRun: false, 25 | } 26 | } 27 | 28 | func (s *Session) Run() { 29 | input := bufio.NewReader(s.Stdin) 30 | for { 31 | fmt.Fprintf(s.Stdout, "> ") 32 | line, err := input.ReadString('\n') 33 | if err != nil { 34 | fmt.Fprintln(s.Stdout, "\nBe seeing you!") 35 | break 36 | } 37 | cmd, err := CmdFromString(line) 38 | if err != nil { 39 | continue 40 | } 41 | if s.DryRun { 42 | fmt.Fprintf(s.Stdout, "%s", line) 43 | continue 44 | } 45 | output, err := cmd.CombinedOutput() 46 | if err != nil { 47 | fmt.Fprintln(s.Stderr, "error:", err) 48 | } 49 | fmt.Fprintf(s.Stdout, "%s", output) 50 | } 51 | } 52 | 53 | func CmdFromString(cmdLine string) (*exec.Cmd, error) { 54 | args := strings.Fields(cmdLine) 55 | if len(args) < 1 { 56 | return nil, errors.New("empty input") 57 | } 58 | return exec.Command(args[0], args[1:]...), nil 59 | } 60 | 61 | func RunCLI() { 62 | session := NewSession(os.Stdin, os.Stdout, os.Stderr) 63 | session.Run() 64 | } 65 | -------------------------------------------------------------------------------- /9.2/shell_test.go: -------------------------------------------------------------------------------- 1 | package shell_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "shell" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestCmdFromStringErrorsOnEmptyInput(t *testing.T) { 15 | t.Parallel() 16 | _, err := shell.CmdFromString("") 17 | if err == nil { 18 | t.Fatal("want error on empty input, got nil") 19 | } 20 | } 21 | 22 | func TestCmdFromString(t *testing.T) { 23 | t.Parallel() 24 | cmd, err := shell.CmdFromString("/bin/ls -l main.go\n") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | args := []string{"/bin/ls", "-l", "main.go"} 29 | got := cmd.Args 30 | if !cmp.Equal(args, got) { 31 | t.Error(cmp.Diff(args, got)) 32 | } 33 | } 34 | 35 | func TestNewSession(t *testing.T) { 36 | t.Parallel() 37 | stdin := os.Stdin 38 | stdout := os.Stdout 39 | stderr := os.Stderr 40 | want := shell.Session{ 41 | Stdin: stdin, 42 | Stdout: stdout, 43 | Stderr: stderr, 44 | } 45 | got := *shell.NewSession(stdin, stdout, stderr) 46 | if want != got { 47 | t.Errorf("want %#v, got %#v", want, got) 48 | } 49 | } 50 | 51 | func TestRun(t *testing.T) { 52 | t.Parallel() 53 | stdin := strings.NewReader("echo hello\n\n") 54 | stdout := &bytes.Buffer{} 55 | session := shell.NewSession(stdin, stdout, io.Discard) 56 | session.DryRun = true 57 | session.Run() 58 | want := "> echo hello\n> > \nBe seeing you!\n" 59 | got := stdout.String() 60 | if !cmp.Equal(want, got) { 61 | t.Error(cmp.Diff(want, got)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 John Arundel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Power of Go: Tools 2 | 3 | [![](img/cover.png)](https://bitfieldconsulting.com/books/tools) 4 | 5 | This repository contains the code examples for an older edition of the book [The Power of Go: Tools](https://bitfieldconsulting.com/books/tools), by John Arundel. For the latest edition, please use [`github.com/bitfield/tpg-tools2`](https://github.com/bitfield/tpg-tools2). 6 | -------------------------------------------------------------------------------- /img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfield/tpg-tools/eb425d3251f4cb2c55c0d07658123433040edd4d/img/cover.png -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for file in [0-9]*; do 4 | if [ -d "$file" ] ; then 5 | echo Testing $file... 6 | cd $file 7 | gotestdox 8 | cd .. 9 | fi 10 | done 11 | --------------------------------------------------------------------------------