package main import ( "fmt" "io/ioutil" "os" "os/exec" "os/user" "path/filepath" "strings" townuser "git.tilde.town/tildetown/town/user" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) const binroot = "/town/commands" var rootCmd = &cobra.Command{ Use: "town", Short: "Run commands unique to tilde.town", } var adminCmd = &cobra.Command{ Use: "admin", Short: "Run administrative commands", } func isAdmin() (bool, error) { u, err := user.Current() if err != nil { return false, fmt.Errorf("failed to get information about current user: %w", err) } return townuser.IsAdmin(u) } func parseCommands(targetCmd *cobra.Command, path string) error { binPath := filepath.Join(binroot, path) files, err := ioutil.ReadDir(binPath) if err != nil { return fmt.Errorf("failed to list directory %s: %s", binPath, err) } for _, file := range files { if strings.HasSuffix(file.Name(), "yml") { parseCommand(targetCmd, filepath.Join(binPath, file.Name())) } } return nil } type commandDoc struct { ShortDesc string `yaml:"shortDesc"` LongDesc string `yaml:"longDesc"` Examples string Maintainer string } func parseCommand(targetCmd *cobra.Command, yamlPath string) { executablePath := strings.TrimSuffix(yamlPath, ".yml") // TODO handle when files lack executable bit _, err := os.Stat(executablePath) if err != nil { fmt.Fprintf(os.Stderr, "could not find matching executable for %s; skipping...\n", yamlPath) return } yamlBytes, err := ioutil.ReadFile(yamlPath) if err != nil { fmt.Fprintf(os.Stderr, "could not read %s; skipping...\n", yamlPath) return } doc := commandDoc{} err = yaml.Unmarshal(yamlBytes, &doc) if err != nil { fmt.Fprintf(os.Stderr, "could not parse %s; skipping...\n", yamlPath) return } if doc.Maintainer == "" { fmt.Fprintf(os.Stderr, "%s is missing maintainer field; skipping...\n", yamlPath) return } parsedCmd := &cobra.Command{ Use: filepath.Base(executablePath), RunE: execWrapper(executablePath), DisableFlagParsing: true, } if doc.ShortDesc != "" { parsedCmd.Short = doc.ShortDesc } if doc.LongDesc != "" { parsedCmd.Long = doc.LongDesc } parsedCmd.Long += fmt.Sprintf("\nMaintained by %s; reach out to them via mail or chat with questions", doc.Maintainer) if doc.Examples != "" { parsedCmd.Example = doc.Examples } targetCmd.AddCommand(parsedCmd) } func execWrapper(executablePath string) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { execCmd := exec.Command(executablePath, args...) execCmd.Stderr = os.Stderr execCmd.Stdout = os.Stdout execCmd.Stdin = os.Stdin return execCmd.Run() } } func cli() int { err := parseCommands(rootCmd, "core") if err != nil { fmt.Fprintf(os.Stderr, "failed to parse core commands: %s", err) return 1 } err = parseCommands(rootCmd, "contrib") if err != nil { fmt.Fprintf(os.Stderr, "failed to parse contrib commands: %s", err) return 1 } admin, err := isAdmin() if err != nil { fmt.Fprintf(os.Stderr, "failed to check admin status: %s", err) return 2 } if admin { rootCmd.AddCommand(adminCmd) err = parseCommands(adminCmd, "admin") if err != nil { fmt.Fprintf(os.Stderr, "failed to parse admin commands: %s", err) return 1 } } // I feel like the example/documentation yaml can be frontmatter for non-binary files. to start // i'll just do the accompanying yaml file. rootCmd.Execute() return 0 } func main() { os.Exit(cli()) }