Index > github.com/kt3k/saku/exec_unix.go (46.7%)
// +build !windows package main import ( "os" "syscall" "os/exec" ) func execCommand(command string) *exec.Cmd { cmd := exec.Command("/bin/sh", "-c", command) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Setsid = true cmd.Env = append(os.Environ(), "IN_SAKU=true") return cmd } func terminateCommand(cmd *exec.Cmd) error { if cmd == nil { return nil } if cmd.Process == nil { return nil } group, err := os.FindProcess(-1 * cmd.Process.Pid) if err == nil { group.Signal(syscall.SIGKILL) } return err }
Index > github.com/kt3k/saku/main.go (92.3%)
package main import ( "fmt" "os" "strconv" "strings" "github.com/fatih/color" "github.com/simonleung8/flags" ) func main() { cwd, _ := os.Getwd() os.Exit(int(run(cwd, os.Args[1:]...))) } func run(cwd string, args ...string) exitCode { fc := flags.New() fc.NewBoolFlag("help", "h", "Show the help message and exits.") fc.NewBoolFlag("version", "v", "") fc.NewBoolFlag("parallel", "p", "Runs tasks in parallel.") fc.NewBoolFlag("race", "r", "") fc.NewBoolFlag("serial", "s", "") fc.NewStringFlagWithDefault("config", "c", "", defaultConfigFile) err := fc.Parse(args...) if err != nil { fmt.Println(color.RedString("Error:"), err) return exitCodeError } if fc.Bool("help") { usage() return exitCodeOk } if fc.Bool("version") { fmt.Printf("saku@%s\n", Version) return exitCodeOk } configFile := fc.String("config") config, err1 := readConfig(cwd, configFile) if err1 != nil { if configFile != defaultConfigFile { fmt.Println(color.RedString("Error:"), "File not found:", configFile) } else { fmt.Println(color.RedString("Error:"), "File not found:", configFile) fmt.Println(" And <!-- saku start --><!-- saku end --> directive not found in README.md as well") } return exitCodeError } tasks := ParseConfig(&config) titles := fc.Args() if len(titles) == 0 { fmt.Println("There are", color.MagentaString(strconv.Itoa(len(tasks.tasks))), "task(s)") for _, t := range tasks.tasks { fmt.Println(" " + color.CyanString("["+t.title+"]")) if len(t.descriptions) == 0 { fmt.Println(" " + color.New(color.Italic).Sprint("No description")) } for _, desc := range t.descriptions { fmt.Println(" " + desc) } } return exitCodeOk } for _, title := range titles { _, ok := tasks.getByTitle(title) if !ok { fmt.Println(color.RedString("Error:"), "Task not defined:", title) return exitCodeError } } runOpts := &runOptions{cwd: "", fc: fc} if runOpts.isSerialAndParallel() { fmt.Println(color.RedString("Error:"), "both --serial and --parallel options are specified") return exitCodeError } runTasks := tasks.filterByTitles(titles) fmt.Print(color.CyanString("[saku]"), " Run ", color.MagentaString(strings.Join(titles, ", "))) if len(titles) > 1 { fmt.Print(" ", runOpts.runLabel()) } fmt.Println() err0 := runTasks.Run(runOpts) if err0 != nil { fmt.Println(color.RedString("Error:"), err0) return exitCodeError } else { fmt.Print(color.CyanString("[saku]")) if !invokedInSaku() { fmt.Print(" ", prependEmoji("✨", "Finish ")) } else { fmt.Print(" Finish ") } fmt.Print(color.MagentaString(strings.Join(titles, ", "))) if len(titles) > 1 { fmt.Print(" ", runOpts.runLabel()) } fmt.Println() } return exitCodeOk }
Index > github.com/kt3k/saku/parse_config.go (100.0%)
package main import ( "gopkg.in/russross/blackfriday.v2" "strings" ) // Parses config markdown and returns tasks. func ParseConfig(config *[]byte) *TaskCollection { tasks := newTaskCollection() node := blackfriday.New().Parse(*config).FirstChild for node != nil { if node.Type == blackfriday.Heading { /* Heading > Text */ title := string(node.FirstChild.Literal) tasks.newTask() tasks.setCurrentTaskTitle(title) } else if node.Type == blackfriday.BlockQuote { /* BlockQuote > Paragraph */ p := node.FirstChild for p != nil { /* Paragraph > Text */ description := string(p.FirstChild.Literal) for _, desc := range strings.Split(description, "\n") { tasks.addCurrentTaskDescription(desc) } p = p.Next } } else if node.Type == blackfriday.CodeBlock { /* CodeBlock > Text */ code := string(node.Literal) commands := strings.Split(code, "\n") for _, command := range commands { if strings.Trim(command, " \t\r") != "" { tasks.addCurrentTaskCommands([]string{command}) } } } node = node.Next } return tasks }
Index > github.com/kt3k/saku/read_config.go (93.3%)
package main import ( "errors" "fmt" "io/ioutil" "path/filepath" "regexp" "github.com/fatih/color" ) const defaultConfigFile = "saku.md" var patternEmbedDirective = regexp.MustCompile(`(?ism)<!--\s*saku\s+start\s*-->(.*)<!--\s*saku\s+end\s*-->`) // Reads task config from markdown files func readConfig(cwd string, configFile string) ([]byte, error) { absPath := filepath.Join(cwd, configFile) data, err := ioutil.ReadFile(absPath) if err == nil { if !invokedInSaku() { fmt.Println("Read", prependEmoji("🔎", color.MagentaString(absPath))) } return data, nil } if configFile != defaultConfigFile { return []byte{}, err } absPath = filepath.Join(cwd, "README.md") data, err = ioutil.ReadFile(absPath) if err != nil { return []byte{}, err } if !patternEmbedDirective.Match(data) { return []byte{}, errors.New("No <!-- saku start --><!-- saku end --> directive found") } return patternEmbedDirective.FindSubmatch(data)[1], nil }
Index > github.com/kt3k/saku/run_options.go (37.5%)
package main import ( "github.com/fatih/color" "github.com/simonleung8/flags" ) type runOptions struct { cwd string fc flags.FlagContext } func (r *runOptions) runLabel() string { if r.isParallel() { return "in " + color.CyanString("parallel") } else if r.isRace() { return "in " + color.CyanString("parallel-race") } else { return "in " + color.CyanString("sequence") } } func (r *runOptions) isSerialAndParallel() bool { return r.fc.Bool("serial") && r.fc.Bool("parallel") } func (r *runOptions) isParallel() bool { return r.fc.Bool("parallel") && !r.fc.Bool("race") } func (r *runOptions) isRace() bool { return r.fc.Bool("parallel") && r.fc.Bool("race") }
Index > github.com/kt3k/saku/task.go (66.7%)
package main import ( "errors" "fmt" "os/exec" "github.com/fatih/color" ) type task struct { title string descriptions []string commands []string options taskOptions aborted bool cmd *exec.Cmd } func newTask() task { return task{ title: "", descriptions: []string{}, commands: []string{}, options: taskOptions{}, aborted: false, cmd: nil, } } type taskOptions struct { } // Runs a task. func (t *task) run(opts *runOptions, c chan error) { for _, command := range t.commands { if t.aborted { c <- nil return } fmt.Println("+" + command) t.cmd = execCommand(command) if t.cmd.Run() != nil { c <- errors.New("Task " + color.MagentaString(t.title) + " failed") return } } c <- nil } // Aborts a task. func (t *task) abort() { if t.aborted { return } terminateCommand(t.cmd) t.aborted = true } // Adds the description. func (t *task) addDescription(description string) { t.descriptions = append(t.descriptions, description) } // Sets the title. func (t *task) setTitle(title string) { t.title = title } // Adds the code. func (t *task) addCommands(commands []string) { t.commands = append(t.commands, commands...) }
Index > github.com/kt3k/saku/task_collection.go (58.7%)
package main // Task collection model. type TaskCollection struct { currentTask *task tasks []*task taskMap map[string]*task } // Creates a new task collection. func newTaskCollection() *TaskCollection { // This is a dummy task, and will be discarded when the first task is created t := newTask() return &TaskCollection{ currentTask: &t, tasks: []*task{}, taskMap: map[string]*task{}, } } func (tc *TaskCollection) Run(opts *runOptions) error { if opts.isParallel() { return tc.runParallel(opts) } else if opts.isRace() { return tc.runInRace(opts) } return tc.runSequentially(opts) } func (tc *TaskCollection) runSequentially(opts *runOptions) error { c := make(chan error) for _, t := range tc.tasks { go t.run(opts, c) err := <-c if err != nil { return err } } return nil } func (tc *TaskCollection) runParallel(opts *runOptions) error { c := make(chan error) for i := range tc.tasks { t := tc.tasks[i] go t.run(opts, c) } for range tc.tasks { err := <-c if err != nil { tc.abort() return err } } return nil } func (tc *TaskCollection) runInRace(opts *runOptions) error { c := make(chan error) for i := range tc.tasks { go tc.tasks[i].run(opts, c) } defer tc.abort() return <-c } func (tc *TaskCollection) abort() { for _, t := range tc.tasks { t.abort() } } func (tc *TaskCollection) newTask() { t := newTask() tc.tasks = append(tc.tasks, &t) tc.currentTask = &t } func (tc *TaskCollection) setCurrentTaskTitle(title string) { tc.currentTask.setTitle(title) tc.taskMap[title] = tc.currentTask } func (tc *TaskCollection) addCurrentTaskDescription(description string) { tc.currentTask.addDescription(description) } func (tc *TaskCollection) addCurrentTaskCommands(commands []string) { tc.currentTask.addCommands(commands) } func (tc *TaskCollection) filterByTitles(titles []string) *TaskCollection { tasks := []*task{} taskMap := map[string]*task{} for _, title := range titles { tasks = append(tasks, tc.taskMap[title]) taskMap[title] = tc.taskMap[title] } return &TaskCollection{ currentTask: tasks[len(tasks)-1], tasks: tasks, taskMap: taskMap, } } // Gets a task by the given title. func (tc *TaskCollection) getByTitle(title string) (*task, bool) { t, ok := tc.taskMap[title] return t, ok }
Index > github.com/kt3k/saku/usage.go (100.0%)
package main
import (
"fmt"
"github.com/fatih/color"
)
// Shows the help message.
func usage() {
fmt.Printf(`
Usage: %s [options] <task, ...> [-- extra-options]
Options:
-v, --version - - - Shows the version number and exits.
-h, --help - - - - - Shows the help message and exits.
-i, --info - - - - - Shows the task information and exits.
-p, --parallel - - - Runs tasks in parallel. Default false.
-s, --sequential - - Runs tasks in serial. Default true.
-c, --config <path> - Specifies the config file. Default is 'saku.md'.
-r, --race - - - - - Set the flag to kill all tasks when a task
finished with zero. This option is valid only
with 'parallel' option.
-q, --quiet - - - - Stops the logging.
The extra options after '--' are passed to each task command.
`, color.CyanString("saku"))
}
Index > github.com/kt3k/saku/util.go (25.0%)
package main import ( "github.com/mattn/go-isatty" "os" ) // Returns the string prepended by the given emoji when the terminal is tty, otherwise drops emoji and returns the string. func prependEmoji(e string, str string) string { if !isatty.IsTerminal(os.Stdout.Fd()) { return str } return e + " " + str } // Returns true if the process is invoked in saku. func invokedInSaku() bool { return os.Getenv("IN_SAKU") == "true" }