github.com/vanadium-archive/go.jiri@v0.0.0-20160715023856-abfb8b131290/profiles/profilescmdline/reader_cmdline.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package profilescmdline provides a command line driver (for v.io/x/lib/cmdline)
     6  // for implementing jiri 'profile' subcommands. The intent is to support
     7  // project specific instances of such profiles for managing software
     8  // dependencies.
     9  package profilescmdline
    10  
    11  import (
    12  	"bytes"
    13  	"flag"
    14  	"fmt"
    15  	"path/filepath"
    16  	"strings"
    17  	"text/template"
    18  
    19  	"v.io/jiri"
    20  	"v.io/jiri/profiles"
    21  	"v.io/jiri/profiles/profilesreader"
    22  	"v.io/x/lib/cmdline"
    23  	"v.io/x/lib/textutil"
    24  )
    25  
    26  // IsFlagSet returns true if the specified flag has been set on
    27  // the command line.
    28  func IsFlagSet(fs *flag.FlagSet, name string) bool {
    29  	found := false
    30  	fs.Visit(func(f *flag.Flag) {
    31  		if f.Name == name {
    32  			found = true
    33  		}
    34  	})
    35  	return found
    36  }
    37  
    38  // NOTE: we use functions to initialize the commands so that we
    39  // can reinitialize them in tests. cmd_test.go contains a 'Reset' function
    40  // that is only available to tests for doing so.
    41  // NOTE: we can't set cmdList.Runner in the initialization loop since runList
    42  // needs to access cmdList.Flags.
    43  var (
    44  	// cmdList represents the "profile list" command.
    45  	cmdList *cmdline.Command
    46  	// cmdEnv represents the "profile env" command.
    47  	cmdEnv *cmdline.Command = newCmdEnv()
    48  )
    49  
    50  func init() {
    51  	cmdList = newCmdList()
    52  	cmdList.Runner = jiri.RunnerFunc(runList)
    53  }
    54  
    55  func newCmdList() *cmdline.Command {
    56  	return &cmdline.Command{
    57  		Name:     "list",
    58  		Short:    "List available or installed profiles",
    59  		Long:     "List available or installed profiles.",
    60  		ArgsName: "[<profiles>]",
    61  		ArgsLong: `<profiles> is a list of profiles to list, defaulting to all
    62  profiles if none are specifically requested. List can also be used
    63  to test for the presence of a specific target for the requested profiles.
    64  If the target is not installed, it will exit with an error.`,
    65  	}
    66  }
    67  
    68  func newCmdEnv() *cmdline.Command {
    69  	// cmdEnv represents the "profile env" command.
    70  	return &cmdline.Command{
    71  		Runner: jiri.RunnerFunc(runEnv),
    72  		Name:   "env",
    73  		Short:  "Display profile environment variables",
    74  		Long: `
    75  List profile specific and target specific environment variables. If the
    76  requested environment variable name ends in = then only the value will
    77  be printed, otherwise both name and value are printed, i.e. CFLAGS="foo" vs
    78  just "foo".
    79  
    80  If no environment variable names are requested then all will be printed
    81  in <name>=<val> format.
    82  `,
    83  		ArgsName: "[<environment variable names>]",
    84  		ArgsLong: "[<environment variable names>] is an optional list of environment variables to display",
    85  	}
    86  }
    87  
    88  // ReaderFlagValues contains the values of the command line flags accepted
    89  // required to configure and use the profiles/Reader package.
    90  type ReaderFlagValues struct {
    91  	// The value of --skip-profiles
    92  	ProfilesMode profilesreader.ProfilesMode
    93  	// The value of --profiles-db
    94  	DBFilename string
    95  	// The value of --profiles
    96  	Profiles string
    97  	// The value of --target and --env
    98  	Target profiles.Target
    99  	// The value of --merge-policies
   100  	MergePolicies profilesreader.MergePolicies
   101  	// The value of -v
   102  	Verbose bool
   103  }
   104  
   105  // listFlagValues contains the flag values expected by the list subcommand
   106  type listFlagValues struct {
   107  	*ReaderFlagValues
   108  	// The value of --info
   109  	info string
   110  }
   111  
   112  // envFlagValues contains the flag values expected by the env subcommand
   113  type envFlagValues struct {
   114  	*ReaderFlagValues
   115  }
   116  
   117  // All flag values are stored in listFlags and envFlags.
   118  var (
   119  	listFlags listFlagValues
   120  	envFlags  envFlagValues
   121  )
   122  
   123  // RegisterDBPathFlag registers the --profiles-db flag with the supplied FlagSet.
   124  func RegisterDBPathFlag(flags *flag.FlagSet, manifest *string, defaultDBPath string) {
   125  	root := jiri.FindRoot()
   126  	flags.StringVar(manifest, "profiles-db", filepath.Join(root, defaultDBPath), "the path, relative to JIRI_ROOT, that contains the profiles database.")
   127  	flags.Lookup("profiles-db").DefValue = filepath.Join("$JIRI_ROOT", defaultDBPath)
   128  }
   129  
   130  // RegisterProfilesFlag registers the --profiles flag
   131  func RegisterProfilesFlag(flags *flag.FlagSet, defaultProfiles string, profiles *string) {
   132  	flags.StringVar(profiles, "profiles", defaultProfiles, "a comma separated list of profiles to use")
   133  }
   134  
   135  // RegisterMergePoliciesFlag registers the --merge-policies flag
   136  func RegisterMergePoliciesFlag(flags *flag.FlagSet, policies *profilesreader.MergePolicies) {
   137  	flags.Var(policies, "merge-policies", "specify policies for merging environment variables")
   138  }
   139  
   140  // RegisterReaderFlags registers the 'reader' flags (see below)
   141  // with the parent command. The values of the flags can be accessed via
   142  // the supplied ReaderFlagValues struct.
   143  // The reader flags are:
   144  //  --skip-profiles
   145  //  --profiles-db
   146  //  --profiles
   147  //  --merge-policies
   148  //  --target and --env
   149  func RegisterReaderFlags(flags *flag.FlagSet, fv *ReaderFlagValues, defaultProfiles, defaultDBPath string) {
   150  	flags.Var(&fv.ProfilesMode, "skip-profiles", "if set, no profiles will be used")
   151  	RegisterDBPathFlag(flags, &fv.DBFilename, defaultDBPath)
   152  	RegisterProfilesFlag(flags, defaultProfiles, &fv.Profiles)
   153  	fv.MergePolicies = profilesreader.JiriMergePolicies()
   154  	RegisterMergePoliciesFlag(flags, &fv.MergePolicies)
   155  	profiles.RegisterTargetAndEnvFlags(flags, &fv.Target)
   156  }
   157  
   158  // RegisterReaderCommandsUsingParent registers the 'reader' flags
   159  // (see RegisterReaderFlags) with the parent command and creates the
   160  // list and env subcommands. The values of the flags can be accessed via
   161  // the supplied ReaderFlagValues struct.
   162  // RegisterReaderCommandsUsingParent results in a command line of the form:
   163  // <parent> <reader-flags> [list|env] <list/env specific commands>
   164  func RegisterReaderCommandsUsingParent(parent *cmdline.Command, fv *ReaderFlagValues, defaultProfiles, defaultDBPath string) {
   165  	envFlags.ReaderFlagValues = fv
   166  	listFlags.ReaderFlagValues = fv
   167  	RegisterReaderFlags(&parent.Flags, fv, defaultProfiles, defaultDBPath)
   168  	RegisterReaderCommands(parent, defaultProfiles, defaultDBPath)
   169  }
   170  
   171  // RegisterReaderCommands registers the list and env subcommands. The
   172  // subcommands will host the 'reader' flags (see RegisterReaderFlags)
   173  // resulting in a command line of the form:
   174  // <parent> [list|env] <reader-flags> <list/env specific specific commands>
   175  func RegisterReaderCommands(parent *cmdline.Command, defaultProfiles, defaultDBPath string) {
   176  	registerListCommand(parent, defaultProfiles, defaultDBPath)
   177  	registerEnvCommand(parent, defaultProfiles, defaultDBPath)
   178  }
   179  
   180  func newReaderFlags() *ReaderFlagValues {
   181  	return &ReaderFlagValues{MergePolicies: profilesreader.JiriMergePolicies()}
   182  }
   183  
   184  // registerListCommand the profiles list subcommand and returns it
   185  // and a struct containing  the values of the command line flags.
   186  func registerListCommand(parent *cmdline.Command, defaultProfiles, defaultDBPath string) {
   187  	parent.Children = append(parent.Children, cmdList)
   188  	if listFlags.ReaderFlagValues == nil {
   189  		listFlags.ReaderFlagValues = newReaderFlags()
   190  		RegisterReaderFlags(&cmdList.Flags, listFlags.ReaderFlagValues, defaultProfiles, defaultDBPath)
   191  	}
   192  	cmdList.Flags.BoolVar(&listFlags.Verbose, "v", false, "print more detailed information")
   193  	cmdList.Flags.StringVar(&listFlags.info, "info", "", infoUsage())
   194  }
   195  
   196  // registerEnvCommand the profiles env subcommand and returns it and a
   197  // struct containing the values of the command line flags.
   198  func registerEnvCommand(parent *cmdline.Command, defaultProfiles, defaultDBPath string) {
   199  	parent.Children = append(parent.Children, cmdEnv)
   200  	if envFlags.ReaderFlagValues == nil {
   201  		envFlags.ReaderFlagValues = newReaderFlags()
   202  		RegisterReaderFlags(&cmdEnv.Flags, envFlags.ReaderFlagValues, defaultProfiles, defaultDBPath)
   203  	}
   204  	cmdEnv.Flags.BoolVar(&envFlags.Verbose, "v", false, "print more detailed information")
   205  }
   206  
   207  func matchingTargets(rd *profilesreader.Reader, profile *profiles.Profile) profiles.Targets {
   208  	var targets profiles.Targets
   209  	if IsFlagSet(cmdList.ParsedFlags, "target") {
   210  		if t := rd.LookupProfileTarget(profile.Name(), listFlags.Target); t != nil {
   211  			targets = profiles.Targets{t}
   212  		}
   213  	} else {
   214  		targets = profile.Targets()
   215  	}
   216  	targets.Sort()
   217  	return targets
   218  }
   219  
   220  func runList(jirix *jiri.X, args []string) error {
   221  	if listFlags.Verbose {
   222  		fmt.Fprintf(jirix.Stdout(), "Profiles Database Path: %s\n", listFlags.DBFilename)
   223  	}
   224  	rd, err := profilesreader.NewReader(jirix, listFlags.ProfilesMode, listFlags.DBFilename)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	profileNames := []string{}
   229  	for _, a := range args {
   230  		if a != "" {
   231  			profileNames = append(profileNames, a)
   232  		}
   233  	}
   234  	if len(args) == 0 {
   235  		if IsFlagSet(cmdList.ParsedFlags, "profiles") {
   236  			profileNames = strings.Split(listFlags.Profiles, ",")
   237  		} else {
   238  			profileNames = rd.ProfileNames()
   239  		}
   240  	}
   241  
   242  	if listFlags.Verbose {
   243  		fmt.Fprintf(jirix.Stdout(), "Installed Profiles: ")
   244  		fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(rd.ProfileNames(), ", "))
   245  		for _, name := range profileNames {
   246  			profile := rd.LookupProfile(name)
   247  			if profile == nil {
   248  				continue
   249  			}
   250  			fmt.Fprintf(jirix.Stdout(), "Profile: %s @ %s\n", profile.Name(), profile.Root())
   251  			for _, target := range matchingTargets(rd, profile) {
   252  				fmt.Fprintf(jirix.Stdout(), "\t%s\n", target.DebugString())
   253  			}
   254  		}
   255  		return nil
   256  	}
   257  	if listFlags.info == "" {
   258  		matchingNames := []string{}
   259  		for _, name := range profileNames {
   260  			profile := rd.LookupProfile(name)
   261  			if profile == nil {
   262  				continue
   263  			}
   264  			if len(matchingTargets(rd, profile)) > 0 {
   265  				matchingNames = append(matchingNames, name)
   266  			}
   267  		}
   268  		if len(matchingNames) > 0 {
   269  			fmt.Fprintln(jirix.Stdout(), strings.Join(matchingNames, ", "))
   270  		} else {
   271  			if IsFlagSet(cmdList.ParsedFlags, "target") {
   272  				return fmt.Errorf("no matching targets for %s", listFlags.Target)
   273  			}
   274  		}
   275  		return nil
   276  	}
   277  	// Handle --info
   278  	found := false
   279  	for _, name := range profileNames {
   280  		profile := rd.LookupProfile(name)
   281  		if profile == nil {
   282  			continue
   283  		}
   284  		targets := matchingTargets(rd, profile)
   285  		out := &bytes.Buffer{}
   286  		printHeader := len(profileNames) > 1 || len(targets) > 1 || len(listFlags.info) == 0
   287  		for _, target := range targets {
   288  			if printHeader {
   289  				out.WriteString(fmtHeader(name, target))
   290  				out.WriteString(" ")
   291  			}
   292  			r, err := fmtInfo(jirix, listFlags.info, rd, profile, target)
   293  			if err != nil {
   294  				return err
   295  			}
   296  			out.WriteString(r)
   297  			if printHeader {
   298  				out.WriteString("\n")
   299  			}
   300  			found = true
   301  		}
   302  		fmt.Fprint(jirix.Stdout(), out.String())
   303  	}
   304  	if !found && IsFlagSet(cmdList.ParsedFlags, "target") {
   305  		return fmt.Errorf("no matching targets for %s", listFlags.Target)
   306  	}
   307  	return nil
   308  }
   309  
   310  func fmtHeader(name string, target *profiles.Target) string {
   311  	if target == nil {
   312  		return name
   313  	}
   314  	return name + " " + target.String()
   315  }
   316  
   317  type listInfo struct {
   318  	SchemaVersion profiles.Version
   319  	DBPath        string
   320  	Target        struct {
   321  		InstallationDir string
   322  		CommandLineEnv  []string
   323  		Env             []string
   324  		Command         string
   325  	}
   326  	Profile struct {
   327  		Root      string
   328  		Name      string
   329  		Installer string
   330  		DBPath    string
   331  	}
   332  }
   333  
   334  func infoUsage() string {
   335  	return `The following fields for use with -info are available:
   336  	SchemaVersion - the version of the profiles implementation.
   337  	DBPath - the path for the profiles database.
   338  	Target.InstallationDir - the installation directory of the requested profile.
   339  	Target.CommandLineEnv - the environment variables specified via the command line when installing this profile target.
   340  	Target.Env - the environment variables computed by the profile installation process for this target.
   341  	Target.Command - a command that can be used to create this profile.
   342  	Note: if no --target is specified then the requested field will be displayed for all targets.
   343  
   344  	Profile.Root - the root directory of the requested profile.
   345  	Profile.Name - the qualified name of the profile.
   346  	Profile.Installer - the name of the profile installer.
   347  	Profile.DBPath - the path to the database file for this profile.
   348  	Note: if no profiles are specified then the requested field will be displayed for all profiles.`
   349  }
   350  
   351  func fmtOutput(jirix *jiri.X, o string) string {
   352  	_, width, err := textutil.TerminalSize()
   353  	if err != nil {
   354  		width = 80
   355  	}
   356  	if len(o) < width {
   357  		return o
   358  	}
   359  	out := &bytes.Buffer{}
   360  	w := textutil.NewUTF8WrapWriter(out, width)
   361  	fmt.Fprint(w, o)
   362  	w.Flush()
   363  	return out.String()
   364  }
   365  
   366  func fmtInfo(jirix *jiri.X, infoFmt string, rd *profilesreader.Reader, profile *profiles.Profile, target *profiles.Target) (string, error) {
   367  	// Populate an instance listInfo
   368  	info := &listInfo{}
   369  	name := profile.Name()
   370  	installer, _ := profiles.SplitProfileName(name)
   371  	info.SchemaVersion = rd.SchemaVersion()
   372  	info.DBPath = rd.Path()
   373  	if target != nil {
   374  		info.Target.InstallationDir = jiri.NewRelPath(target.InstallationDir).Abs(jirix)
   375  		info.Target.CommandLineEnv = target.CommandLineEnv().Vars
   376  		info.Target.Env = target.Env.Vars
   377  		clenv := ""
   378  		if len(info.Target.CommandLineEnv) > 0 {
   379  			clenv = fmt.Sprintf(" --env=\"%s\" ", strings.Join(info.Target.CommandLineEnv, ","))
   380  		}
   381  		if installer != "" {
   382  			info.Target.Command = fmt.Sprintf("jiri profile install --target=%s %s%s", target, clenv, name)
   383  		} else {
   384  			// TODO(cnicolaou): remove this when the transition is complete.
   385  			info.Target.Command = fmt.Sprintf("jiri v23-profile install --target=%s %s%s", target, clenv, name)
   386  		}
   387  	}
   388  	if profile != nil {
   389  		rp := jiri.NewRelPath(profile.Root())
   390  		info.Profile.Root = rp.Abs(jirix)
   391  		info.Profile.Name = name
   392  		info.Profile.Installer = installer
   393  		info.Profile.DBPath = info.DBPath
   394  		if installer != "" {
   395  			info.Profile.DBPath = filepath.Join(info.DBPath, installer)
   396  		}
   397  	}
   398  
   399  	// Use a template to print out any field in our instance of listInfo.
   400  	tmpl, err := template.New("list").Parse("{{ ." + infoFmt + "}}")
   401  	if err != nil {
   402  		return "", err
   403  	}
   404  	out := &bytes.Buffer{}
   405  	if err = tmpl.Execute(out, info); err != nil {
   406  		return "", fmt.Errorf("please specify a supported field:\n%s", infoUsage())
   407  	}
   408  	return out.String(), nil
   409  }
   410  
   411  func runEnv(jirix *jiri.X, args []string) error {
   412  	if len(envFlags.Profiles) == 0 {
   413  		return fmt.Errorf("no profiles were specified using --profiles")
   414  	}
   415  	rd, err := profilesreader.NewReader(jirix, envFlags.ProfilesMode, envFlags.DBFilename)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	profileNames := strings.Split(envFlags.Profiles, ",")
   420  	if err := rd.ValidateRequestedProfilesAndTarget(profileNames, envFlags.Target); err != nil {
   421  		return err
   422  	}
   423  	rd.MergeEnvFromProfiles(envFlags.MergePolicies, envFlags.Target, profileNames...)
   424  	out := fmtVars(rd.ToMap(), args)
   425  	if len(out) > 0 {
   426  		fmt.Fprintln(jirix.Stdout(), out)
   427  	}
   428  	return nil
   429  }
   430  
   431  func expr(k, v string, trimmed bool) string {
   432  	if trimmed {
   433  		return v
   434  	}
   435  	return fmt.Sprintf("%s=%q ", k, v)
   436  }
   437  
   438  func fmtVars(vars map[string]string, args []string) string {
   439  	buf := bytes.Buffer{}
   440  	if len(args) == 0 {
   441  		for k, v := range vars {
   442  			buf.WriteString(fmt.Sprintf("%s=%q ", k, v))
   443  		}
   444  	} else {
   445  		for _, arg := range args {
   446  			name := strings.TrimSuffix(arg, "=")
   447  			trimmed := name != arg
   448  			for k, v := range vars {
   449  				if k == name {
   450  					buf.WriteString(expr(k, v, trimmed))
   451  				}
   452  			}
   453  		}
   454  	}
   455  	return strings.TrimSuffix(buf.String(), " ")
   456  }