github.com/grailbio/base@v0.0.11/config/flag.go (about) 1 // Copyright 2019 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 package config 6 7 import ( 8 "context" 9 "flag" 10 "fmt" 11 "os" 12 "strings" 13 14 "github.com/grailbio/base/backgroundcontext" 15 "github.com/grailbio/base/errors" 16 "github.com/grailbio/base/file" 17 ) 18 19 type ( 20 // flags is an ordered representation of profile flags. Each entry (implementing flagEntry) 21 // is a type of flag, and entry types may be interleaved. They're handled in the order 22 // the user passed them. 23 // 24 // The flags object is wrapped for each entry type, and each wrapper's flag.Value implementation 25 // appends the appropriate entry. 26 flags struct { 27 defaultProfilePath string 28 entries []flagEntry 29 } 30 flagsProfilePaths flags 31 flagsProfileInlines flags 32 flagsSets flags 33 34 flagEntry interface { 35 process(context.Context, *Profile) error 36 } 37 flagEntryProfilePath struct{ string } 38 flagEntryProfileInline struct{ string } 39 flagEntrySet struct{ key, value string } 40 ) 41 42 var ( 43 _ flagEntry = flagEntryProfilePath{} 44 _ flagEntry = flagEntryProfileInline{} 45 _ flagEntry = flagEntrySet{} 46 47 _ flag.Value = (*flagsProfilePaths)(nil) 48 _ flag.Value = (*flagsProfileInlines)(nil) 49 _ flag.Value = (*flagsSets)(nil) 50 ) 51 52 func (e flagEntryProfilePath) process(ctx context.Context, p *Profile) error { 53 return p.loadFile(ctx, e.string) 54 } 55 func (e flagEntryProfileInline) process(_ context.Context, p *Profile) error { 56 return p.Parse(strings.NewReader(e.string)) 57 } 58 func (e flagEntrySet) process(_ context.Context, p *Profile) error { 59 return p.Set(e.key, e.value) 60 } 61 62 func (f *flagsProfilePaths) String() string { return f.defaultProfilePath } 63 func (*flagsProfileInlines) String() string { return "" } 64 func (*flagsSets) String() string { return "" } 65 66 func (f *flagsProfilePaths) Set(s string) error { 67 if s == "" { 68 return errors.New("empty path to profile") 69 } 70 f.entries = append(f.entries, flagEntryProfilePath{s}) 71 return nil 72 } 73 func (f *flagsProfileInlines) Set(s string) error { 74 if s != "" { 75 f.entries = append(f.entries, flagEntryProfileInline{s}) 76 } 77 return nil 78 } 79 func (f *flagsSets) Set(s string) error { 80 elems := strings.SplitN(s, "=", 2+1) // Split an additional part to detect errors. 81 if len(elems) != 2 || elems[0] == "" { 82 return fmt.Errorf("wrong argument format, expected key=value, got %q", s) 83 } 84 f.entries = append(f.entries, flagEntrySet{elems[0], elems[1]}) 85 return nil 86 } 87 88 // RegisterFlags registers a set of flags on the provided FlagSet. 89 // These flags configure the profile when ProcessFlags is called 90 // (after flag parsing). The flags are: 91 // 92 // -profile path 93 // Parses and loads the profile at the given path. This flag may be 94 // repeated, loading each profile in turn. If no -profile flags are 95 // specified, then the provided default path is loaded instead. If 96 // the default path does not exist, it is skipped; other profile loading 97 // errors cause ProcessFlags to return an error. 98 // 99 // -set key=value 100 // Sets the value of the named parameter. See Profile.Set for 101 // details. This flag may be repeated. 102 // 103 // -profileinline text 104 // Parses the argument. This is equivalent to writing the text to a file 105 // and using -profile. 106 // 107 // -profiledump 108 // Writes the profile (after processing the above flags) to standard 109 // error and exits. 110 // 111 // The flag names are prefixed with the provided prefix. 112 func (p *Profile) RegisterFlags(fs *flag.FlagSet, prefix string, defaultProfilePath string) { 113 p.flags.defaultProfilePath = defaultProfilePath 114 fs.Var((*flagsProfilePaths)(&p.flags), prefix+"profile", "load the profile at the provided path; may be repeated") 115 fs.Var((*flagsSets)(&p.flags), prefix+"set", "set a profile parameter; may be repeated") 116 fs.Var((*flagsProfileInlines)(&p.flags), prefix+"profileinline", "parse the profile passed as an argument; may be repeated") 117 fs.BoolVar(&p.flagDump, "profiledump", false, "dump the profile to stderr and exit") 118 } 119 120 // NeedProcessFlags returns true when a call to p.ProcessFlags should 121 // not be delayed -- i.e., the flag values have user-visible side effects. 122 func (p *Profile) NeedProcessFlags() bool { 123 return p.flagDump 124 } 125 126 func (f *flags) hasProfilePathEntry() bool { 127 for _, entry := range f.entries { 128 if _, ok := entry.(flagEntryProfilePath); ok { 129 return true 130 } 131 } 132 return false 133 } 134 135 func (p *Profile) loadFile(ctx context.Context, path string) (err error) { 136 f, err := file.Open(ctx, path) 137 if err != nil { 138 return err 139 } 140 defer errors.CleanUpCtx(ctx, f.Close, &err) 141 return p.Parse(f.Reader(ctx)) 142 } 143 144 // ProcessFlags processes the flags as registered by RegisterFlags, 145 // and is documented by that method. 146 func (p *Profile) ProcessFlags() error { 147 ctx := backgroundcontext.Get() 148 if p.flags.defaultProfilePath != "" && !p.flags.hasProfilePathEntry() { 149 if err := p.loadFile(ctx, p.flags.defaultProfilePath); err != nil { 150 if !errors.Is(errors.NotExist, err) { 151 return err 152 } 153 } 154 } 155 for _, entry := range p.flags.entries { 156 if err := entry.process(ctx, p); err != nil { 157 return err 158 } 159 } 160 if p.flagDump { 161 // TODO(marius): also prune uninstantiable instances? 162 for _, inst := range p.sorted() { 163 if len(inst.params) == 0 && inst.parent == "" { 164 continue 165 } 166 fmt.Fprintln(os.Stderr, inst.SyntaxString(p.docs(inst))) 167 } 168 os.Exit(1) 169 } 170 return nil 171 } 172 173 // RegisterFlags registers the default profile on flag.CommandLine 174 // with the provided prefix. See Profile.RegisterFlags for details. 175 func RegisterFlags(prefix string, defaultProfilePath string) { 176 Application().RegisterFlags(flag.CommandLine, prefix, defaultProfilePath) 177 } 178 179 // ProcessFlags processes the flags as registered by RegisterFlags. 180 func ProcessFlags() error { 181 return Application().ProcessFlags() 182 }