Skip to content

Commit c135b0f

Browse files
authored
feat: CLI parameters (#3)
Allow configuring what to download with CLI parameters.
1 parent fec08b0 commit c135b0f

6 files changed

Lines changed: 203 additions & 11 deletions

File tree

download/download_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package download_test
33
import (
44
"testing"
55

6-
"github.com/stretchr/testify/require"
7-
86
"github.com/murfffi/getaduck/download"
7+
"github.com/stretchr/testify/require"
98
)
109

1110
func TestDo(t *testing.T) {
12-
res, err := download.Do(download.DefaultSpec())
13-
require.NoError(t, err)
14-
require.FileExists(t, res.OutputFile)
15-
11+
if !testing.Short() {
12+
t.Skip("skipping test that downloads from Github in short mode.")
13+
}
14+
t.Run("default lib", func(t *testing.T) {
15+
res, err := download.Do(download.DefaultSpec())
16+
require.NoError(t, err)
17+
require.FileExists(t, res.OutputFile)
18+
})
19+
// cli is tested e2e in shell/run_test.go . Avoid multiple downloads.
1620
}

internal/enumflag/enumflag.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package enumflag
2+
3+
// Adapted from https://github.com/creachadair/goflags/blob/main/enumflag/flag.go
4+
// under BSD 3-Clause license
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
)
10+
11+
// A Value represents an enumeration of string values. A pointer to a Value
12+
// satisfies the flag.Value interface. Use the Key method to recover the
13+
// currently-selected value of the enumeration.
14+
type Value struct {
15+
keys []string
16+
index int // The selected index in the enumeration
17+
}
18+
19+
// Help concatenates a human-readable string summarizing the legal values of v
20+
// to h, for use in generating a documentation string.
21+
func (v *Value) Help(h string) string {
22+
return fmt.Sprintf("%s (%s)", h, strings.Join(v.keys, "|"))
23+
}
24+
25+
// New returns a *Value for the specified enumerators, where defaultKey is the
26+
// default value and otherKeys are additional options. The index of a selected
27+
// key reflects its position in the order given to this function, so that if:
28+
//
29+
// v := enumflag.New("a", "b", "c", "d")
30+
//
31+
// then the index of "a" is 0, "b" is 1, "c" is 2, "d" is 3. The default key is
32+
// always stored at index 0.
33+
func New(defaultKey string, otherKeys ...string) *Value {
34+
return &Value{keys: append([]string{defaultKey}, otherKeys...)}
35+
}
36+
37+
// Key returns the currently-selected key in the enumeration. The original
38+
// spelling of the selected value is returned, as given to the constructor, not
39+
// the value as parsed.
40+
func (v *Value) Key() string {
41+
if len(v.keys) == 0 {
42+
return "" // BUG: https://github.com/golang/go/issues/16694
43+
}
44+
return v.keys[v.index]
45+
}
46+
47+
// Get satisfies the flag.Getter interface.
48+
// The concrete value is the the string of the current key.
49+
func (v *Value) Get() any { return v.Key() }
50+
51+
// Index returns the currently-selected index in the enumeration.
52+
// The order of keys reflects the original order in which they were passed to
53+
// the constructor, so index 0 is the default value.
54+
func (v *Value) Index() int { return v.index }
55+
56+
// String satisfies part of the flag.Value interface.
57+
func (v *Value) String() string { return fmt.Sprintf("%q", v.Key()) }
58+
59+
// Set satisfies part of the flag.Value interface.
60+
func (v *Value) Set(s string) error {
61+
for i, key := range v.keys {
62+
if strings.EqualFold(s, key) {
63+
v.index = i
64+
return nil
65+
}
66+
}
67+
return fmt.Errorf("expected one of (%s)", strings.Join(v.keys, "|"))
68+
}

internal/enumflag/enumflag_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package enumflag
2+
3+
// Adapted from https://github.com/creachadair/goflags/blob/main/enumflag/flag.go
4+
// under BSD 3-Clause license
5+
6+
import (
7+
"bytes"
8+
"flag"
9+
"io"
10+
"testing"
11+
)
12+
13+
func newFlagSet(name string, w io.Writer) *flag.FlagSet {
14+
fs := flag.NewFlagSet(name, flag.ContinueOnError)
15+
fs.SetOutput(w)
16+
return fs
17+
}
18+
19+
func TestFlagBits(t *testing.T) {
20+
color := New("red", "orange", "yellow", "green", "blue")
21+
22+
const initial = "red"
23+
const flagged = "green"
24+
const flaggedIndex = 3
25+
26+
var buf bytes.Buffer
27+
fs := newFlagSet("color", &buf)
28+
fs.Var(color, "color", color.Help("The color to paint the bike shed"))
29+
fs.PrintDefaults()
30+
t.Logf("Color flag set:\n%s", buf.String())
31+
buf.Reset()
32+
33+
if key := color.Key(); key != initial {
34+
t.Errorf("Initial value for -color: got %q, want %q", key, initial)
35+
}
36+
37+
if err := fs.Parse([]string{"-color", "GREEN"}); err != nil {
38+
t.Fatalf("Argument parsing failed: %v", err)
39+
}
40+
41+
if key := color.Key(); key != flagged {
42+
t.Errorf("Value for -color: got %q, want %q", key, flagged)
43+
}
44+
if idx := color.Index(); idx != flaggedIndex {
45+
t.Errorf("Index for -color: got %d, want %d", idx, flaggedIndex)
46+
}
47+
48+
taste := New("", "sweet", "sour")
49+
fs = newFlagSet("taste", &buf)
50+
fs.Var(taste, "taste", taste.Help("The flavour of the ice cream"))
51+
fs.PrintDefaults()
52+
t.Logf("Taste flag set:\n%s", buf.String())
53+
54+
if err := fs.Parse([]string{"-taste", "crud"}); err == nil {
55+
t.Error("Expected error from bogus flag, but got none")
56+
} else {
57+
t.Logf("Got expected error from bogus -taste: %v", err)
58+
}
59+
}

main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package main
22

33
import (
4+
"flag"
5+
"log"
6+
"os"
7+
48
"github.com/murfffi/getaduck/shell"
59
)
610

711
func main() {
8-
shell.Run()
12+
err := shell.RunArgs(os.Args, flag.ExitOnError)
13+
if err != nil {
14+
log.Fatal(err)
15+
}
916
}

shell/run.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,51 @@
22
package shell
33

44
import (
5+
"flag"
56
"log"
67
"path/filepath"
78

89
"github.com/murfffi/getaduck/download"
10+
"github.com/murfffi/getaduck/internal/enumflag"
911
)
1012

11-
// Run executes getaduck
12-
func Run() {
13-
spec := download.DefaultSpec()
13+
// RunArgs executes getaduck CLI
14+
func RunArgs(args []string, onError flag.ErrorHandling) error {
15+
spec, err := parseSpec(args, onError)
16+
if err != nil {
17+
return err
18+
}
19+
1420
res, err := download.Do(spec)
1521
if err != nil {
16-
log.Fatalf("download failed: %v", err)
22+
return err
1723
}
1824
outFileName := res.OutputFile
1925
absPath, err := filepath.Abs(outFileName)
2026
if err != nil {
2127
absPath = outFileName
2228
}
2329
log.Print("downloaded: ", absPath)
30+
return nil
31+
}
32+
33+
func parseSpec(args []string, onError flag.ErrorHandling) (download.Spec, error) {
34+
fs := flag.NewFlagSet(args[0], onError)
35+
spec := download.DefaultSpec()
36+
37+
// order of args must match download.BinType const order
38+
binType := enumflag.New("lib", "cli")
39+
fs.Var(binType, "type", binType.Help("type of binary to download"))
40+
version := fs.String("version", spec.Version, "DuckDB version")
41+
binOS := fs.String("os", spec.OS, "target OS")
42+
binArch := fs.String("arch", spec.Arch, "target architecture")
43+
if err := fs.Parse(args[1:]); err != nil {
44+
return download.Spec{}, err
45+
}
46+
47+
spec.Type = download.BinType(binType.Index())
48+
spec.Version = *version
49+
spec.OS = *binOS
50+
spec.Arch = *binArch
51+
return spec, nil
2452
}

shell/run_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package shell
2+
3+
import (
4+
"flag"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestRunArgs(t *testing.T) {
11+
if !testing.Short() {
12+
t.Skip("skipping test that downloads from Github in short mode.")
13+
}
14+
t.Run("cli", func(t *testing.T) {
15+
err := RunArgs([]string{"test", "-type", "cli"}, flag.ContinueOnError)
16+
require.NoError(t, err)
17+
})
18+
}
19+
20+
func TestParseSpec(t *testing.T) {
21+
t.Run("version", func(t *testing.T) {
22+
spec, err := parseSpec([]string{"test", "--version", "1.1.0"}, flag.ContinueOnError)
23+
require.NoError(t, err)
24+
require.Equal(t, "1.1.0", spec.Version)
25+
})
26+
}

0 commit comments

Comments
 (0)