From cc759896c39964ffcaa95b90cc6622503be7c06a Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 8 May 2026 11:30:13 +0200 Subject: [PATCH] chore/cli: migrate users to urfave/cli The command migration is moving legacy commander commands onto urfave/cli so they share the same help, flag, and dispatch behavior as newly migrated commands. Migrate src users and its list, get, create, delete, prune, and tag subcommands to cli.Command definitions. Register both users and user in the migrated command map, and remove users from the legacy docs path so generated docs come from the migrated root command. Test Plan: - go test ./cmd/src - go test ./... - go run ./cmd/src users -h - go run ./cmd/src user -h - go run ./cmd/src users list -h --- cmd/src/doc.go | 1 - cmd/src/run_migration_compat.go | 4 +- cmd/src/users.go | 41 +++++++------- cmd/src/users_create.go | 84 +++++++++++++++------------- cmd/src/users_delete.go | 72 +++++++++++------------- cmd/src/users_get.go | 76 ++++++++++++------------- cmd/src/users_list.go | 82 ++++++++++++++------------- cmd/src/users_prune.go | 98 ++++++++++++++++++--------------- cmd/src/users_tag.go | 76 ++++++++++++------------- lib/batches/batch_spec.go | 1 + 10 files changed, 275 insertions(+), 260 deletions(-) diff --git a/cmd/src/doc.go b/cmd/src/doc.go index d433aa24cb..8132189c87 100644 --- a/cmd/src/doc.go +++ b/cmd/src/doc.go @@ -63,7 +63,6 @@ Examples: "extsvc": &extsvcCommands, "code-intel": &codeintelCommands, "repos": &reposCommands, - "users": &usersCommands, } pending := out.Pending(output.Line("", output.StylePending, "Rendering Markdown...")) diff --git a/cmd/src/run_migration_compat.go b/cmd/src/run_migration_compat.go index 5a9296d5e9..6671b07da3 100644 --- a/cmd/src/run_migration_compat.go +++ b/cmd/src/run_migration_compat.go @@ -25,8 +25,10 @@ var migratedCommands = map[string]*cli.Command{ // instead of writing lots of plumbing to handle an alias, lets just register it explicitly for now "codeowner": codeownersCommand, "login": loginCommand, - "orgs": orgsCommand, + "orgs": orgsCommand, "org": orgsCommand, + "users": usersCommand, + "user": usersCommand, "version": versionCommand, } diff --git a/cmd/src/users.go b/cmd/src/users.go index 3b915d75e1..5fa4c99c49 100644 --- a/cmd/src/users.go +++ b/cmd/src/users.go @@ -1,14 +1,28 @@ package main import ( - "flag" - "fmt" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -var usersCommands commander +var usersCommand = clicompat.Wrap(&cli.Command{ + Name: "users", + Aliases: []string{"user"}, + Usage: "manages users", + UsageText: "src users [command options]", + Description: usersExamples, + HideVersion: true, + Commands: []*cli.Command{ + usersListCommand, + usersGetCommand, + usersCreateCommand, + usersDeleteCommand, + usersPruneCommand, + usersTagCommand, + }, +}) -func init() { - usage := `'src users' is a tool that manages users on a Sourcegraph instance. +const usersExamples = `'src users' is a tool that manages users on a Sourcegraph instance. Usage: @@ -26,23 +40,6 @@ The commands are: Use "src users [command] -h" for more information about a command. ` - flagSet := flag.NewFlagSet("users", flag.ExitOnError) - handler := func(args []string) error { - usersCommands.run(flagSet, "src users", usage, args) - return nil - } - - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - aliases: []string{"user"}, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, - }) -} - const userFragment = ` fragment UserFields on User { id diff --git a/cmd/src/users_create.go b/cmd/src/users_create.go index b33b5808ba..71ebe7dad8 100644 --- a/cmd/src/users_create.go +++ b/cmd/src/users_create.go @@ -2,14 +2,13 @@ package main import ( "context" - "flag" "fmt" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersCreateExamples = ` Examples: Create a user account: @@ -18,25 +17,36 @@ Examples: ` - flagSet := flag.NewFlagSet("create", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - usernameFlag = flagSet.String("username", "", `The new user's username. (required)`) - emailFlag = flagSet.String("email", "", `The new user's email address. (required)`) - resetPasswordURLFlag = flagSet.Bool("reset-password-url", false, `Print the reset password URL to manually send to the new user.`) - apiFlags = api.NewFlags(flagSet) - ) +var usersCreateCommand = clicompat.Wrap(&cli.Command{ + Name: "create", + Usage: "creates a user account", + UsageText: "src users create [options]", + Description: usersCreateExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "username", + Usage: "The new user's username.", + Required: true, + Validator: requiresNotEmpty("provide a username name using -username"), + }, + &cli.StringFlag{ + Name: "email", + Usage: "The new user's email address", + Required: true, + Validator: requiresNotEmpty("provide a email name using -email"), + }, + &cli.BoolFlag{ + Name: "reset-password-url", + Usage: "Print the reset password URL to manually send to the new user.", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + username := cmd.String("username") + email := cmd.String("email") + resetPasswordURL := cmd.Bool("reset-password-url") - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation CreateUser( $username: String!, @@ -56,24 +66,22 @@ Examples: } } if ok, err := client.NewRequest(query, map[string]any{ - "username": *usernameFlag, - "email": *emailFlag, - }).Do(context.Background(), &result); err != nil || !ok { + "username": username, + "email": email, + }).Do(ctx, &result); err != nil || !ok { return err } - fmt.Printf("User %q created.\n", *usernameFlag) - if *resetPasswordURLFlag && result.CreateUser.ResetPasswordURL != "" { - fmt.Println() - fmt.Printf("\tReset pasword URL: %s\n", result.CreateUser.ResetPasswordURL) + if _, err := fmt.Fprintf(cmd.Writer, "User %q created.\n", username); err != nil { + return err + } + if resetPasswordURL && result.CreateUser.ResetPasswordURL != "" { + if _, err := fmt.Fprintln(cmd.Writer); err != nil { + return err + } + _, err := fmt.Fprintf(cmd.Writer, "\tReset pasword URL: %s\n", result.CreateUser.ResetPasswordURL) + return err } return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/users_delete.go b/cmd/src/users_delete.go index c4b59b6c3f..47f46503ad 100644 --- a/cmd/src/users_delete.go +++ b/cmd/src/users_delete.go @@ -3,17 +3,16 @@ package main import ( "bufio" "context" - "flag" "fmt" "os" "strconv" "strings" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersDeleteExamples = ` Examples: Delete a user account by ID: @@ -30,25 +29,23 @@ Examples: ` - flagSet := flag.NewFlagSet("delete", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - userIDFlag = flagSet.String("id", "", `The ID of the user to delete.`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) - - if *userIDFlag == "" { +var usersDeleteCommand = clicompat.Wrap(&cli.Command{ + Name: "delete", + Usage: "deletes a user account", + UsageText: "src users delete [options]", + Description: usersDeleteExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "id", + Usage: "The ID of the user to delete.", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + userID := cmd.String("id") + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + + if userID == "" { query := `query UsersTotalCountCountUsers { users { totalCount } }` var result struct { @@ -56,12 +53,14 @@ Examples: TotalCount int } } - ok, err := client.NewQuery(query).Do(context.Background(), &result) + ok, err := client.NewQuery(query).Do(ctx, &result) if err != nil || !ok { return err } - fmt.Printf("No user ID specified. This would delete %d users.\nType in this number to confirm and hit return: ", result.Users.TotalCount) + if _, err := fmt.Fprintf(cmd.Writer, "No user ID specified. This would delete %d users.\nType in this number to confirm and hit return: ", result.Users.TotalCount); err != nil { + return err + } reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') if err != nil { @@ -74,8 +73,8 @@ Examples: } if count != result.Users.TotalCount { - fmt.Println("Number does not match. Aborting.") - return nil + _, err := fmt.Fprintln(cmd.Writer, "Number does not match. Aborting.") + return err } } @@ -93,19 +92,12 @@ Examples: DeleteUser struct{} } if ok, err := client.NewRequest(query, map[string]any{ - "user": *userIDFlag, - }).Do(context.Background(), &result); err != nil || !ok { + "user": userID, + }).Do(ctx, &result); err != nil || !ok { return err } - fmt.Printf("User with ID %q deleted.\n", *userIDFlag) - return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + _, err := fmt.Fprintf(cmd.Writer, "User with ID %q deleted.\n", userID) + return err + }, +}) diff --git a/cmd/src/users_get.go b/cmd/src/users_get.go index 6b27865f19..cf496a04a4 100644 --- a/cmd/src/users_get.go +++ b/cmd/src/users_get.go @@ -2,16 +2,15 @@ package main import ( "context" - "flag" - "fmt" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersGetExamples = ` Examples: Get user with username alice: @@ -20,31 +19,39 @@ Examples: ` - flagSet := flag.NewFlagSet("get", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - usernameFlag = flagSet.String("username", "", `Look up user by username. (e.g. "alice")`) - emailFlag = flagSet.String("email", "", `Look up user by email. (e.g. "alice@sourcegraph.com")`) - formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})")`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) - - if usernameFlag != nil && *usernameFlag != "" && emailFlag != nil && *emailFlag != "" { +var usersGetCommand = clicompat.Wrap(&cli.Command{ + Name: "get", + Usage: "gets a user", + UsageText: "src users get [options]", + Description: usersGetExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "username", + Usage: `Look up user by username. (e.g. "alice")`, + }, + &cli.StringFlag{ + Name: "email", + Usage: `Look up user by email. (e.g. "alice@sourcegraph.com")`, + }, + &cli.StringFlag{ + Name: "f", + Value: "{{.|json}}", + Usage: `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})")`, + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + username := cmd.String("username") + email := cmd.String("email") + format := cmd.String("f") + + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + + if username != "" && email != "" { return errors.New("cannot specify both email and username") } - tmpl, err := parseTemplate(*formatFlag) + tmpl, err := parseTemplate(format) if err != nil { return err } @@ -65,19 +72,12 @@ Examples: User *User } if ok, err := client.NewRequest(query, map[string]any{ - "username": api.NullString(*usernameFlag), - "email": api.NullString(*emailFlag), - }).Do(context.Background(), &result); err != nil || !ok { + "username": api.NullString(username), + "email": api.NullString(email), + }).Do(ctx, &result); err != nil || !ok { return err } return execTemplate(tmpl, result.User) - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/users_list.go b/cmd/src/users_list.go index a72e1d8a3e..93dae9601c 100644 --- a/cmd/src/users_list.go +++ b/cmd/src/users_list.go @@ -2,14 +2,13 @@ package main import ( "context" - "flag" - "fmt" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersListExamples = ` Examples: List users: @@ -26,36 +25,48 @@ Examples: ` - flagSet := flag.NewFlagSet("list", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - firstFlag = flagSet.Int("first", 1000, "Returns the first n users from the list.") - queryFlag = flagSet.String("query", "", `Returns users whose names match the query. (e.g. "alice")`) - tagFlag = flagSet.String("tag", "", `Returns users with the given tag.`) - formatFlag = flagSet.String("f", "{{.Username}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})" or "{{.|json}}")`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - ctx := context.Background() - client := cfg.apiClient(apiFlags, flagSet.Output()) - - tmpl, err := parseTemplate(*formatFlag) +var usersListCommand = clicompat.Wrap(&cli.Command{ + Name: "list", + Usage: "lists users", + UsageText: "src users list [options]", + Description: usersListExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.IntFlag{ + Name: "first", + Value: 1000, + Usage: "Returns the first n users from the list.", + }, + &cli.StringFlag{ + Name: "query", + Usage: `Returns users whose names match the query. (e.g. "alice")`, + }, + &cli.StringFlag{ + Name: "tag", + Usage: `Returns users with the given tag.`, + }, + &cli.StringFlag{ + Name: "f", + Value: "{{.Username}}", + Usage: `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})" or "{{.|json}}")`, + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + first := cmd.Int("first") + queryValue := cmd.String("query") + tag := cmd.String("tag") + format := cmd.String("f") + + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + + tmpl, err := parseTemplate(format) if err != nil { return err } vars := map[string]any{ - "first": api.NullInt(*firstFlag), - "query": api.NullString(*queryFlag), - "tag": api.NullString(*tagFlag), + "first": api.NullInt(first), + "query": api.NullString(queryValue), + "tag": api.NullString(tag), } queryTagVar := "" queryTag := "" @@ -94,12 +105,5 @@ first: $first, } } return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/users_prune.go b/cmd/src/users_prune.go index d67fff6be0..c4bf3a044e 100644 --- a/cmd/src/users_prune.go +++ b/cmd/src/users_prune.go @@ -2,7 +2,6 @@ package main import ( "context" - "flag" "fmt" "os" "strings" @@ -11,10 +10,11 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersPruneExamples = ` This command removes users from a Sourcegraph instance who have been inactive for 60 or more days. Admin accounts are omitted by default. Examples: @@ -24,32 +24,49 @@ Examples: $ src users prune -remove-admin -remove-null-users ` - flagSet := flag.NewFlagSet("prune", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - daysToDelete = flagSet.Int("days", 60, "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ") - removeAdmin = flagSet.Bool("remove-admin", false, "prune admin accounts") - removeNoLastActive = flagSet.Bool("remove-null-users", false, "removes users with no last active value") - skipConfirmation = flagSet.Bool("force", false, "skips user confirmation step allowing programmatic use") - displayUsersToDelete = flagSet.Bool("display-users", false, "display table of users to be deleted by prune") - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { +var usersPruneCommand = clicompat.Wrap(&cli.Command{ + Name: "prune", + Usage: "deletes inactive users", + UsageText: "src users prune [options]", + Description: usersPruneExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.IntFlag{ + Name: "days", + Value: 60, + Usage: "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ", + }, + &cli.BoolFlag{ + Name: "remove-admin", + Usage: "prune admin accounts", + }, + &cli.BoolFlag{ + Name: "remove-null-users", + Usage: "removes users with no last active value", + }, + &cli.BoolFlag{ + Name: "force", + Usage: "skips user confirmation step allowing programmatic use", + }, + &cli.BoolFlag{ + Name: "display-users", + Usage: "display table of users to be deleted by prune", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + daysToDelete := cmd.Int("days") + removeAdmin := cmd.Bool("remove-admin") + removeNoLastActive := cmd.Bool("remove-null-users") + skipConfirmation := cmd.Bool("force") + displayUsersToDelete := cmd.Bool("display-users") + + if daysToDelete < 60 { + _, err := fmt.Fprintln(cmd.Writer, "-days flag must be set to 60 or greater") return err } - if *daysToDelete < 60 { - fmt.Println("-days flag must be set to 60 or greater") - return nil - } - ctx := context.Background() - client := cfg.apiClient(apiFlags, flagSet.Output()) + apiFlags := clicompat.APIFlagsFromCmd(cmd) + client := cfg.apiClient(apiFlags, cmd.Writer) // get current user so as not to delete issuer of the prune request currentUserQuery := `query getCurrentUser { currentUser { username }}` @@ -58,7 +75,7 @@ Examples: Username string } } - if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(currentUserQuery, nil).Do(context.Background(), ¤tUserResult); err != nil || !ok { + if ok, err := client.NewRequest(currentUserQuery, nil).Do(ctx, ¤tUserResult); err != nil || !ok { return err } @@ -71,7 +88,7 @@ Examples: } } } - if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(totalUsersQuery, nil).Do(context.Background(), &totalUsers); err != nil || !ok { + if ok, err := client.NewRequest(totalUsersQuery, nil).Do(ctx, &totalUsers); err != nil || !ok { return err } @@ -140,15 +157,15 @@ Examples: return err } // don't remove users with no last active value unless option flag is set - if !hasLastActive && !*removeNoLastActive { + if !hasLastActive && !removeNoLastActive { continue } // don't remove admins unless option flag is set - if !*removeAdmin && user.SiteAdmin { + if !removeAdmin && user.SiteAdmin { continue } // remove users who have been inactive for longer than the threshold set by the -days flag - if daysSinceLastUse <= *daysToDelete && hasLastActive { + if daysSinceLastUse <= daysToDelete && hasLastActive { continue } // serialize user to print in table as part of confirmUserRemoval, add to delete slice @@ -156,7 +173,7 @@ Examples: usersToDelete = append(usersToDelete, userToDelete) } - if *skipConfirmation { + if skipConfirmation { for _, user := range usersToDelete { if err := removeUser(user.User, client, ctx); err != nil { return err @@ -166,11 +183,11 @@ Examples: } // confirm and remove users - if confirmed, _ := confirmUserRemoval(usersToDelete, *daysToDelete, *displayUsersToDelete); !confirmed { - fmt.Println("Aborting removal") + if confirmed, _ := confirmUserRemoval(usersToDelete, daysToDelete, displayUsersToDelete); !confirmed { + fmt.Fprintln(cmd.Writer, "Aborting removal") return nil } else { - fmt.Println("REMOVING USERS") + fmt.Fprintln(cmd.Writer, "REMOVING USERS") for _, user := range usersToDelete { if err := removeUser(user.User, client, ctx); err != nil { return err @@ -179,15 +196,8 @@ Examples: } return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) // computes days since last usage from current day and time and aggregated_user_statistics.lastActiveAt, uses time.Parse func computeDaysSinceLastUse(user SiteUser) (timeDiff int, hasLastActive bool, _ error) { diff --git a/cmd/src/users_tag.go b/cmd/src/users_tag.go index 4404c88c19..d18b0c0f13 100644 --- a/cmd/src/users_tag.go +++ b/cmd/src/users_tag.go @@ -2,14 +2,12 @@ package main import ( "context" - "flag" - "fmt" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersTagExamples = ` Examples: Add a tag "foo" to a user: @@ -28,25 +26,36 @@ Related examples: ` - flagSet := flag.NewFlagSet("tag", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - userIDFlag = flagSet.String("user-id", "", `The ID of the user to tag. (required)`) - tagFlag = flagSet.String("tag", "", `The tag to set on the user. (required)`) - removeFlag = flagSet.Bool("remove", false, `Remove the tag. (default: add the tag`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) +var usersTagCommand = clicompat.Wrap(&cli.Command{ + Name: "tag", + Usage: "add/remove a tag on a user", + UsageText: "src users tag [options]", + Description: usersTagExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "user-id", + Usage: "The ID of the user to tag.", + Required: true, + Validator: requiresNotEmpty("provide a user ID by using -user-id"), + }, + &cli.StringFlag{ + Name: "tag", + Usage: "The tag to set on the user.", + Required: true, + Validator: requiresNotEmpty("provide a tag by using -tag"), + }, + &cli.BoolFlag{ + Name: "remove", + Usage: "Remove the tag. (default: add the tag)", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + userID := cmd.String("user-id") + tag := cmd.String("tag") + remove := cmd.Bool("remove") + + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation SetUserTag( $user: ID!, @@ -63,17 +72,10 @@ Related examples: }` _, err := client.NewRequest(query, map[string]any{ - "user": *userIDFlag, - "tag": *tagFlag, - "present": !*removeFlag, - }).Do(context.Background(), &struct{}{}) + "user": userID, + "tag": tag, + "present": !remove, + }).Do(ctx, &struct{}{}) return err - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go index a9ccc6e146..269093a82d 100644 --- a/lib/batches/batch_spec.go +++ b/lib/batches/batch_spec.go @@ -185,6 +185,7 @@ func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { return &spec, errs } + // docker uses Golang's `encoding/csv` library to parse arguments passed to `--mount` const invalidMountCharacters = ",\"\n\r"