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  }