package main import ( // External "gopkg.in/yaml.v2" // Internal "launcher/log" // Standard "fmt" "io/ioutil" "os/exec" "regexp" ) // Create reads the file and parses its YAML content. func CreateCommandSet(fName string) (*CommandSet, error) { var commandSet CommandSet fileContent, err := ioutil.ReadFile(fName) if err != nil { return nil, err } err = yaml.UnmarshalStrict(fileContent, &commandSet) if err != nil { return nil, err } return &commandSet, nil } // Dump marshals the struct and outputs it. func (cs *CommandSet) Dump() { if str, err := yaml.Marshal(cs); err != nil { fmt.Println(err) } else { fmt.Printf("%s\n", str) } } // GetCommand returns the commands with names matching the given regex. func (cs *CommandSet) GetCommands(nameRegex string) ([]*CommandSet, error) { var results []*CommandSet re, err := regexp.Compile(nameRegex) if err != nil { return []*CommandSet{}, err } for _, cmd := range cs.Commands { if re.MatchString(cmd.Name) { // A commandset can be dynamic, which needs to run a script that generates YAML items. if cmd.Dynamic != "" && !cmd.completed { configData, err := exec.Command("/bin/bash", "-c", cmd.Dynamic).Output() if err != nil { if exErr, ok := err.(*exec.ExitError); ok { return nil, fmt.Errorf("Script:\n%s", exErr.Stderr) } else { return nil, err } } else { // Ignore the topmost name and add its commands instead, // to prevent a forced extra level as we need one toplevel commandset. var commandSet CommandSet err = yaml.UnmarshalStrict(configData, &commandSet) if err != nil { return nil, err } for _, commands := range commandSet.Commands { cmd.Commands = append(cmd.Commands, commands) } log.Print("dynamic data: %s", configData) cmd.completed = true } } results = append(results, cmd) } } return results, nil } // Search takes a search string, splits it and iteratively walks along the // provided commandset. func (cs *CommandSet) Search(search string) ([]SearchResult, error) { // Regex is much more flexible to search by than just string including // searched text. It also makes it possible to use ^ and $ when search // terms isn't conclusive. re := regexp.MustCompile("\\s+") words := re.Split(search, -1) if words[len(words)-1] == "" { words = words[:len(words)-1] } var results []SearchResult currCommandset := cs // We need to return everything when no search term is provided, // as the search term is more akin to a filter. if len(words) == 0 { words = []string{""} } WordLoop: for _, w := range words { if cmds, err := currCommandset.GetCommands(w); err == nil { switch len(cmds) { // No more results, search is over. case 0: results = append(results, SearchResult{ Search: w, Commands: []*CommandSet{}, }) break WordLoop // Only one result case 1: results = append(results, SearchResult{ Search: w, Commands: cmds, }) currCommandset = cmds[0] // Many results - return them to let user know what can be searched, // but the search is over. default: results = append(results, SearchResult{ Search: w, Commands: cmds, }) break WordLoop } } else { // error in regex compilation or dynamic command execution results = append(results, SearchResult{ Commands: []*CommandSet{}, Error: err.Error(), }) break } } return results, nil } // refresh runs the dynamic script, parses the input and puts it into place. func (cs *CommandSet) refresh() error { return nil }