Watch File Changes with Godo


Godo is a make tool in Golang inspired by tools like rake and gulp in other languages. Similarly, it serves to execute tasks, watch changes, and can be called directly from command line. It's a new tool and is still growing. There might not be as many tools as in gulp but you can write Go code in it to take advantage to its performance.

Install

go get -u gopkg.in/godo.v2/cmd/godo

The Basics

This is how you can write a simple script that rebuilds the Go program every time the file has changed.

  1. Create a Gododir or tasks in your working directory.
  2. Create a Godofile.go or main.go in that directory.
  3. Write the following:
package main

import (
    godo "gopkg.in/godo.v2"
)

func Tasks(p *godo.Project) {
    p.Task("build", nil, func(c *godo.Context) {
        c.Run("go build main.go")
    }).Src("**/*.go")
}

func main() {
    godo.Godo(Tasks)
}

So once any .go file has changed in your working directory, Godo will run the build command against the main.go. Then run in command line:

godo -w

which means compile the Gododir and start watching the filesystem. Without -w it will only compile and run the default task once.

Separate Tasks in Different Files

If we have more than one task and want to separate them into different files, Godo automatically includes all files in Gododir like we run go build *.go. In this case, we can create different functions in different files and call them in the Tasks().

Here's an example.

- Gododir
  - main.go
  - foo.go

and we can write as follow:

// foo.go
package main

import (
    godo "gopkg.in/godo.v2"
)

func Foo(p *godo.Project) {
    p.Task("foo", nil, func(c *godo.Context) {
        // Do something
    })
}

and call the function in Tasks():

// main.go
func Tasks(p *godo.Project) {
    Foo(p)
}

Therefore we have defined another task in another file.

Get FileEvent

Sometimes we only want to run commands against specific files, and we can take advantages of Godo's FileEvent attribute under the context.

Supposedly we're testing a set of files under one directory when any .go file is changed in this directory. The FileEvent returns a FileEvent struct, which we can call Path to retrieve the full path. For example:

p.Task("test", nil, func(c *godo.Context) {
        if c.FileEvent != nil {
            fmt.Println(c.FileEvent.Path) // => /path/to/this/file.go
        }
    }).Src("**/*.go")
}

Beware that in complie stage, there will be no FileEvent because nothing is triggered at that time. In this case we only run the code if FileEvent is not nil.

A full example will be this:

package main

import (
    "bytes"
    godo "gopkg.in/godo.v2"
    "io/ioutil"
    "log"
    "strings"
)

func Tasks(p *godo.Project) {
    // If any file is changed in one directory, run go test against the directory
    p.Task("test", nil, func(c *godo.Context) {
        if c.FileEvent != nil {
            c.Run("go test " + GoFiles(c.FileEvent.Path))
        }
    }).Src("**/*.go")
}

// GoFiles returns all files in the directory from the eventPath.
func GoFiles(eventPath string) string {
    dir := DirFromPath(eventPath)
    list, err := ioutil.ReadDir(dir)
    if err != nil {
        log.Fatal(err)
    }

    var files bytes.Buffer
    for _, file := range list {
        if strings.Contains(file.Name(), ".go") {
            files.WriteString(dir + file.Name() + " ")
        }
    }
    return files.String()
}

// DirFromPath removes the file from the path and returns the directory only.
func DirFromPath(path string) string {
    slice := strings.Split(path, "/")
    dir := "/" + strings.Join(slice[1:len(slice)-1], "/") + "/"
    return dir
}


func main() {
    godo.Godo(Tasks)
}

Run godo test -w to watch it and we're done!