aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-05-14 15:06:31 -0400
committerSam Anthony <sam@samanthony.xyz>2026-05-14 15:06:31 -0400
commit8b39d27eea85691cc45e7d6798060e110e627806 (patch)
tree23ff6f39bdd0c4881de048ff300670ccd8824883 /cmd
parentb2fc5a8c62b71d52d167e992c42fa6630cfca9b3 (diff)
downloadlulu-8b39d27eea85691cc45e7d6798060e110e627806.zip
cli: refactor flag handling
Diffstat (limited to 'cmd')
-rw-r--r--cmd/lulu/bool.go39
-rw-r--r--cmd/lulu/duration.go29
-rw-r--r--cmd/lulu/flag.go209
-rw-r--r--cmd/lulu/main.go105
-rw-r--r--cmd/lulu/mfg.go74
-rw-r--r--cmd/lulu/pkgid.go80
-rw-r--r--cmd/lulu/text.go37
-rw-r--r--cmd/lulu/uint.go30
-rw-r--r--cmd/lulu/url.go27
9 files changed, 383 insertions, 247 deletions
diff --git a/cmd/lulu/bool.go b/cmd/lulu/bool.go
new file mode 100644
index 0000000..20b1f2c
--- /dev/null
+++ b/cmd/lulu/bool.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "strconv"
+)
+
+type boolFlag interface {
+ flag.Value
+ IsBoolFlag() bool
+}
+
+// boolValue implements flag.Value.
+type boolValue struct {
+ p *bool
+ isSet bool
+}
+
+func (v boolValue) String() string {
+ if v.p != nil {
+ return fmt.Sprint(*v.p)
+ }
+ return ""
+}
+
+func (v *boolValue) Set(s string) error {
+ b, err := strconv.ParseBool(s)
+ if err != nil {
+ return err
+ }
+ *v.p = b
+ v.isSet = true
+ return nil
+}
+
+func (v boolValue) IsSet() bool { return v.isSet }
+
+func (v boolValue) IsBoolFlag() bool { return true }
diff --git a/cmd/lulu/duration.go b/cmd/lulu/duration.go
new file mode 100644
index 0000000..a9aa375
--- /dev/null
+++ b/cmd/lulu/duration.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "time"
+)
+
+type durationValue struct {
+ p *time.Duration
+ isSet bool
+}
+
+func (v durationValue) String() string {
+ if v.p != nil {
+ return v.p.String()
+ }
+ return ""
+}
+
+func (v *durationValue) Set(s string) error {
+ d, err := time.ParseDuration(s)
+ if err != nil {
+ return err
+ }
+ *v.p = d
+ v.isSet = true
+ return nil
+}
+
+func (v durationValue) IsSet() bool { return v.isSet }
diff --git a/cmd/lulu/flag.go b/cmd/lulu/flag.go
index 0103dfc..a6592d7 100644
--- a/cmd/lulu/flag.go
+++ b/cmd/lulu/flag.go
@@ -1,24 +1,37 @@
package main
import (
- "encoding"
+ "cmp"
"flag"
"fmt"
- "net/url"
+ "io"
+ "maps"
"os"
- "strconv"
+ "slices"
"strings"
+ "time"
"unicode"
+
+ "git.samanthony.xyz/lulu"
)
-func nPagesFlag(nPages *uint) Flag {
- return Flag{&uintValue{p: nPages}, "n", "Number of interior pages"}
+func pkgIdFlag(p *lulu.PkgId, required bool) Flag {
+ return Flag{&pkgIdValue{p, false}, "mfg", fmt.Sprintf("Manufacturing options (%T)", *p), required}
+}
+
+func nPagesFlag(p *uint, required bool) Flag {
+ return Flag{&uintValue{p: p}, "n", "Number of interior pages", required}
+}
+
+func timeoutFlag(p *time.Duration, required bool) Flag {
+ return Flag{&durationValue{p, false}, "t", "Timeout", required}
}
type Flag struct {
- val FlagValue
- name string
- usage string
+ val FlagValue
+ name string
+ usage string
+ required bool
}
type FlagValue interface {
@@ -34,7 +47,7 @@ func (f Flag) Synopsis() string {
return s
}
-func (f Flag) AddTo(fs *flag.FlagSet) {
+func (f Flag) AddTo(fs *FlagSet) {
fs.Var(f.val, f.name, f.usage)
}
@@ -42,135 +55,108 @@ func (f Flag) IsSet() bool {
return f.val.IsSet()
}
-func (f Flag) MustBeSet(fs *flag.FlagSet) {
+func (f Flag) MustBeSet(fs *FlagSet) {
if !f.IsSet() {
errMissingFlag(fs, f)
}
}
-type boolFlag interface {
- flag.Value
- IsBoolFlag() bool
-}
-
-// boolValue implements flag.Value.
-type boolValue struct {
- p *bool
- isSet bool
-}
-
-func (v boolValue) String() string {
- if v.p != nil {
- return fmt.Sprint(*v.p)
+func (f Flag) MustNotBeSetWith(fs *FlagSet, other Flag) {
+ if f.IsSet() && other.IsSet() {
+ lg.Printf("lulu: %s -%s is incompatible with -%s\n", fs.Name(), f.name, other.name)
+ fs.Usage()
+ os.Exit(1)
}
- return ""
}
-func (v *boolValue) Set(s string) error {
- b, err := strconv.ParseBool(s)
- if err != nil {
- return err
- }
- *v.p = b
- v.isSet = true
- return nil
+type FlagDataType struct {
+ name string
+ format string
+ subtypes []FlagDataType
}
-func (v boolValue) IsSet() bool { return v.isSet }
-
-func (v boolValue) IsBoolFlag() bool { return true }
-
-type uintValue struct {
- p *uint
- isSet bool
+type FlagDataTyper interface {
+ DataType() FlagDataType
}
-func (v uintValue) String() string {
- if v.p != nil {
- return fmt.Sprint(*v.p)
- }
- return ""
+type FlagSet struct {
+ *flag.FlagSet
+ flags []Flag
}
-func (v *uintValue) Set(s string) error {
- u, err := strconv.ParseUint(s, 10, 32)
- if err != nil {
- return err
+func newFlagSet(name string, flags []Flag, synopses ...string) *FlagSet {
+ fs := &FlagSet{
+ flag.NewFlagSet(name, flag.ExitOnError),
+ flags,
}
- *v.p = uint(u)
- v.isSet = true
- return nil
-}
-
-func (v uintValue) IsSet() bool { return v.isSet }
-
-// textValue implements flag.Value for string types that implement
-// TextUnmarshaler.
-type textValue[T ~string, PT interface {
- *T
- encoding.TextUnmarshaler
-}] struct {
- p PT
- isSet bool
-}
-
-func newTextValue[T ~string, PT interface {
- *T
- encoding.TextUnmarshaler
-}](p PT) *textValue[T, PT] {
- return &textValue[T, PT]{p: p}
-}
-
-func (v textValue[T, PT]) String() string {
- if v.p != nil {
- return string(*(*T)(v.p))
+ for _, f := range flags {
+ fs.Var(f.val, f.name, f.usage)
}
- return ""
-}
-
-func (v *textValue[T, PT]) Set(s string) error {
- if err := v.p.UnmarshalText([]byte(s)); err != nil {
- return err
+ fs.Usage = func() {
+ fmt.Fprintf(fs.Output(), "Usage of %s:\n", name)
+ for _, s := range synopses {
+ fmt.Fprintf(fs.Output(), " %s\n", strings.TrimSpace(s))
+ }
+ if len(synopses) > 0 {
+ fmt.Fprintf(fs.Output(), "Flags:\n")
+ }
+ fs.PrintDefaults()
+ if dts := fs.DataTypes(); len(dts) > 0 {
+ fmt.Fprintf(fs.Output(), "Data Types:\n")
+ printDataTypes(fs.Output(), dts)
+ }
}
- v.isSet = true
- return nil
+ return fs
}
-func (v textValue[T, PT]) IsSet() bool { return v.isSet }
-
-type urlValue struct {
- p *url.URL
- isSet bool
+func printDataTypes(w io.Writer, dts []FlagDataType) {
+ for _, dt := range dts {
+ fmt.Fprintf(w, " %s\t%s\n", dt.name, dt.format)
+ }
}
-func (v urlValue) String() string {
- if v.p != nil {
- return v.p.String()
+func (fs *FlagSet) DataTypes() []FlagDataType {
+ types := make(map[string]FlagDataType)
+ var insert func(dt FlagDataType)
+ insert = func(dt FlagDataType) {
+ types[dt.name] = dt
+ for _, subt := range dt.subtypes {
+ insert(subt)
+ }
+ }
+ for _, f := range fs.flags {
+ if dt, ok := f.val.(FlagDataTyper); ok {
+ insert(dt.DataType())
+ }
}
- return ""
+ return sortMap(types)
}
-func (v *urlValue) Set(s string) error {
- u, err := url.Parse(s)
- if err != nil {
- return err
+func sortMap[K cmp.Ordered, V any](m map[K]V) []V {
+ s := make([]V, 0, len(m))
+ for _, k := range slices.Sorted(maps.Keys(m)) {
+ s = append(s, m[k])
}
- *v.p = *u
- v.isSet = true
- return nil
+ return s
}
-func (v urlValue) IsSet() bool { return v.isSet }
+func (fs *FlagSet) Add(f Flag) {
+ fs.flags = append(fs.flags, f)
+ fs.Var(f.val, f.name, f.usage)
+}
-func mutexFlags(fs *flag.FlagSet, a, b Flag) {
- if a.IsSet() && b.IsSet() {
- lg.Printf("lulu: %s: -%s is incompatible with -%s\n", fs.Name(), a.name, b.name)
- fs.Usage()
- os.Exit(1)
+func (fs *FlagSet) Parse(args []string) {
+ if err := fs.FlagSet.Parse(args); err != nil {
+ lg.Fatalf("lulu: %v\n", err)
+ }
+ for _, f := range fs.flags {
+ if f.required {
+ f.MustBeSet(fs)
+ }
}
}
-func errMissingFlag(fs *flag.FlagSet, f Flag) {
+func errMissingFlag(fs *FlagSet, f Flag) {
lg.Printf("lulu %s: missing -%s flag\n", fs.Name(), f.name)
fs.Usage()
os.Exit(1)
@@ -184,3 +170,12 @@ func firstWord(s string) string {
}
return s
}
+
+func typeName(v any) string {
+ s := fmt.Sprintf("%T", v)
+ parts := strings.Split(s, ".")
+ if len(parts) > 1 {
+ return parts[len(parts)-1]
+ }
+ return parts[0]
+}
diff --git a/cmd/lulu/main.go b/cmd/lulu/main.go
index 06d5ad4..7cf6bb3 100644
--- a/cmd/lulu/main.go
+++ b/cmd/lulu/main.go
@@ -18,7 +18,7 @@ var lg = log.New(os.Stderr, "", 0)
type command struct {
longhand, shorthand string
- f func(clnt *lulu.Client, args []string)
+ f func(name string, clnt *lulu.Client, args []string)
usage string
}
@@ -94,7 +94,7 @@ func main() {
for _, cmd := range commands {
switch cmdStr {
case cmd.longhand, cmd.shorthand:
- cmd.f(clnt, subArgs)
+ cmd.f(cmd.longhand, clnt, subArgs)
return
}
}
@@ -113,36 +113,27 @@ func flagBoolFunc(name, usage string, f func()) {
})
}
-func validateInterior(clnt *lulu.Client, args []string) {
+func validateInterior(name string, clnt *lulu.Client, args []string) {
var (
basic bool
url url.URL
- mfg = newMfgFlagSet()
+ mfg lulu.PkgId
timeout time.Duration
)
- basicFlag := Flag{&boolValue{p: &basic}, "basic", "Basic validation without pod_package_id manufacturing options"}
- urlFlag := Flag{&urlValue{p: &url}, "url", "URL of interior file for Lulu to download"}
-
- fs := flag.NewFlagSet("validate-interior", flag.ExitOnError)
- basicFlag.AddTo(fs)
- urlFlag.AddTo(fs)
- mfg.AddTo(fs)
- fs.DurationVar(&timeout, "t", defaultTimeout, "Timeout")
-
- fs.Usage = func() {
- out := fs.Output()
- name := "validate-interior"
- fmt.Fprintf(out, "Usage of %s:\n", name)
- fmt.Fprintf(out, " %s %s %s [flags]\n", name, urlFlag.Synopsis(), mfg.Synopsis())
- fmt.Fprintf(out, " %s %s %s [flags]\n", name, basicFlag.Synopsis(), urlFlag.Synopsis())
- fmt.Fprintf(out, "Flags:\n")
- fs.PrintDefaults()
- }
-
- if err := fs.Parse(args); err != nil {
- lg.Fatalf("lulu: %v\n", err)
- }
+ basicFlag := Flag{&boolValue{p: &basic}, "basic", "Basic validation without pod_package_id manufacturing options", false}
+ urlFlag := Flag{&urlValue{p: &url}, "url", "URL of interior file for Lulu to download", true}
+ mfgFlag := pkgIdFlag(&mfg, false)
+ fs := newFlagSet(name,
+ []Flag{
+ basicFlag,
+ urlFlag,
+ mfgFlag,
+ timeoutFlag(&timeout, false),
+ },
+ fmt.Sprintf("%s %s %s [flags]\n", name, urlFlag.Synopsis(), mfgFlag.Synopsis()),
+ fmt.Sprintf(" %s %s %s [flags]\n", name, basicFlag.Synopsis(), urlFlag.Synopsis()))
+ fs.Parse(args)
var val lulu.InteriorValidation
var err error
@@ -150,14 +141,12 @@ func validateInterior(clnt *lulu.Client, args []string) {
defer cancel()
if basic {
urlFlag.MustBeSet(fs)
- for _, f := range mfg.flags {
- mutexFlags(fs, f, basicFlag)
- }
+ mfgFlag.MustNotBeSetWith(fs, basicFlag)
val, err = clnt.ValidateInteriorBasic(ctx, url.String())
} else {
urlFlag.MustBeSet(fs)
- mfg.MustBeSet(fs)
- val, err = clnt.ValidateInterior(ctx, url.String(), mfg.PkgId())
+ mfgFlag.MustBeSet(fs)
+ val, err = clnt.ValidateInterior(ctx, url.String(), mfg)
}
if err != nil {
lg.Fatal(err)
@@ -165,60 +154,44 @@ func validateInterior(clnt *lulu.Client, args []string) {
fmt.Println(val) // TODO: output format?
}
-func validateCover(clnt *lulu.Client, args []string) {
+func validateCover(name string, clnt *lulu.Client, args []string) {
var (
url url.URL
- mfg = newMfgFlagSet()
+ mfg lulu.PkgId
nPages uint
timeout time.Duration
)
-
- urlFlag := Flag{&urlValue{p: &url}, "url", "URL of cover file for Lulu to download"}
- nPagesFlag := nPagesFlag(&nPages)
-
- fs := flag.NewFlagSet("validate-cover", flag.ExitOnError)
- urlFlag.AddTo(fs)
- mfg.AddTo(fs)
- nPagesFlag.AddTo(fs)
- fs.DurationVar(&timeout, "t", defaultTimeout, "Timeout")
-
- if err := fs.Parse(args); err != nil {
- lg.Fatalf("lulu: %v\n", err)
- }
- urlFlag.MustBeSet(fs)
- mfg.MustBeSet(fs)
- nPagesFlag.MustBeSet(fs)
+ fs := newFlagSet(name, []Flag{
+ Flag{&urlValue{p: &url}, "url", "URL of cover file for Lulu to download", true},
+ pkgIdFlag(&mfg, true),
+ nPagesFlag(&nPages, true),
+ timeoutFlag(&timeout, false),
+ })
+ fs.Parse(args)
ctx, cancel := context.WithTimeout(clnt.Context(), timeout)
defer cancel()
- val, err := clnt.ValidateCover(ctx, url.String(), mfg.PkgId(), nPages)
+ val, err := clnt.ValidateCover(ctx, url.String(), mfg, nPages)
if err != nil {
lg.Fatal(err)
}
fmt.Println(val) // TODO: output format?
}
-func coverDimensions(clnt *lulu.Client, args []string) {
+func coverDimensions(name string, clnt *lulu.Client, args []string) {
var (
- mfg = newMfgFlagSet()
+ mfg lulu.PkgId
nPages uint
unit = lulu.Points
)
- nPagesFlag := nPagesFlag(&nPages)
- unitFlag := Flag{newTextValue(&unit), "u", fmt.Sprintf("Unit of measurement %v", lulu.UnitValues())}
-
- fs := flag.NewFlagSet("cover-dimensions", flag.ExitOnError)
- mfg.AddTo(fs)
- nPagesFlag.AddTo(fs)
- unitFlag.AddTo(fs)
-
- if err := fs.Parse(args); err != nil {
- lg.Fatalf("lulu: %v\n", err)
- }
- mfg.MustBeSet(fs)
- nPagesFlag.MustBeSet(fs)
+ fs := newFlagSet(name, []Flag{
+ pkgIdFlag(&mfg, true),
+ nPagesFlag(&nPages, true),
+ Flag{newTextValue(&unit), "u", fmt.Sprintf("Unit of measurement %v", lulu.UnitValues()), false},
+ })
+ fs.Parse(args)
- dims, err := clnt.CoverDimensions(mfg.PkgId(), nPages, unit)
+ dims, err := clnt.CoverDimensions(mfg, nPages, unit)
if err != nil {
lg.Fatal(err)
}
diff --git a/cmd/lulu/mfg.go b/cmd/lulu/mfg.go
deleted file mode 100644
index c94c42a..0000000
--- a/cmd/lulu/mfg.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "strings"
-
- "git.samanthony.xyz/lulu"
-)
-
-// mfgFlags is a set of flags that specify a PkgId.
-type mfgFlagSet struct {
- size lulu.TrimSize
- color lulu.ColorType
- quality lulu.Quality
- binding lulu.Binding
- paper lulu.Paper
- finish lulu.Finish
- linen lulu.Linen
- foil lulu.Foil
-
- flags []Flag
-}
-
-func newMfgFlagSet() *mfgFlagSet {
- mfg := new(mfgFlagSet)
- mfg.flags = []Flag{
- {newTextValue(&mfg.size), "s", fmt.Sprintf("Trim size %v", lulu.TrimSizeValues())},
- {newTextValue(&mfg.color), "c", fmt.Sprintf("Color mode %v", lulu.ColorTypeValues())},
- {newTextValue(&mfg.quality), "q", fmt.Sprintf("Quality %v", lulu.QualityValues())},
- {newTextValue(&mfg.binding), "b", fmt.Sprintf("Binding %v", lulu.BindingValues())},
- {newTextValue(&mfg.paper), "p", fmt.Sprintf("Paper %v", lulu.PaperValues())},
- {newTextValue(&mfg.finish), "f", fmt.Sprintf("Cover finish %v", lulu.FinishValues())},
- {newTextValue(&mfg.linen), "l", fmt.Sprintf("Linen wrap color %v", lulu.LinenValues())},
- {newTextValue(&mfg.foil), "o", fmt.Sprintf("Foil color %v", lulu.FoilValues())},
- }
- return mfg
-}
-
-func (m mfgFlagSet) PkgId() lulu.PkgId {
- return lulu.PkgId{
- m.size,
- m.color,
- m.quality,
- m.binding,
- m.paper,
- m.finish,
- m.linen,
- m.foil,
- }
-}
-
-func (m mfgFlagSet) Synopsis() string {
- s := new(strings.Builder)
- for i, f := range m.flags {
- if i != 0 {
- fmt.Fprint(s, " ")
- }
- fmt.Fprint(s, f.Synopsis())
- }
- return s.String()
-}
-
-func (m mfgFlagSet) AddTo(fs *flag.FlagSet) {
- for _, f := range m.flags {
- f.AddTo(fs)
- }
-}
-
-func (m mfgFlagSet) MustBeSet(fs *flag.FlagSet) {
- for _, f := range m.flags {
- f.MustBeSet(fs)
- }
-}
diff --git a/cmd/lulu/pkgid.go b/cmd/lulu/pkgid.go
new file mode 100644
index 0000000..8681bfd
--- /dev/null
+++ b/cmd/lulu/pkgid.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "fmt"
+
+ "git.samanthony.xyz/lulu"
+)
+
+type pkgIdValue struct {
+ p *lulu.PkgId
+ isSet bool
+}
+
+func (v pkgIdValue) String() string {
+ if v.p != nil {
+ return v.p.String()
+ }
+ return ""
+}
+
+func (v *pkgIdValue) Set(s string) error {
+ if err := v.p.UnmarshalText([]byte(s)); err != nil {
+ return err
+ }
+ v.isSet = true
+ return nil
+}
+
+func (v pkgIdValue) IsSet() bool { return v.isSet }
+
+func (v pkgIdValue) DataType() FlagDataType {
+ return FlagDataType{
+ typeName(*v.p),
+ fmt.Sprintf("%s.%s.%s.%s.%s.%s%s%s",
+ typeName(v.p.TrimSize),
+ typeName(v.p.ColorType),
+ typeName(v.p.Quality),
+ typeName(v.p.Binding),
+ typeName(v.p.Paper),
+ typeName(v.p.Finish),
+ typeName(v.p.Linen),
+ typeName(v.p.Foil),
+ ),
+ []FlagDataType{
+ {
+ typeName(v.p.TrimSize),
+ fmt.Sprint(lulu.TrimSizeValues()),
+ nil,
+ }, {
+ typeName(v.p.ColorType),
+ fmt.Sprint(lulu.ColorTypeValues()),
+ nil,
+ }, {
+ typeName(v.p.Quality),
+ fmt.Sprint(lulu.QualityValues()),
+ nil,
+ }, {
+ typeName(v.p.Binding),
+ fmt.Sprint(lulu.BindingValues()),
+ nil,
+ }, {
+ typeName(v.p.Paper),
+ fmt.Sprint(lulu.PaperValues()),
+ nil,
+ }, {
+ typeName(v.p.Finish),
+ fmt.Sprint(lulu.FinishValues()),
+ nil,
+ }, {
+ typeName(v.p.Linen),
+ fmt.Sprint(lulu.LinenValues()),
+ nil,
+ }, {
+ typeName(v.p.Foil),
+ fmt.Sprint(lulu.FoilValues()),
+ nil,
+ },
+ },
+ }
+}
diff --git a/cmd/lulu/text.go b/cmd/lulu/text.go
new file mode 100644
index 0000000..ee8d7d7
--- /dev/null
+++ b/cmd/lulu/text.go
@@ -0,0 +1,37 @@
+package main
+
+import "encoding"
+
+// textValue implements flag.Value for string types that implement
+// TextUnmarshaler.
+type textValue[T ~string, PT interface {
+ *T
+ encoding.TextUnmarshaler
+}] struct {
+ p PT
+ isSet bool
+}
+
+func newTextValue[T ~string, PT interface {
+ *T
+ encoding.TextUnmarshaler
+}](p PT) *textValue[T, PT] {
+ return &textValue[T, PT]{p: p}
+}
+
+func (v textValue[T, PT]) String() string {
+ if v.p != nil {
+ return string(*(*T)(v.p))
+ }
+ return ""
+}
+
+func (v *textValue[T, PT]) Set(s string) error {
+ if err := v.p.UnmarshalText([]byte(s)); err != nil {
+ return err
+ }
+ v.isSet = true
+ return nil
+}
+
+func (v textValue[T, PT]) IsSet() bool { return v.isSet }
diff --git a/cmd/lulu/uint.go b/cmd/lulu/uint.go
new file mode 100644
index 0000000..91b958b
--- /dev/null
+++ b/cmd/lulu/uint.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "fmt"
+ "strconv"
+)
+
+type uintValue struct {
+ p *uint
+ isSet bool
+}
+
+func (v uintValue) String() string {
+ if v.p != nil {
+ return fmt.Sprint(*v.p)
+ }
+ return ""
+}
+
+func (v *uintValue) Set(s string) error {
+ u, err := strconv.ParseUint(s, 10, 32)
+ if err != nil {
+ return err
+ }
+ *v.p = uint(u)
+ v.isSet = true
+ return nil
+}
+
+func (v uintValue) IsSet() bool { return v.isSet }
diff --git a/cmd/lulu/url.go b/cmd/lulu/url.go
new file mode 100644
index 0000000..1626269
--- /dev/null
+++ b/cmd/lulu/url.go
@@ -0,0 +1,27 @@
+package main
+
+import "net/url"
+
+type urlValue struct {
+ p *url.URL
+ isSet bool
+}
+
+func (v urlValue) String() string {
+ if v.p != nil {
+ return v.p.String()
+ }
+ return ""
+}
+
+func (v *urlValue) Set(s string) error {
+ u, err := url.Parse(s)
+ if err != nil {
+ return err
+ }
+ *v.p = *u
+ v.isSet = true
+ return nil
+}
+
+func (v urlValue) IsSet() bool { return v.isSet }