v.io/jiri: add policy driven flags for merging environment variables.

Environment variables may originate from the inherited environment
of the process or from profiles. The variables may need to be
combined (e.g. CGO_FLAGS or PATH) or chosen from one source
or other. No single policy has been found to work for the combination
of end user development and jenkins workflows for native and
cross compilation. The intent of this CL is to make it easy
to configure environment variables as needed in as obvious, yet
convenient way, as possible.

Also tidy up v23-profile env, list etc.etc

MultiPart: 1/11

Change-Id: I1342cd2a1c292107ba1027a5b2a2eb611c5f51ae
diff --git a/profiles/commandline/driver.go b/profiles/commandline/driver.go
index 9b53d40..e8cd02f 100644
--- a/profiles/commandline/driver.go
+++ b/profiles/commandline/driver.go
@@ -12,14 +12,14 @@
 	"bytes"
 	"flag"
 	"fmt"
-	"path/filepath"
+	"os"
 	"strings"
+	"text/template"
 
 	"v.io/jiri/profiles"
 	"v.io/jiri/project"
 	"v.io/jiri/tool"
 	"v.io/x/lib/cmdline"
-	"v.io/x/lib/envvar"
 	"v.io/x/lib/textutil"
 )
 
@@ -39,7 +39,6 @@
 		cmdEnv,
 		cmdUninstall,
 		cmdUpdate,
-		cmdInfo,
 		cmdRecreate,
 	},
 }
@@ -122,25 +121,17 @@
 	ArgsLong: "<profiles> is a list of profiles to uninstall.",
 }
 
-// cmdInfo represents the "profile info" command.
-var cmdInfo = &cmdline.Command{
-	Runner:   cmdline.RunnerFunc(runInfo),
-	Name:     "info",
-	Short:    "Display info about the available profiles",
-	Long:     "Display info about the available profiles.",
-	ArgsName: "<profiles>",
-	ArgsLong: "<profiles> is a list of profiles to show info for, if omitted, info is shown for all profiles.",
-}
-
 var (
-	targetFlag       profiles.Target
-	manifestFlag     string
-	showManifestFlag bool
-	profileFlag      string
-	rootDir          string
-	availableFlag    bool
-	verboseFlag      bool
-	allFlag          bool
+	targetFlag        profiles.Target
+	manifestFlag      string
+	showManifestFlag  bool
+	profilesFlag      string
+	rootDir           string
+	availableFlag     bool
+	verboseFlag       bool
+	allFlag           bool
+	infoFlag          string
+	mergePoliciesFlag profiles.MergePolicies
 )
 
 func Main(name string) {
@@ -150,13 +141,13 @@
 
 func Init(defaultManifestFilename string) {
 	targetFlag = profiles.DefaultTarget()
+	mergePoliciesFlag = profiles.JiriMergePolicies()
 
 	var err error
 	rootDir, err = project.JiriRoot()
 	if err != nil {
 		panic(err)
 	}
-	manifest := filepath.Join(rootDir, defaultManifestFilename)
 
 	// Every sub-command accepts: --manifest
 	for _, fs := range []*flag.FlagSet{
@@ -167,32 +158,39 @@
 		&cmdEnv.Flags,
 		&cmdList.Flags,
 		&cmdRecreate.Flags} {
-		profiles.RegisterManifestFlag(fs, &manifestFlag, manifest)
+		profiles.RegisterManifestFlag(fs, &manifestFlag, defaultManifestFilename)
 	}
 
 	// install accepts: --target and, --env.
 	profiles.RegisterTargetAndEnvFlags(&cmdInstall.Flags, &targetFlag)
 
-	// uninstall and env accept: --target,
+	// uninstall, env and list accept: --target,
 	for _, fs := range []*flag.FlagSet{
 		&cmdUninstall.Flags,
-		&cmdEnv.Flags} {
+		&cmdEnv.Flags,
+		&cmdList.Flags} {
 		profiles.RegisterTargetFlag(fs, &targetFlag)
 	}
 
+	// env accepts --profiles and --merge-policies
+	profiles.RegisterProfilesFlag(&cmdEnv.Flags, &profilesFlag)
+	profiles.RegisterMergePoliciesFlag(&cmdEnv.Flags, &mergePoliciesFlag)
+
+	// uninstall, list and env accept: --v
+	for _, fs := range []*flag.FlagSet{
+		&cmdUpdate.Flags,
+		&cmdList.Flags,
+		&cmdEnv.Flags} {
+		fs.BoolVar(&verboseFlag, "v", false, "print more detailed information")
+	}
+
 	// uninstall accept --all-targets but with different defaults.
 	cmdUninstall.Flags.BoolVar(&allFlag, "all-targets", false, "apply to all targets for the specified profile(s)")
 
-	// update accepts --v
-	cmdUpdate.Flags.BoolVar(&verboseFlag, "v", false, "print more detailed information")
-
-	// list accepts --show-manifest, --availabe, --v
+	// list accepts --show-manifest, --available, --dir, --default, --versions
 	cmdList.Flags.BoolVar(&showManifestFlag, "show-manifest", false, "print out the manifest file")
 	cmdList.Flags.BoolVar(&availableFlag, "available", false, "print the list of available profiles")
-	cmdList.Flags.BoolVar(&verboseFlag, "v", false, "print more detailed information")
-
-	// env accepts --profile
-	cmdEnv.Flags.StringVar(&profileFlag, "profile", "", "the profile whose environment is to be displayed")
+	cmdList.Flags.StringVar(&infoFlag, "info", "", infoUsage())
 
 	for _, mgr := range profiles.Managers() {
 		profiles.LookupManager(mgr).AddFlags(&cmdInstall.Flags, profiles.Install)
@@ -219,7 +217,7 @@
 			for _, name := range profiles.Managers() {
 				mgr := profiles.LookupManager(name)
 				vi := mgr.VersionInfo()
-				fmt.Fprintf(ctx.Stdout(), "%s: versions: %s - %s\n", name, vi.Default(), strings.Join(vi.Supported(), " "))
+				fmt.Fprintf(ctx.Stdout(), "%s: versions: %s\n", name, vi)
 			}
 		} else {
 			fmt.Fprintf(ctx.Stdout(), "%s\n", strings.Join(profiles.Managers(), ", "))
@@ -252,14 +250,125 @@
 	} else {
 		for _, name := range availableNames {
 			profile := profiles.LookupProfile(name)
-			for _, target := range profile.Targets() {
-				fmt.Fprintf(ctx.Stdout(), "%s %s\n", name, target)
+			mgr := profiles.LookupManager(name)
+			out := &bytes.Buffer{}
+			var targets profiles.OrderedTargets
+			if targetFlag.IsSet() {
+				targets = append(targets, profiles.LookupProfileTarget(name, targetFlag))
+			} else {
+				targets = profile.Targets()
 			}
+			printHeader := len(availableNames) > 1 || len(targets) > 1 || len(infoFlag) == 0
+			for _, target := range targets {
+				if printHeader {
+					out.WriteString(fmtHeader(name, target))
+					out.WriteString(" ")
+				}
+				r, err := fmtInfo(mgr, profile, target)
+				if err != nil {
+					return err
+				}
+				out.WriteString(r)
+				if printHeader {
+					out.WriteString("\n")
+				}
+			}
+			fmt.Fprint(ctx.Stdout(), fmtOutput(ctx, out.String()))
 		}
 	}
 	return nil
 }
 
+func fmtHeader(name string, target *profiles.Target) string {
+	if target == nil {
+		return name
+	}
+	return name + " " + target.String()
+}
+
+type listInfo struct {
+	SchemaVersion profiles.Version
+	Target        struct {
+		InstallationDir string
+		CommandLineEnv  []string
+		Env             []string
+	}
+	Profile struct {
+		Description    string
+		Root           string
+		DefaultVersion string
+		LatestVersion  string
+		Versions       []string
+	}
+}
+
+func infoUsage() string {
+	return `The following fields for use with --profile-info are available:
+	SchemaVersion - the version of the profiles implementation.
+	Target.InstallationDir - the installation directory of the requested profile.
+	Target.CommandLineEnv - the environment variables specified via the command line when installing this profile target.
+	Target.Env - the environment variables computed by the profile installation process for this target.
+	Note: if no --target is specified then metadata for all targets will be displayed.
+	Profile.Description - description of the requested profile.
+	Profile.Root - the root directory of the requested profile.
+	Profile.Versions - the set of supported versions for this profile.
+	Profile.DefaultVersion - the default version of the requested profile.
+	Profile.LatestVersion - the latest version available for the requested profile.
+	Note: if no profiles are specified then metadata for all profiles will be displayed.`
+}
+
+func fmtOutput(ctx *tool.Context, o string) string {
+	_, width, err := textutil.TerminalSize()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "err: %v\n", err)
+		width = 80
+	}
+	fmt.Fprintf(os.Stderr, "width %d\n", width)
+
+	if len(o) < width {
+		return o
+	}
+	out := &bytes.Buffer{}
+	w := textutil.NewUTF8LineWriter(out, width)
+	fmt.Fprint(w, o)
+	w.Flush()
+	return out.String()
+}
+
+func fmtInfo(mgr profiles.Manager, profile *profiles.Profile, target *profiles.Target) (string, error) {
+	if len(infoFlag) > 0 {
+		info := &listInfo{}
+		info.SchemaVersion = profiles.SchemaVersion()
+		if target != nil {
+			info.Target.InstallationDir = target.InstallationDir
+			info.Target.CommandLineEnv = target.CommandLineEnv().Vars
+			info.Target.Env = target.Env.Vars
+		}
+		if profile != nil {
+			info.Profile.Root = profile.Root
+		}
+		if mgr != nil {
+			info.Profile.Description = mgr.Info()
+			vi := mgr.VersionInfo()
+			if supported := vi.Supported(); len(supported) > 0 {
+				info.Profile.Versions = supported
+				info.Profile.LatestVersion = supported[0]
+			}
+			info.Profile.DefaultVersion = vi.Default()
+		}
+		tmpl, err := template.New("list").Parse("{{ ." + infoFlag + "}}")
+		if err != nil {
+			return "", err
+		}
+		out := &bytes.Buffer{}
+		if err = tmpl.Execute(out, info); err != nil {
+			return "", fmt.Errorf("please specify a supported field:\n%s", infoUsage())
+		}
+		return out.String(), nil
+	}
+	return "", nil
+}
+
 func runRecreate(env *cmdline.Env, args []string) error {
 	ctx := tool.NewContextFromEnv(env)
 	if err := profiles.Read(ctx, manifestFlag); err != nil {
@@ -288,35 +397,27 @@
 	return nil
 }
 
-func runInfo(env *cmdline.Env, args []string) error {
+func runEnv(env *cmdline.Env, args []string) error {
+	if len(profilesFlag) == 0 {
+		return fmt.Errorf("no profiles were specified using --profiles")
+	}
 	ctx := tool.NewContextFromEnv(env)
-	profileNames := args
-	if len(args) == 0 {
-		profileNames = profiles.Managers()
-	}
-	_, width, err := textutil.TerminalSize()
+	ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, manifestFlag)
 	if err != nil {
-		width = 80
+		return err
 	}
-	w := textutil.NewUTF8LineWriter(ctx.Stdout(), width)
-	defer w.Flush()
-	for _, name := range profileNames {
-		mgr := profiles.LookupManager(name)
-		if mgr == nil {
-			return fmt.Errorf("profile %q is not available", name)
-		}
-		fmt.Fprintf(w, "%s: %s\n\n", name, mgr.Info())
+	profileNames := strings.Split(profilesFlag, ",")
+	if err := ch.ValidateRequestedProfilesAndTarget(profileNames, targetFlag); err != nil {
+		return err
+	}
+	ch.MergeEnvFromProfiles(mergePoliciesFlag, targetFlag, profileNames...)
+	out := fmtVars(ch.ToMap(), args)
+	if len(out) > 0 {
+		fmt.Fprintln(ctx.Stdout(), out)
 	}
 	return nil
 }
 
-type mapper func(target *profiles.Target) string
-
-var pseudoVariables = map[string]mapper{
-	"V23_TARGET_INSTALLATION_DIR": func(t *profiles.Target) string { return t.InstallationDir },
-	"V23_TARGET_VERSION":          func(t *profiles.Target) string { return t.Version() },
-}
-
 func expr(k, v string, trimmed bool) string {
 	if trimmed {
 		return v
@@ -324,40 +425,16 @@
 	return fmt.Sprintf("%s=%q ", k, v)
 }
 
-func runEnv(env *cmdline.Env, args []string) error {
-	if len(profileFlag) == 0 {
-		return fmt.Errorf("no profile was specified using --profile")
-	}
-	ctx := tool.NewContextFromEnv(env)
-	if err := profiles.Read(ctx, manifestFlag); err != nil {
-		return fmt.Errorf("Failed to read manifest: %v", err)
-	}
-	profile := profiles.LookupProfile(profileFlag)
-	if profile == nil {
-		return fmt.Errorf("profile %q is not installed", profileFlag)
-	}
-	target := profiles.FindTarget(profile.Targets(), &targetFlag)
-	if target == nil {
-		return fmt.Errorf("target %q is not installed for profile %q", targetFlag, profileFlag)
-	}
-	vars := envvar.SliceToMap(target.Env.Vars)
+func fmtVars(vars map[string]string, args []string) string {
 	buf := bytes.Buffer{}
 	if len(args) == 0 {
 		for k, v := range vars {
 			buf.WriteString(fmt.Sprintf("%s=%q ", k, v))
 		}
-		for k, fn := range pseudoVariables {
-			buf.WriteString(fmt.Sprintf("%s=%q ", k, fn(target)))
-		}
 	} else {
 		for _, arg := range args {
 			name := strings.TrimSuffix(arg, "=")
 			trimmed := name != arg
-			for k, fn := range pseudoVariables {
-				if k == name {
-					buf.WriteString(expr(k, fn(target), trimmed))
-				}
-			}
 			for k, v := range vars {
 				if k == name {
 					buf.WriteString(expr(k, v, trimmed))
@@ -365,8 +442,7 @@
 			}
 		}
 	}
-	fmt.Fprintf(ctx.Stdout(), strings.TrimSuffix(buf.String(), " ")+"\n")
-	return nil
+	return strings.TrimSuffix(buf.String(), " ")
 }
 
 func initCommand(ctx *tool.Context, args []string) error {
@@ -483,6 +559,7 @@
 			names = append(names, name)
 		}
 	}
+	targetFlag.UseCommandLineEnv()
 	for _, name := range args {
 		if p := profiles.LookupProfileTarget(name, targetFlag); p != nil {
 			fmt.Fprintf(ctx.Stdout(), "%v %v is already installed as %v\n", name, targetFlag, p)
@@ -507,7 +584,7 @@
 		return err
 	}
 	if allFlag && targetFlag.IsSet() {
-		return fmt.Errorf("don't specify a target in conjunction with --all")
+		return fmt.Errorf("don't specify a target (%v) in conjunction with --all-targets", targetFlag)
 	}
 	if allFlag {
 		for _, name := range args {
diff --git a/profiles/env.go b/profiles/env.go
index befec74..b848d57 100644
--- a/profiles/env.go
+++ b/profiles/env.go
@@ -5,6 +5,7 @@
 package profiles
 
 import (
+	"bytes"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -124,7 +125,7 @@
 		return nil, err
 	}
 	if profilesMode == UseProfiles && len(filename) > 0 {
-		if err := Read(ctx, filepath.Join(root, filename)); err != nil {
+		if err := Read(ctx, filename); err != nil {
 			return nil, err
 		}
 	}
@@ -161,79 +162,56 @@
 	return ch.legacyMode
 }
 
+// MergeEnv merges the embedded environment with the environment
+// variables provided by the vars parameter according to the policies parameter.
+func (ch *ConfigHelper) MergeEnv(policies map[string]MergePolicy, vars ...[]string) {
+	if ch.legacyMode {
+		return
+	}
+	MergeEnv(policies, ch.Vars, vars...)
+}
+
+// MergeEnvFromProfiles merges the embedded environment with the environment
+// variables stored in the requested profiles. The profiles are those read from
+// the manifest and in addition the 'jiri' profile may be used which refers to
+// the environment variables maintained by the jiri tool itself.
+func (ch *ConfigHelper) MergeEnvFromProfiles(policies map[string]MergePolicy, target Target, profileNames ...string) {
+	if ch.legacyMode {
+		return
+	}
+	envs := [][]string{}
+	for _, profile := range profileNames {
+		var e []string
+		if profile == "jiri" {
+			e = ch.JiriProfile()
+		} else {
+			e = EnvFromProfile(target, profile)
+		}
+		if e == nil {
+			continue
+		}
+		envs = append(envs, e)
+	}
+	MergeEnv(policies, ch.Vars, envs...)
+}
+
 // SkippingProfiles returns true if no profiles are being used.
 func (ch *ConfigHelper) SkippingProfiles() bool {
 	return ch.profilesMode == bool(SkipProfiles)
 }
 
-// CommonConcatVariables returns a map of variables that are commonly
-// used for the concat parameter to SetEnvFromProfilesAndTarget.
-func CommonConcatVariables() map[string]string {
-	return map[string]string{
-		"PATH":         ":",
-		"CCFLAGS":      " ",
-		"CXXFLAGS":     " ",
-		"LDFLAGS":      " ",
-		"CGO_CFLAGS":   " ",
-		"CGO_CXXFLAGS": " ",
-		"CGO_LDFLAGS":  " ",
-	}
-}
-
-// CommonIgnoreVariables returns a map of variables that are commonly
-// used for the ignore parameter to SetEnvFromProfilesAndTarget.
-func CommonIgnoreVariables() map[string]bool {
-	return map[string]bool{
-		"GOPATH": true,
-		"GOARCH": true,
-		"GOOS":   true,
-	}
-}
-
-// SetEnvFromProfiles populates the embedded environment with the environment
-// variables stored in the specified profiles for the specified target if
-// new-style profiles are being used, otherwise it uses compiled in values as per
-// the original profiles implementation.
-// The profiles parameter contains a comma separated list of profile names; if the
-// requested target does not exist for any of these profiles then those profiles
-// will be ignored. The 'concat' parameter includes a map of variable names
-// whose values are to concatenated with any existing ones rather than
-// overwriting them (e.g. CFLAGS for example). The value of the concat map
-// is the separator to use for that environment  variable (e.g. space for
-// CFLAGs or ':' for PATH-like ones).
-func (ch *ConfigHelper) SetEnvFromProfiles(concat map[string]string, ignore map[string]bool, profiles string, target Target) {
-	if ch.profilesMode || ch.legacyMode {
-		return
-	}
-	for _, profile := range strings.Split(profiles, ",") {
-		t := LookupProfileTarget(profile, target)
-		if t == nil {
-			continue
-		}
-		for _, tmp := range t.Env.Vars {
-			k, v := envvar.SplitKeyValue(tmp)
-			if ignore[k] {
-				continue
-			}
-			if sep := concat[k]; len(sep) > 0 {
-				ov := ch.Vars.GetTokens(k, sep)
-				nv := envvar.SplitTokens(v, sep)
-				ch.Vars.SetTokens(k, append(ov, nv...), " ")
-				continue
-			}
-			ch.Vars.Set(k, v)
-		}
-	}
-}
-
 // ValidateRequestProfilesAndTarget checks that the supplied slice of profiles
-// names is supported and that each has the specified target installed taking
-// account if runnin in bootstrap mode or with old-style profiles.
+// names is supported (including the 'jiri' profile) and that each has
+// the specified target installed taking account if running using profiles
+// at all or if using old-style profiles.
 func (ch *ConfigHelper) ValidateRequestedProfilesAndTarget(profileNames []string, target Target) error {
 	if ch.profilesMode || ch.legacyMode {
 		return nil
 	}
 	for _, n := range profileNames {
+		if n == "jiri" {
+			continue
+		}
 		if LookupProfileTarget(n, target) == nil {
 			return fmt.Errorf("%q for %q is not available or not installed, use the \"list\" command to see the installed/available profiles.", target, n)
 		}
@@ -247,25 +225,37 @@
 	ch.SetTokens("PATH", append([]string{path}, existing...), ":")
 }
 
-// SetGoPath computes and sets the GOPATH environment variable based on the
-// current jiri configuration.
-func (ch *ConfigHelper) SetGoPath() {
-	if !ch.legacyMode {
-		ch.pathHelper("GOPATH", ch.root, ch.projects, ch.config.GoWorkspaces(), "")
-	}
+// JiriProfile returns a pseudo profile that is maintained by the Jiri
+// tool itself, this currently consists of the GoPath and VDLPath variables.
+// It will generally be used as the last profile in the set of profiles
+// passed to MergeEnv.
+func (ch *ConfigHelper) JiriProfile() []string {
+	return []string{ch.GoPath(), ch.VDLPath()}
 }
 
-// SetVDLPath computes and sets the VDLPATH environment variable based on the
+// GoPath computes and returns the GOPATH environment variable based on the
 // current jiri configuration.
-func (ch *ConfigHelper) SetVDLPath() {
+func (ch *ConfigHelper) GoPath() string {
 	if !ch.legacyMode {
-		ch.pathHelper("VDLPATH", ch.root, ch.projects, ch.config.VDLWorkspaces(), "src")
+		path := pathHelper(ch.ctx, ch.root, ch.projects, ch.config.GoWorkspaces(), "")
+		return "GOPATH=" + envvar.JoinTokens(path, ":")
 	}
+	return ""
+}
+
+// VDLPath computes and returns the VDLPATH environment variable based on the
+// current jiri configuration.
+func (ch *ConfigHelper) VDLPath() string {
+	if !ch.legacyMode {
+		path := pathHelper(ch.ctx, ch.root, ch.projects, ch.config.VDLWorkspaces(), "src")
+		return "VDLPATH=" + envvar.JoinTokens(path, ":")
+	}
+	return ""
 }
 
 // pathHelper is a utility function for determining paths for project workspaces.
-func (ch *ConfigHelper) pathHelper(name, root string, projects project.Projects, workspaces []string, suffix string) {
-	path := ch.GetTokens(name, ":")
+func pathHelper(ctx *tool.Context, root string, projects project.Projects, workspaces []string, suffix string) []string {
+	path := []string{}
 	for _, workspace := range workspaces {
 		absWorkspace := filepath.Join(root, workspace, suffix)
 		// Only append an entry to the path if the workspace is rooted
@@ -279,47 +269,358 @@
 			// account for Go workspaces that span multiple jiri projects,
 			// such as: $JIRI_ROOT/release/go.
 			if strings.HasPrefix(absWorkspace, project.Path) || strings.HasPrefix(project.Path, absWorkspace) {
-				if _, err := ch.ctx.Run().Stat(filepath.Join(absWorkspace)); err == nil {
+				if _, err := ctx.Run().Stat(filepath.Join(absWorkspace)); err == nil {
 					path = append(path, absWorkspace)
 					break
 				}
 			}
 		}
 	}
-	ch.SetTokens(name, path, ":")
+	return path
 }
 
-// MergeEnv merges vars with the variables in env taking care to concatenate
-// values as per the concat and ignore parameters similarly to SetEnvFromProfiles.
-func MergeEnv(concat map[string]string, ignore map[string]bool, env *envvar.Vars, vars ...[]string) {
+// The environment variables passed to a subprocess are the result
+// of merging those in the processes environment and those from
+// one or more profiles according to the policies defined below.
+// There is a starting environment, nominally called 'base', and one
+// or profile environments. The base environment will typically be that
+// inherited by the running process from its invoking shell. A policy
+// consists of an 'action' and an optional separator to use when concatenating
+// variables.
+type MergePolicy struct {
+	Action    MergeAction
+	Separator string
+}
+
+type MergeAction int
+
+const (
+	// Use the first value encountered
+	First MergeAction = iota
+	// Use the last value encountered.
+	Last
+	// Ignore the variable regardless of where it occurs.
+	Ignore
+	// Append the current value to the values already accumulated.
+	Append
+	// Prepend the current value to the values already accumulated.
+	Prepend
+	// Ignore the value in the base environment, but append in the profiles.
+	IgnoreBaseAndAppend
+	// Ignore the value in the base environment, but prepend in the profiles.
+	IgnoreBaseAndPrepend
+	// Ignore the value in the base environment, but use the first value from profiles.
+	IgnoreBaseAndUseFirst
+	// Ignore the value in the base environment, but use the last value from profiles.
+	IgnoreBaseAndUseLast
+	// Ignore the values in the profiles.
+	IgnoreProfiles
+)
+
+var (
+	// A MergePolicy with a Last action.
+	UseLast = MergePolicy{Action: Last}
+	// A MergePolicy with a First action.
+	UseFirst = MergePolicy{Action: First}
+	// A MergePolicy that ignores the variable, regardless of where it occurs.
+	IgnoreVariable = MergePolicy{Action: Ignore}
+	// A MergePolicy that appends using : as a separator.
+	AppendPath = MergePolicy{Action: Append, Separator: ":"}
+	// A MergePolicy that appends using " " as a separator.
+	AppendFlag = MergePolicy{Action: Append, Separator: " "}
+	// A MergePolicy that prepends using : as a separator.
+	PrependPath = MergePolicy{Action: Prepend, Separator: ":"}
+	// A MergePolicy that prepends using " " as a separator.
+	PrependFlag = MergePolicy{Action: Prepend, Separator: " "}
+	// A MergePolicy that will ignore base, but append across profiles using ':'
+	IgnoreBaseAppendPath = MergePolicy{Action: IgnoreBaseAndAppend, Separator: ":"}
+	// A MergePolicy that will ignore base, but append across profiles using ' '
+	IgnoreBaseAppendFlag = MergePolicy{Action: IgnoreBaseAndAppend, Separator: " "}
+	// A MergePolicy that will ignore base, but prepend across profiles using ':'
+	IgnoreBasePrependPath = MergePolicy{Action: IgnoreBaseAndPrepend, Separator: ":"}
+	// A MergePolicy that will ignore base, but prepend across profiles using ' '
+	IgnoreBasePrependFlag = MergePolicy{Action: IgnoreBaseAndPrepend, Separator: " "}
+	// A MergePolicy that will ignore base, but use the last value from profiles.
+	IgnoreBaseUseFirst = MergePolicy{Action: IgnoreBaseAndUseFirst}
+	// A MergePolicy that will ignore base, but use the last value from profiles.
+	IgnoreBaseUseLast = MergePolicy{Action: IgnoreBaseAndUseLast}
+	// A MergePolicy that will always use the value from base and ignore profiles.
+	UseBaseIgnoreProfiles = MergePolicy{Action: IgnoreProfiles}
+)
+
+// ProfileMergePolicies returns an instance of MergePolicies that containts
+// appropriate default policies for use with MergeEnv from within
+// profile implementations.
+func ProfileMergePolicies() MergePolicies {
+	values := MergePolicies{
+		"PATH":         AppendPath,
+		"CCFLAGS":      AppendFlag,
+		"CXXFLAGS":     AppendFlag,
+		"LDFLAGS":      AppendFlag,
+		"CGO_CFLAGS":   AppendFlag,
+		"CGO_CXXFLAGS": AppendFlag,
+		"CGO_LDFLAGS":  AppendFlag,
+		"GOPATH":       IgnoreBaseAppendPath,
+		"GOARCH":       UseBaseIgnoreProfiles,
+		"GOOS":         UseBaseIgnoreProfiles,
+	}
+	mp := MergePolicies{}
+	for k, v := range values {
+		mp[k] = v
+	}
+	return mp
+}
+
+// JiriMergePolicies returns an instance of MergePolicies that contains
+// appropriate default policies for use with MergeEnv from jiri packages
+// and subcommands such as those used to build go, java etc.
+func JiriMergePolicies() MergePolicies {
+	mp := ProfileMergePolicies()
+	mp["GOPATH"] = PrependPath
+	mp["VDLPATH"] = PrependPath
+	mp["GOARCH"] = UseFirst
+	mp["GOOS"] = UseFirst
+	mp["GOROOT"] = IgnoreBaseUseLast
+	return mp
+}
+
+// MergeEnv merges environment variables in base with those
+// in vars according to the suppled policies.
+func MergeEnv(policies map[string]MergePolicy, base *envvar.Vars, vars ...[]string) {
+	// Remove any variables that have the IgnoreBase policy.
+	for k, _ := range base.ToMap() {
+		switch policies[k].Action {
+		case Ignore, IgnoreBaseAndAppend, IgnoreBaseAndPrepend, IgnoreBaseAndUseFirst, IgnoreBaseAndUseLast:
+			base.Delete(k)
+		}
+	}
 	for _, ev := range vars {
 		for _, tmp := range ev {
 			k, v := envvar.SplitKeyValue(tmp)
-			if ignore[k] {
-				continue
+			policy := policies[k]
+			action := policy.Action
+			switch policy.Action {
+			case IgnoreBaseAndAppend:
+				action = Append
+			case IgnoreBaseAndPrepend:
+				action = Prepend
+			case IgnoreBaseAndUseLast:
+				action = Last
+			case IgnoreBaseAndUseFirst:
+				action = First
 			}
-			if sep := concat[k]; len(sep) > 0 {
-				ov := env.GetTokens(k, sep)
+			switch action {
+			case Ignore, IgnoreProfiles:
+				continue
+			case Append, Prepend:
+				sep := policy.Separator
+				ov := base.GetTokens(k, sep)
 				nv := envvar.SplitTokens(v, sep)
-				env.SetTokens(k, append(ov, nv...), " ")
-				continue
+				if action == Append {
+					base.SetTokens(k, append(ov, nv...), sep)
+				} else {
+					base.SetTokens(k, append(nv, ov...), sep)
+				}
+			case First:
+				if !base.Contains(k) {
+					base.Set(k, v)
+				}
+			case Last:
+				base.Set(k, v)
 			}
-			env.Set(k, v)
 		}
 	}
 }
 
-// MergeEnvFromProfiles merges the environment variables stored in the specified
-// profiles and target with the env parameter. It uses MergeEnv to do so.
-func MergeEnvFromProfiles(concat map[string]string, ignore map[string]bool, env *envvar.Vars, target Target, profileNames ...string) ([]string, error) {
-	vars := [][]string{}
-	for _, name := range profileNames {
-		t := LookupProfileTarget(name, target)
-		if t == nil {
-			return nil, fmt.Errorf("failed to lookup %v --target=%v", name, target)
-		}
-		vars = append(vars, t.Env.Vars)
+// EnvFromProfile obtains the environment variable settings from the specified
+// profile and target. It returns nil if the target and/or profile could not
+// be found.
+func EnvFromProfile(target Target, profileName string) []string {
+	t := LookupProfileTarget(profileName, target)
+	if t == nil {
+		return nil
 	}
-	MergeEnv(concat, ignore, env, vars...)
-	return env.ToSlice(), nil
+	return t.Env.Vars
+}
+
+// WithDefaultVersion returns a copy of the supplied target with its
+// version set to the default (i.e. emtpy string).
+func WithDefaultVersion(target Target) Target {
+	t := &target
+	t.SetVersion("")
+	return target
+}
+
+type MergePolicies map[string]MergePolicy
+
+func (mp *MergePolicy) String() string {
+	switch mp.Action {
+	case First:
+		return "use first"
+	case Last:
+		return "use last"
+	case Append:
+		return "append using '" + mp.Separator + "'"
+	case Prepend:
+		return "prepend using '" + mp.Separator + "'"
+	case IgnoreBaseAndAppend:
+		return "ignore in environment/base, append using '" + mp.Separator + "'"
+	case IgnoreBaseAndPrepend:
+		return "ignore in environment/base, prepend using '" + mp.Separator + "'"
+	case IgnoreBaseAndUseLast:
+		return "ignore in environment/base, use last value from profiles"
+	case IgnoreBaseAndUseFirst:
+		return "ignore in environment/base, use first value from profiles"
+	case IgnoreProfiles:
+		return "ignore in profiles"
+	}
+	return "unrecognised action"
+}
+
+func (mp MergePolicies) Usage() string {
+	return `<var>:<var>|<var>:|+<var>|<var>+|=<var>|<var>=
+<var> - use the first value of <var> encountered, this is the default action.
+<var>* - use the last value of <var> encountered.
+-<var> - ignore the variable, regardless of where it occurs.
+:<var> - append instances of <var> using : as a separator.
+<var>: - prepend instances of <var> using : as a separator.
++<var> - append instances of <var> using space as a separator.
+<var>+ - prepend instances of <var> using space as a separator.
+^:<var> - ignore <var> from the base/inherited environment but append in profiles as per :<var>.
+^<var>: - ignore <var> from the base/inherited environment but prepend in profiles as per <var>:.
+^+<var> - ignore <var> from the base/inherited environment but append in profiles as per +<var>.
+^<var>+ - ignore <var> from the base/inherited environment but prepend in profiles as per <var>+.
+^<var> - ignore <var> from the base/inherited environment but use the first value encountered in profiles.
+^<var>* - ignore <var> from the base/inherited environment but use the last value encountered in profiles.
+<var>^ - ignore <var> from profiles.`
+}
+
+func separator(s string) string {
+	switch s {
+	case ":":
+		return ":"
+	default:
+		return "+"
+	}
+}
+
+// String implements flag.Value. It generates a string that can be used
+// to recreate the MergePolicies value and that can be passed as a parameter
+// to another process.
+func (mp MergePolicies) String() string {
+	buf := bytes.Buffer{}
+	// Ensure a stable order.
+	keys := make([]string, 0, len(mp))
+	for k, _ := range mp {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	for _, k := range keys {
+		v := mp[k]
+		var s string
+		switch v.Action {
+		case First:
+			s = k
+		case Last:
+			s = k + "*"
+		case Append:
+			s = separator(v.Separator) + k
+		case Prepend:
+			s = k + separator(v.Separator)
+		case IgnoreBaseAndAppend:
+			s = "^" + separator(v.Separator) + k
+		case IgnoreBaseAndPrepend:
+			s = "^" + k + separator(v.Separator)
+		case IgnoreBaseAndUseLast:
+			s = "^" + k + "*"
+		case IgnoreBaseAndUseFirst:
+			s = "^" + k
+		case IgnoreProfiles:
+			s = k + "^"
+		}
+		buf.WriteString(s)
+		buf.WriteString(",")
+	}
+	return strings.TrimSuffix(buf.String(), ",")
+}
+
+func (mp MergePolicies) DebugString() string {
+	buf := bytes.Buffer{}
+	for k, v := range mp {
+		buf.WriteString(k + ": " + v.String() + ", ")
+	}
+	return strings.TrimSuffix(buf.String(), ", ")
+}
+
+// Get implements flag.Getter
+func (mp MergePolicies) Get() interface{} {
+	r := make(MergePolicies, len(mp))
+	for k, v := range mp {
+		r[k] = v
+	}
+	return r
+}
+
+func parseIgnoreBase(val string) (MergePolicy, string) {
+	if len(val) == 0 {
+		return IgnoreBaseUseLast, val
+	}
+	// [:+]<var>
+	switch val[0] {
+	case ':':
+		return IgnoreBaseAppendPath, val[1:]
+	case '+':
+		return IgnoreBaseAppendFlag, val[1:]
+	}
+	// <var>[:+]
+	last := len(val) - 1
+	switch val[last] {
+	case ':':
+		return IgnoreBasePrependPath, val[:last]
+	case '+':
+		return IgnoreBasePrependFlag, val[:last]
+	case '*':
+		return IgnoreBaseUseLast, val[:last]
+	}
+	return IgnoreBaseUseFirst, val
+}
+
+// Set implements flag.Value
+func (mp MergePolicies) Set(values string) error {
+	if len(values) == 0 {
+		return fmt.Errorf("no value!")
+	}
+	for _, val := range strings.Split(values, ",") {
+		// [:+^-]<var>
+		switch val[0] {
+		case '^':
+			a, s := parseIgnoreBase(val[1:])
+			mp[s] = a
+			continue
+		case '-':
+			mp[val[1:]] = IgnoreVariable
+			continue
+		case ':':
+			mp[val[1:]] = AppendPath
+			continue
+		case '+':
+			mp[val[1:]] = AppendFlag
+			continue
+		}
+		// <var>[:+^]
+		last := len(val) - 1
+		switch val[last] {
+		case ':':
+			mp[val[:last]] = PrependPath
+		case '+':
+			mp[val[:last]] = PrependFlag
+		case '*':
+			mp[val[:last]] = UseLast
+		case '^':
+			mp[val[:last]] = UseBaseIgnoreProfiles
+		default:
+			mp[val] = UseFirst
+		}
+	}
+	return nil
 }
diff --git a/profiles/env_test.go b/profiles/env_test.go
index ccae649..d1975f3 100644
--- a/profiles/env_test.go
+++ b/profiles/env_test.go
@@ -5,9 +5,12 @@
 package profiles_test
 
 import (
+	"flag"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"sort"
 	"testing"
 
 	"v.io/jiri/profiles"
@@ -19,14 +22,18 @@
 
 func TestConfigHelper(t *testing.T) {
 	ctx := tool.NewDefaultContext()
-	ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, "release/go/src/v.io/jiri/profiles/testdata/m2.xml")
+	root, err := project.JiriRoot()
+	if err != nil {
+		t.Fatal(err)
+	}
+	ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, filepath.Join(root, "release/go/src/v.io/jiri/profiles/testdata/m2.xml"))
 	if err != nil {
 		t.Fatal(err)
 	}
 	ch.Vars = envvar.VarsFromOS()
 	ch.Delete("CGO_CFLAGS")
-	target, _ := profiles.NewTarget("native=")
-	ch.SetEnvFromProfiles(profiles.CommonConcatVariables(), map[string]bool{}, "go,syncbase", target)
+	native, _ := profiles.NewTarget("native")
+	ch.MergeEnvFromProfiles(profiles.JiriMergePolicies(), native, "go", "syncbase")
 	if got, want := ch.Get("CGO_CFLAGS"), "-IX -IY -IA -IB"; got != want {
 		t.Errorf("got %v, want %v", got, want)
 	}
@@ -40,17 +47,17 @@
 	profiles.InstallProfile("b", "root")
 	t1, t2 := &profiles.Target{}, &profiles.Target{}
 	t1.Set("t1=cpu1-os1")
-	t1.Env.Set("A=B C=D, B=C Z=Z")
+	t1.Env.Set("A=B C=D,B=C Z=Z")
 	t2.Set("t1=cpu1-os1")
-	t2.Env.Set("A=Z,B=Z,Z=Z")
+	t2.Env.Set("A=Z,B=Z,Z=Z1")
 	profiles.AddProfileTarget("a", *t1)
 	profiles.AddProfileTarget("b", *t2)
 	tmpdir, err := ioutil.TempDir(".", "pdb")
 	if err != nil {
 		t.Fatal(err)
 	}
-	filename := filepath.Join("release", "go", "src", "v.io", "jiri", "profiles", tmpdir, "manifest")
-	if err := profiles.Write(ctx, filepath.Join(root, filename)); err != nil {
+	filename := filepath.Join(root, "release", "go", "src", "v.io", "jiri", "profiles", tmpdir, "manifest")
+	if err := profiles.Write(ctx, filename); err != nil {
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(tmpdir)
@@ -59,8 +66,12 @@
 		t.Fatal(err)
 	}
 	ch.Vars = envvar.VarsFromSlice([]string{})
-	target, _ := profiles.NewTarget("t1=")
-	ch.SetEnvFromProfiles(map[string]string{"A": " "}, map[string]bool{"Z": true}, "a,b", target)
+	t1Target, _ := profiles.NewTarget("t1")
+	ch.MergeEnvFromProfiles(map[string]profiles.MergePolicy{
+		"A": profiles.AppendFlag,
+		"B": profiles.UseLast,
+		"Z": profiles.IgnoreBaseUseLast},
+		t1Target, "a", "b")
 	vars := ch.ToMap()
 	if got, want := len(vars), 3; got != want {
 		t.Errorf("got %v, want %v", got, want)
@@ -73,6 +84,59 @@
 	}
 }
 
+func TestMergeEnv(t *testing.T) {
+	base := []string{"FS1=A", "IF=A", "A=B", "B=A", "C=D", "P=A", "V=A", "P1=A", "V1=A", "IA=A", "IB=A", "IC=A", "ID=A", "IE=A", "IG1=A"}
+	b := []string{"FS1=B", "FS2=B", "IF=B", "A=B1", "B=B", "C=D1", "P=B", "V=B", "P1=B", "V1=B", "W=X", "Y=Z", "GP=X", "IA=B", "IB=B", "IC=B", "ID=B", "IE=B", "IG2=A"}
+	c := []string{"FS1=C", "FS2=C", "FS3=C", "A=BL", "B=C", "C=DL", "P=C", "V=C", "P1=C", "V1=C", "Y=ZL", "GP=XL", "IA=C", "IB=C", "IC=C", "ID=C", "IE=C", "IG3=B"}
+	env := envvar.VarsFromSlice(base)
+
+	policies := map[string]profiles.MergePolicy{
+		"GP":  profiles.UseLast,
+		"P":   profiles.PrependPath,
+		"V":   profiles.PrependFlag,
+		"P1":  profiles.AppendPath,
+		"V1":  profiles.AppendFlag,
+		"A":   profiles.IgnoreBaseUseLast,
+		"B":   profiles.UseBaseIgnoreProfiles,
+		"IA":  profiles.IgnoreBaseAppendPath,
+		"IB":  profiles.IgnoreBaseAppendFlag,
+		"IC":  profiles.IgnoreBasePrependPath,
+		"ID":  profiles.IgnoreBasePrependFlag,
+		"IE":  profiles.IgnoreBaseUseLast,
+		"IF":  profiles.IgnoreBaseUseFirst,
+		"IG1": profiles.IgnoreVariable,
+		"IG2": profiles.IgnoreVariable,
+		"IG3": profiles.IgnoreVariable,
+		"C":   profiles.UseLast,
+		"Y":   profiles.UseLast,
+	}
+	profiles.MergeEnv(policies, env, b, c)
+
+	expected := []string{"B=A", "A=BL", "C=DL", "GP=XL", "P1=A:B:C", "P=C:B:A",
+		"V1=A B C", "V=C B A", "W=X", "Y=ZL",
+		"IA=B:C", "IB=B C", "IC=C:B", "ID=C B", "IE=C",
+		"FS1=A", "FS2=B", "FS3=C", "IF=B",
+	}
+	sort.Strings(expected)
+	if got, want := env.ToSlice(), expected; len(got) != len(want) {
+		sort.Strings(got)
+		t.Errorf("got: %v", got)
+		t.Errorf("want: %v", want)
+		t.Errorf("got %v, want %v", len(got), len(want))
+	}
+	for _, g := range env.ToSlice() {
+		found := false
+		for _, w := range expected {
+			if g == w {
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("failed to find %v in %v", g, expected)
+		}
+	}
+}
+
 func testSetPathHelper(t *testing.T, name string) {
 	profiles.Clear()
 	ctx := tool.NewDefaultContext()
@@ -130,55 +194,87 @@
 		t.Fatalf("%v", err)
 	}
 
-	ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, "profiles-manifest")
+	ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, filepath.Join(jiriRoot, "profiles-manifest"))
 	if err != nil {
 		t.Fatal(err)
 	}
-	ch.Vars = envvar.VarsFromOS()
-	ch.Set(name, "")
 
-	var want string
+	var got, want string
 	switch name {
 	case "GOPATH":
-		want = filepath.Join(jiriRoot, "test")
-		ch.SetGoPath()
+		want = "GOPATH=" + filepath.Join(jiriRoot, "test")
+		got = ch.GoPath()
 	case "VDLPATH":
 		// Make a fake src directory.
 		want = filepath.Join(jiriRoot, "test", "src")
 		if err := ctx.Run().MkdirAll(want, 0755); err != nil {
 			t.Fatalf("%v", err)
 		}
-		ch.SetVDLPath()
+		want = "VDLPATH=" + want
+		got = ch.VDLPath()
 	}
-	if got := ch.Get(name); got != want {
+	if got != want {
 		t.Fatalf("unexpected value: got %v, want %v", got, want)
 	}
 }
 
-func TestSetGoPath(t *testing.T) {
+func TestGoPath(t *testing.T) {
 	testSetPathHelper(t, "GOPATH")
 }
 
-func TestSetVdlPath(t *testing.T) {
+func TestVDLPath(t *testing.T) {
 	testSetPathHelper(t, "VDLPATH")
 }
 
-func TestMergeEnv(t *testing.T) {
-	a := []string{"A=B", "C=D"}
-	b := []string{"W=X", "Y=Z", "GP=X"}
-	env := envvar.VarsFromSlice(a)
-	profiles.MergeEnv(map[string]string{}, map[string]bool{"GP": true}, env, b)
-	if got, want := len(env.ToSlice()), 4; got != want {
+func TestMergePolicyFlags(t *testing.T) {
+	mp := profiles.MergePolicies{}
+	fs := flag.NewFlagSet("test", flag.ContinueOnError)
+	fs.Var(mp, "p", mp.Usage())
+	all := []string{"-p=:a", "-p=+b", "-p=^c", "-p=^:d", "-p=^e:", "-p=^+f", "-p=^g+", "-p=last*", "-p=xx:", "-p=yy+", "-p=zz^"}
+	if err := fs.Parse(all); err != nil {
+		t.Fatal(err)
+	}
+	for _, c := range []struct {
+		k string
+		p profiles.MergePolicy
+	}{
+		{"a", profiles.AppendPath},
+		{"b", profiles.AppendFlag},
+		{"c", profiles.IgnoreBaseUseFirst},
+		{"d", profiles.IgnoreBaseAppendPath},
+		{"e", profiles.IgnoreBasePrependPath},
+		{"f", profiles.IgnoreBaseAppendFlag},
+		{"g", profiles.IgnoreBasePrependFlag},
+		{"last", profiles.UseLast},
+		{"xx", profiles.PrependPath},
+		{"yy", profiles.PrependFlag},
+		{"zz", profiles.UseBaseIgnoreProfiles},
+	} {
+		if got, want := mp[c.k], c.p; got != want {
+			t.Errorf("(%s) got %v, want %v", c.k, got, want)
+		}
+	}
+
+	mp = profiles.MergePolicies{}
+	fs1 := flag.NewFlagSet("test1", flag.ContinueOnError)
+	fs1.Var(mp, "p", mp.Usage())
+	if err := fs1.Parse([]string{"-p=yy+,zz^"}); err != nil {
+		t.Fatal(err)
+	}
+	if got, want := len(mp), 2; got != want {
 		t.Errorf("got %v, want %v", got, want)
 	}
-	if got, want := env.Get("W"), "X"; got != want {
-		t.Errorf("got %v, want %v", got, want)
-	}
-	profiles.MergeEnv(map[string]string{"W": " "}, map[string]bool{"GP": true}, env, []string{"W=an option"})
-	if got, want := len(env.ToSlice()), 4; got != want {
-		t.Errorf("got %v, want %v", got, want)
-	}
-	if got, want := env.Get("W"), "X an option"; got != want {
-		t.Errorf("got %v, want %v", got, want)
+
+	for i, cl := range append(all, "-p=+b,^c,zz^") {
+		mp := profiles.MergePolicies{}
+		fs := flag.NewFlagSet(fmt.Sprintf("t%d", i), flag.ContinueOnError)
+		fs.Var(mp, "p", mp.Usage())
+		err := fs.Parse([]string{cl})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if got, want := "-p="+mp.String(), cl; got != want {
+			t.Errorf("%d: got %v, want %v", i, got, want)
+		}
 	}
 }
diff --git a/profiles/manifest.go b/profiles/manifest.go
index 54942d1..da6e0e5 100644
--- a/profiles/manifest.go
+++ b/profiles/manifest.go
@@ -233,6 +233,7 @@
 	data, err := ctx.Run().ReadFile(filename)
 	if err != nil {
 		if os.IsNotExist(err) {
+			fmt.Fprintf(ctx.Stderr(), "WARNING: %v doesn't exist\n", filename)
 			return nil
 		}
 		return err
diff --git a/profiles/target.go b/profiles/target.go
index f54dfe0..a24fcb9 100644
--- a/profiles/target.go
+++ b/profiles/target.go
@@ -69,6 +69,14 @@
 	return r
 }
 
+// UseCommandLineEnv copies the command line supplied environment variables
+// into the mutable environment of the Target. It should be called as soon
+// as all command line parsing has been completed and before the target is
+// otherwise used.
+func (pt *Target) UseCommandLineEnv() {
+	pt.Env = pt.CommandLineEnv()
+}
+
 // TargetSpecificDirname returns a directory name that is specific
 // to that target taking account the tag, architecture, operating system and
 // command line environment variables, if relevant, into account (e.g
diff --git a/profiles/util.go b/profiles/util.go
index a08822d..3ded6a3 100644
--- a/profiles/util.go
+++ b/profiles/util.go
@@ -15,6 +15,7 @@
 	"strings"
 
 	"v.io/jiri/collect"
+	"v.io/jiri/project"
 	"v.io/jiri/tool"
 )
 
@@ -41,22 +42,52 @@
 	flags.Var(&target.commandLineEnv, "env", target.commandLineEnv.Usage())
 }
 
-// RegisterManifestFlag registers the commonly used --manifest
+// RegisterManifestFlag registers the commonly used --profiles-manifest
 // flag with the supplied FlagSet.
 func RegisterManifestFlag(flags *flag.FlagSet, manifest *string, defaultManifest string) {
-	flags.StringVar(manifest, "manifest", defaultManifest, "specify the profiles XML manifest filename.")
-	flags.Lookup("manifest").DefValue = filepath.Join("$JIRI_ROOT", defaultManifest)
+	root, _ := project.JiriRoot()
+	flags.StringVar(manifest, "profiles-manifest", filepath.Join(root, defaultManifest), "specify the profiles XML manifest filename.")
+	flags.Lookup("profiles-manifest").DefValue = filepath.Join("$JIRI_ROOT", defaultManifest)
 }
 
-// RegisterProfileFlags registers the commonly used --manifest, --profiles and
-// --target flags with the supplied FlagSet.
-func RegisterProfileFlags(flags *flag.FlagSet, profilesMode *ProfilesMode, manifest, profiles *string, defaultManifest string, target *Target) {
+// RegisterProfileFlags registers the commonly used --profiles-manifest, --profiles,
+// --target and --merge-policies flags with the supplied FlagSet.
+func RegisterProfileFlags(flags *flag.FlagSet, profilesMode *ProfilesMode, manifest, profiles *string, defaultManifest string, policies *MergePolicies, target *Target) {
 	flags.Var(profilesMode, "skip-profiles", "if set, no profiles will be used")
-	flags.StringVar(profiles, "profiles", "base", "a comma separated list of profiles to use")
+	RegisterProfilesFlag(flags, profiles)
+	RegisterMergePoliciesFlag(flags, policies)
 	RegisterManifestFlag(flags, manifest, defaultManifest)
 	RegisterTargetFlag(flags, target)
 }
 
+// RegisterProfilesFlag registers the --profiles flag
+func RegisterProfilesFlag(flags *flag.FlagSet, profiles *string) {
+	flags.StringVar(profiles, "profiles", "base,jiri", "a comma separated list of profiles to use")
+}
+
+// RegisterMergePoliciesFlag registers the --merge-policies flag
+func RegisterMergePoliciesFlag(flags *flag.FlagSet, policies *MergePolicies) {
+	flags.Var(policies, "merge-policies", "specify policies for merging environment variables")
+}
+
+type AppendJiriProfileMode bool
+
+const (
+	AppendJiriProfile      AppendJiriProfileMode = true
+	DoNotAppendJiriProfile                       = false
+)
+
+// InitProfilesFromFlag splits a comma separated list of profile names into
+// a slice and optionally appends the 'jiri' profile if it's not already
+// present.
+func InitProfilesFromFlag(flag string, appendJiriProfile AppendJiriProfileMode) []string {
+	n := strings.Split(flag, ",")
+	if appendJiriProfile == AppendJiriProfile && !strings.Contains(flag, "jiri") {
+		n = append(n, "jiri")
+	}
+	return n
+}
+
 // AtomicAction performs an action 'atomically' by keeping track of successfully
 // completed actions in the supplied completion log and re-running them if they
 // are not successfully logged therein after deleting the entire contents of the
@@ -72,6 +103,7 @@
 			if !ctx.Run().FileExists(completionLogPath) {
 				ctx.Run().RemoveAll(dir)
 			} else {
+				fmt.Fprintf(ctx.Stdout(), "AtomicAction: %s already completed in %s\n", message, dir)
 				return nil
 			}
 		}
diff --git a/profiles/util_test.go b/profiles/util_test.go
index acfb16c..c7bdf6a 100644
--- a/profiles/util_test.go
+++ b/profiles/util_test.go
@@ -4,7 +4,20 @@
 
 package profiles
 
+import "testing"
+
 // Clear resets the current database and is intended for use from tests only.
 func Clear() {
 	db = newDB()
 }
+
+func TestAppendJiriProfile(t *testing.T) {
+	p := InitProfilesFromFlag("foo", DoNotAppendJiriProfile)
+	if got, want := p, []string{"foo"}; len(got) != 1 || got[0] != "foo" {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	p = InitProfilesFromFlag("foo", AppendJiriProfile)
+	if got, want := p, []string{"foo", "jiri"}; len(got) != 2 || got[0] != "foo" || got[1] != "jiri" {
+		t.Errorf("got %v, want %v", got, want)
+	}
+}
diff --git a/profiles/versions.go b/profiles/versions.go
index 8fde0a1..0885783 100644
--- a/profiles/versions.go
+++ b/profiles/versions.go
@@ -9,6 +9,7 @@
 	"fmt"
 	"reflect"
 	"sort"
+	"strings"
 )
 
 // VersionInfo represents the supported and default versions offered
@@ -82,14 +83,13 @@
 // default.
 func (vi *VersionInfo) String() string {
 	r := bytes.Buffer{}
-	r.WriteString(vi.name + ":")
 	for _, v := range vi.ordered {
 		r.WriteString(" " + v)
 		if v == vi.defaultVersion {
 			r.WriteString("*")
 		}
 	}
-	return r.String()
+	return strings.TrimLeft(r.String(), " ")
 }
 
 // Default returns the default version.
diff --git a/profiles/versions_test.go b/profiles/versions_test.go
index 6e9a4f2..0e1f748 100644
--- a/profiles/versions_test.go
+++ b/profiles/versions_test.go
@@ -76,7 +76,7 @@
 		t.Errorf("got %v, want %v", got, want)
 	}
 
-	if ver, err := vi.Select("2"); ver != "" || err.Error() != "unsupported version: \"2\" for test: 6 5 4 3*" {
-		t.Errorf("failed to detect unsupported version: %v", err)
+	if ver, err := vi.Select("2"); ver != "" || err.Error() != "unsupported version: \"2\" for 6 5 4 3*" {
+		t.Errorf("failed to detect unsupported version: %q", err)
 	}
 }