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 }