基于Cobra的命令行工具:实现prepare与run命令的属性共享
Great question! When building a Cobra-based CLI where one command depends on the output of another, you have a few solid approaches to share state between prepare and run. Let’s break down the most practical ones, with code examples tailored to your use case:
1. Persist State to a Configuration File (Most Reliable)
This is the go-to approach for cross-command state sharing in CLI tools. The prepare command writes critical data (like temp directory path, file validation status) to a structured config file (JSON/YAML), and run reads this file to get what it needs.
Step 1: Define a Shared State Structure
Create a reusable package to hold your state (e.g., internal/state/state.go):
package state import "time" type RunContext struct { TempDir string `json:"temp_dir"` FilesValid bool `json:"files_valid"` CreatedAt time.Time `json:"created_at"` }
Step 2: Update prepare to Write State
Modify your prepare command's RunE function to save state after completing its tasks:
func prepareCmdRun(cmd *cobra.Command, args []string) error { // 1. Create temp directory tempDir, err := os.MkdirTemp("", "btr-*") if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } // 2. Validate files (replace with your actual logic) filesValid := validateRequiredFiles() // 3. Build and save state ctx := state.RunContext{ TempDir: tempDir, FilesValid: filesValid, CreatedAt: time.Now(), } // Get standard config directory (works across OSes) configDir, err := os.UserConfigDir() if err != nil { return fmt.Errorf("failed to get config directory: %w", err) } configPath := filepath.Join(configDir, "btr", "context.json") // Create parent directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } // Serialize state to JSON data, err := json.MarshalIndent(ctx, "", " ") if err != nil { return fmt.Errorf("failed to serialize state: %w", err) } if err := os.WriteFile(configPath, data, 0644); err != nil { return fmt.Errorf("failed to write state file: %w", err) } fmt.Printf("✅ Prepare complete! Temp directory: %s\n", tempDir) return nil }
Step 3: Update run to Read and Validate State
Modify your run command to load the state, and add checks to ensure prepare was run successfully:
func runCmdRun(cmd *cobra.Command, args []string) error { // Get config path (match what's used in prepare) configDir, err := os.UserConfigDir() if err != nil { return fmt.Errorf("failed to get config directory: %w", err) } configPath := filepath.Join(configDir, "btr", "context.json") // Check if state file exists if _, err := os.Stat(configPath); os.IsNotExist(err) { return fmt.Errorf("❌ Prepare has not been run yet! Please execute `btr prepare` first") } // Read and deserialize state data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("failed to read state file: %w", err) } var ctx state.RunContext if err := json.Unmarshal(data, &ctx); err != nil { return fmt.Errorf("failed to parse state file: %w", err) } // Add optional validity checks if time.Since(ctx.CreatedAt) > 2*time.Hour { return fmt.Errorf("❌ Prepare state is outdated (older than 2 hours)! Re-run `btr prepare`") } if !ctx.FilesValid { return fmt.Errorf("❌ Previous prepare step found invalid files! Fix issues and re-run `btr prepare`") } // Use the state in your run logic fmt.Printf("🚀 Starting run with temp directory: %s\n", ctx.TempDir) // ... your core run command logic here ... return nil }
2. Use Environment Variables (Lightweight Option)
If you prefer not to write files, you can have prepare output shell commands to set environment variables, which run can then read. Note that this only works for the current shell session.
prepare Command Output
func prepareCmdRun(cmd *cobra.Command, args []string) error { tempDir, err := os.MkdirTemp("", "btr-*") if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } filesValid := validateRequiredFiles() // Print export commands for the user to source fmt.Printf("export BTR_TEMP_DIR=\"%s\"\n", tempDir) fmt.Printf("export BTR_FILES_VALID=\"%t\"\n", filesValid) fmt.Println("\n💡 Run `eval $(btr prepare)` to set these variables automatically") return nil }
run Command Read
func runCmdRun(cmd *cobra.Command, args []string) error { tempDir := os.Getenv("BTR_TEMP_DIR") if tempDir == "" { return fmt.Errorf("❌ BTR_TEMP_DIR not set! Run `eval $(btr prepare)` first") } filesValidStr := os.Getenv("BTR_FILES_VALID") filesValid, err := strconv.ParseBool(filesValidStr) if err != nil || !filesValid { return fmt.Errorf("❌ Files are invalid or BTR_FILES_VALID not set! Re-run `btr prepare`") } // Execute run logic fmt.Printf("🚀 Running with temp dir: %s\n", tempDir) return nil }
3. Optional: Auto-Run Prepare from run
For user convenience, you can add a flag to run that automatically triggers prepare if no state exists:
func init() { runCmd.Flags().BoolP("auto-prepare", "a", false, "Automatically run prepare if not completed") } func runCmdRun(cmd *cobra.Command, args []string) error { autoPrepare, _ := cmd.Flags().GetBool("auto-prepare") configPath := getConfigPath() // Reuse the path logic from earlier if _, err := os.Stat(configPath); os.IsNotExist(err) { if autoPrepare { fmt.Println("🔧 Prepare not found, running automatically...") if err := prepareCmdRun(cmd, args); err != nil { return err } } else { return fmt.Errorf("❌ Prepare not run! Use `btr prepare` or `btr run --auto-prepare`") } } // Load state and proceed with run logic... return nil }
Key Best Practices
- Always add validation checks in
run(e.g., state freshness, file validity) to avoid using stale or invalid data. - Let users override the config file path with a flag (e.g.,
--config) for flexibility. - Add a
btr cleancommand to delete the state file and temp directory when needed.
内容的提问来源于stack exchange,提问作者user6124024

