<|start_filename|>Makefile<|end_filename|>
VERSION ?= $(shell cat VERSION)

GITCOMMIT := $(shell git rev-parse HEAD 2>/dev/null)
GITBRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
BUILDTIME := $(shell TZ=GMT date "+%Y-%m-%d_%H:%M_GMT")

SRCS = $(shell find . -name '*.go' | grep -v '^./vendor/')
PKGS := $(foreach pkg, $(sort $(dir $(SRCS))), $(pkg))

TESTARGS ?=

default:
	GO15VENDOREXPERIMENT=1 go build -v

install:
	cp rocker-compose /usr/local/bin/rocker-compose
	chmod +x /usr/local/bin/rocker-compose

cross: dist_dir
	docker run --rm -ti -v $(shell pwd):/go/src/github.com/grammarly/rocker-compose \
		-e GOOS=linux -e GOARCH=amd64 -e GO15VENDOREXPERIMENT=1 -e GOPATH=/go \
		-w /go/src/github.com/grammarly/rocker-compose \
		dockerhub.grammarly.io/golang-1.5.1-cross:v1 go build \
		-ldflags "-X main.Version=$(VERSION) -X main.GitCommit=$(GITCOMMIT) -X main.GitBranch=$(GITBRANCH) -X main.BuildTime=$(BUILDTIME)" \
		-v -o ./dist/linux_amd64/rocker-compose

	docker run --rm -ti -v $(shell pwd):/go/src/github.com/grammarly/rocker-compose \
		-e GOOS=darwin -e GOARCH=amd64 -e GO15VENDOREXPERIMENT=1 -e GOPATH=/go \
		-w /go/src/github.com/grammarly/rocker-compose \
		dockerhub.grammarly.io/golang-1.5.1-cross:v1 go build \
		-ldflags "-X main.Version=$(VERSION) -X main.GitCommit=$(GITCOMMIT) -X main.GitBranch=$(GITBRANCH) -X main.BuildTime=$(BUILDTIME)" \
		-v -o ./dist/darwin_amd64/rocker-compose

cross_tars: cross
	COPYFILE_DISABLE=1 tar -zcvf ./dist/rocker-compose_linux_amd64.tar.gz -C dist/linux_amd64 rocker-compose
	COPYFILE_DISABLE=1 tar -zcvf ./dist/rocker-compose_darwin_amd64.tar.gz -C dist/darwin_amd64 rocker-compose

dist_dir:
	mkdir -p ./dist/linux_amd64
	mkdir -p ./dist/darwin_amd64

clean:
	rm -Rf dist

testdeps:
	@ go get github.com/GeertJohan/fgt

fmtcheck:
	$(foreach file,$(SRCS),gofmt $(file) | diff -u $(file) - || exit;)

lint:
	@ go get github.com/golang/lint/golint
	$(foreach file,$(SRCS),fgt golint $(file) || exit;)

vet:
	@ go get golang.org/x/tools/cmd/vet
	$(foreach pkg,$(PKGS),fgt go vet $(pkg) || exit;)

gocyclo:
	@ go get github.com/fzipp/gocyclo
	gocyclo -over 25 ./src

test: testdeps fmtcheck lint vet
	GO15VENDOREXPERIMENT=1 go test ./src/... $(TESTARGS)

version:
	@echo $(VERSION)

.PHONY: clean test fmtcheck lint vet gocyclo version testdeps cross cross_tars dist_dir default install


<|start_filename|>ahoy.go<|end_filename|>
package main

import (
	"errors"
	"flag"
	"fmt"
	"github.com/codegangsta/cli"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"sort"
	"strings"
)

// Config handles the overall configuration in an ahoy.yml file
// with one Config per file.
type Config struct {
	Usage      string
	AhoyAPI    string
	Commands   map[string]Command
	Entrypoint []string
}

// Command is an ahoy command detailed in ahoy.yml files. Multiple
// commands can be defined per ahoy.yml file.
type Command struct {
	Description string
	Usage       string
	Cmd         string
	Hide        bool
	Imports     []string
}

var app *cli.App
var sourcefile string
var args []string
var verbose bool
var bashCompletion bool

//The build version can be set using the go linker flag `-ldflags "-X main.version=$VERSION"`
//Complete command: `go build -ldflags "-X main.version=$VERSION"`
var version string

// AhoyConf stores the global config.
var AhoyConf struct {
	srcDir  string
	srcFile string
}

func logger(errType string, text string) {
	errText := ""
	// Disable the flags which add date and time for instance.
	log.SetFlags(0)
	if errType != "debug" {
		errText = "[" + errType + "] " + text + "\n"
		log.Println(errText)
	}

	if errType == "fatal" {
		os.Exit(1)
	}
}

func getConfigPath(sourcefile string) (string, error) {
	var err error
	var config = ""

	// If a specific source file was set, then try to load it directly.
	if sourcefile != "" {
		if _, err := os.Stat(sourcefile); err == nil {
			return sourcefile, err
		}
		err = errors.New("An ahoy config file was specified using -f to be at " + sourcefile + " but couldn't be found. Check your path.")
		return config, err
	}

	dir, err := os.Getwd()
	if err != nil {
		return config, err
	}
	for dir != "/" && err == nil {
		ymlpath := filepath.Join(dir, ".ahoy.yml")
		//log.Println(ymlpath)
		if _, err := os.Stat(ymlpath); err == nil {
			logger("debug", "Found .ahoy.yml at "+ymlpath)
			return ymlpath, err
		}
		// Chop off the last part of the path.
		dir = path.Dir(dir)
	}
	logger("debug", "Can't find a .ahoy.yml file.")
	return "", err
}

func getConfig(file string) (Config, error) {
	var config = Config{}
	yamlFile, err := ioutil.ReadFile(file)
	if err != nil {
		err = errors.New("an ahoy config file couldn't be found in your path. You can create an example one by using 'ahoy init'")
		return config, err
	}

	// Extract the yaml file into the config varaible.
	err = yaml.Unmarshal(yamlFile, &config)
	if err != nil {
		return config, err
	}

	// All ahoy files (and imports) must specify the ahoy version.
	// This is so we can support backwards compatability in the future.
	if config.AhoyAPI != "v2" {
		err = errors.New("Ahoy only supports API version 'v2', but '" + config.AhoyAPI + "' given in " + sourcefile)
		return config, err
	}

	if config.Entrypoint == nil {
		config.Entrypoint = []string{"bash", "-c", "{{cmd}}", "{{name}}"}
	}

	return config, err
}

func getSubCommands(includes []string) []cli.Command {
	subCommands := []cli.Command{}
	if 0 == len(includes) {
		return subCommands
	}
	commands := map[string]cli.Command{}
	for _, include := range includes {
		if len(include) == 0 {
			continue
		}
		if include[0] != "/"[0] || include[0] != "~"[0] {
			include = filepath.Join(AhoyConf.srcDir, include)
		}
		if _, err := os.Stat(include); err != nil {
			//Skipping files that cannot be loaded allows us to separate
			//subcommands into public and private.
			continue
		}
		config, _ := getConfig(include)
		includeCommands := getCommands(config)
		for _, command := range includeCommands {
			commands[command.Name] = command
		}
	}

	var names []string
	for k := range commands {
		names = append(names, k)
	}
	sort.Strings(names)
	for _, name := range names {
		subCommands = append(subCommands, commands[name])
	}
	return subCommands
}

func getCommands(config Config) []cli.Command {
	exportCmds := []cli.Command{}

	var keys []string
	for k := range config.Commands {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, name := range keys {
		cmd := config.Commands[name]

		// Check that a command has 'cmd' OR 'imports' set.
		if cmd.Cmd == "" && cmd.Imports == nil {
			logger("fatal", "Command ["+name+"] has neither 'cmd' or 'imports' set. Check your yaml file.")
		}

		// Check that a command has 'cmd' OR 'imports' set.
		if cmd.Cmd != "" && cmd.Imports != nil {
			logger("fatal", "Command ["+name+"] has both 'cmd' and 'imports' set, but only one is allowed. Check your yaml file.")
		}

		// Check that a command with 'imports' set has a least one entry.
		if cmd.Imports != nil && len(cmd.Imports) == 0 {
			logger("fatal", "Command ["+name+"] has 'imports' set, but it is empty. Check your yaml file.")
		}

		newCmd := cli.Command{
			Name:            name,
			SkipFlagParsing: true,
			HideHelp:        cmd.Hide,
		}

		if cmd.Usage != "" {
			newCmd.Usage = cmd.Usage
		}

		if cmd.Cmd != "" {
			newCmd.Action = func(c *cli.Context) {
				// For some unclear reason, if we don't add an item at the end here,
				// the first argument is skipped... actually it's not!
				// 'bash -c' says that arguments will be passed starting with $0, which also means that
				// $@ skips the first item. See http://stackoverflow.com/questions/41043163/xargs-sh-c-skipping-the-first-argument
				var cmdItems []string
				var cmdArgs []string
				var cmdEntrypoint []string

				// c.Args()  is not a slice apparently.
				for _, arg := range c.Args() {
					cmdArgs = append(cmdArgs, arg)
				}

				// Replace the entry point placeholders.
				cmdEntrypoint = config.Entrypoint[:]
				for i := range cmdEntrypoint {
					if cmdEntrypoint[i] == "{{cmd}}" {
						cmdEntrypoint[i] = cmd.Cmd
					} else if cmdEntrypoint[i] == "{{name}}" {
						cmdEntrypoint[i] = c.Command.Name
					}
				}
				cmdItems = append(cmdEntrypoint, cmdArgs...)

				if verbose {
					log.Println("===> AHOY", name, "from", sourcefile, ":", cmdItems)
				}
				command := exec.Command(cmdItems[0], cmdItems[1:]...)
				command.Dir = AhoyConf.srcDir
				command.Stdout = os.Stdout
				command.Stdin = os.Stdin
				command.Stderr = os.Stderr
				if err := command.Run(); err != nil {
					fmt.Fprintln(os.Stderr)
					os.Exit(1)
				}
			}
		}

		if cmd.Imports != nil {
			subCommands := getSubCommands(cmd.Imports)
			if subCommands == nil || len(subCommands) == 0 {
				logger("fatal", "Command ["+name+"] has 'imports' set, but no commands were found. Check your yaml file.")
			}
			newCmd.Subcommands = subCommands
		}

		//log.Println("found command: ", name, " > ", cmd.Cmd )
		exportCmds = append(exportCmds, newCmd)
	}

	return exportCmds
}

func addDefaultCommands(commands []cli.Command) []cli.Command {

	defaultInitCmd := cli.Command{
		Name:  "init",
		Usage: "Initialize a new .ahoy.yml config file in the current directory.",
		Action: func(c *cli.Context) {
			// Grab the URL or use a default for the initial ahoy file.
			// Allows users to define their own files to call to init.
			var wgetURL = "https://raw.githubusercontent.com/ahoy-cli/ahoy/master/examples/examples.ahoy.yml"
			if len(c.Args()) > 0 {
				wgetURL = c.Args()[0]
			}
			grabYaml := "wget " + wgetURL + " -O .ahoy.yml"
			cmd := exec.Command("bash", "-c", grabYaml)
			cmd.Stdin = os.Stdin
			cmd.Stderr = os.Stderr
			if err := cmd.Run(); err != nil {
				fmt.Fprintln(os.Stderr)
				os.Exit(1)
			} else {
				fmt.Println("example.ahoy.yml downloaded to the current directory. You can customize it to suit your needs!")
			}
		},
	}

	// Don't add default commands if they've already been set.
	if c := app.Command(defaultInitCmd.Name); c == nil {
		commands = append(commands, defaultInitCmd)
	}
	return commands
}

//TODO Move these to flag.go?
func init() {
	logger("debug", "init()")
	flag.StringVar(&sourcefile, "f", "", "specify the sourcefile")
	flag.BoolVar(&bashCompletion, "generate-bash-completion", false, "")
	flag.BoolVar(&verbose, "verbose", false, "")
}

// BashComplete prints the list of subcommands as the default app completion method
func BashComplete(c *cli.Context) {
	logger("debug", "BashComplete()")

	if sourcefile != "" {
		log.Println(sourcefile)
		os.Exit(0)
	}
	for _, command := range c.App.Commands {
		for _, name := range command.Names() {
			fmt.Fprintln(c.App.Writer, name)
		}
	}
}

// NoArgsAction is the application wide default action, for when no flags or arguments
// are passed or when a command doesn't exist.
// Looks like -f flag still works through here though.
func NoArgsAction(c *cli.Context) {
	args := c.Args()
	if len(args) > 0 {
		msg := "Command not found for '" + strings.Join(args, " ") + "'"
		logger("fatal", msg)
	}

	cli.ShowAppHelp(c)

	if AhoyConf.srcFile == "" {
		logger("error", "No .ahoy.yml found. You can use 'ahoy init' to download an example.")
	}

	if !c.Bool("help") || !c.Bool("version") {
		logger("fatal", "Missing flag or argument.")
	}

	// Looks like we never reach here.
	fmt.Println("ERROR: NoArg Action ")
}

// BeforeCommand runs before every command so arguments or flags must be passed
func BeforeCommand(c *cli.Context) error {
	args := c.Args()
	if c.Bool("version") {
		fmt.Println(version)
		return errors.New("don't continue with commands")
	}
	if c.Bool("help") {
		if len(args) > 0 {
			cli.ShowCommandHelp(c, args.First())
		} else {
			cli.ShowAppHelp(c)
		}
		return errors.New("don't continue with commands")
	}
	//fmt.Printf("%+v\n", args)
	return nil
}

func setupApp(localArgs []string) *cli.App {
	var err error
	initFlags(localArgs)
	// cli stuff
	app = cli.NewApp()
	app.Action = NoArgsAction
	app.Before = BeforeCommand
	app.Name = "ahoy"
	app.Version = version
	app.Usage = "Creates a configurable cli app for running commands."
	app.EnableBashCompletion = true
	app.BashComplete = BashComplete
	overrideFlags(app)

	AhoyConf.srcFile, err = getConfigPath(sourcefile)
	if err != nil {
		logger("fatal", err.Error())
	} else {
		AhoyConf.srcDir = filepath.Dir(AhoyConf.srcFile)
		// If we don't have a sourcefile, then just supply the default commands.
		if AhoyConf.srcFile == "" {
			app.Commands = addDefaultCommands(app.Commands)
			app.Run(os.Args)
			os.Exit(0)
		}
		config, err := getConfig(AhoyConf.srcFile)
		if err != nil {
			logger("fatal", err.Error())
		}
		app.Commands = getCommands(config)
		app.Commands = addDefaultCommands(app.Commands)
		if config.Usage != "" {
			app.Usage = config.Usage
		}
	}

	cli.AppHelpTemplate = `NAME:
   {{.Name}} - {{.Usage}}
USAGE:
   {{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
   {{if len .Authors}}
AUTHOR(S):
   {{range .Authors}}{{ . }}{{end}}
   {{end}}{{if .Commands}}
COMMANDS:
{{range .Commands}}{{if not .HideHelp}}   {{join .Names ", "}}{{ if len .Subcommands }}{{" \u25BC"}}{{end}}{{ "\t" }}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .Flags}}
GLOBAL OPTIONS:
   {{range .Flags}}{{.}}
   {{end}}{{end}}{{if .Copyright }}
COPYRIGHT:
   {{.Copyright}}
   {{end}}{{if .Version}}
VERSION:
   {{.Version}}
   {{end}}
`

	return app
}

func main() {
	logger("debug", "main()")
	app = setupApp(os.Args[1:])
	app.Run(os.Args)
}


<|start_filename|>ahoy_test.go<|end_filename|>
package main

import (
	"fmt"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"os"
	"testing"
)

func TestOverrideExample(t *testing.T) {
	expected := "Overrode you.\n"
	actual, _ := appRun([]string{"ahoy", "-f", "testdata/override-base.ahoy.yml", "docker", "override-example"})
	if expected != actual {
		t.Errorf("ahoy docker override-example: expected - %s; actual - %s", string(expected), string(actual))
	}
}

func TestGetCommands(t *testing.T) {
	// Get Command with no sub Commands.
	config := Config{
		Usage:   "Test getSubCommands Usage.",
		AhoyAPI: "v2",
		Commands: map[string]Command{
			"test-command": Command{
				Description: "Testing example Command.",
				Usage:       "test-command a",
				Cmd:         "echo a.ahoy.yml",
				Hide:        false,
			},
		},
	}

	commands := getCommands(config)

	if len(commands) != 1 {
		t.Error("Expect that getCommands can get one command if passed config with one command.")
	}
}

func TestGetSubCommand(t *testing.T) {
	// Since we're not running the app directly, sourcedir doesn't get reset, so
	// we need to reset it ourselves. TODO: Remove these globals somehow.
	AhoyConf.srcDir = ""

	// When empty return empty list of commands.

	actual := getSubCommands([]string{})

	if len(actual) != 0 {
		t.Error("Expect that getSubCommands([]string) returns []Command{}")
	}

	// List of bogus or empty strings returns empty list of commands.
	actual = getSubCommands([]string{
		"./testing/bogus1.ahoy.yml",
		"./testing/private.ahoy.yml",
	})

	if len(actual) != 0 {
		t.Error("Expect that getSubCommands([]string) returns []Command{}")
	}

	// Commands with same name are merged, last one wins.
	err := os.MkdirAll("testing", 0755)
	if err != nil {
		t.Error("Something went wrong creating the 'testing' directory")
	}

	file1, err := os.Create("testing/a.ahoy.yml")
	if err != nil {
		t.Error("Something went wrong with the file creation - file1.")
	}

	file2, err := os.Create("testing/b.ahoy.yml")
	if err != nil {
		t.Error("Something went wrong with the file creation - file2.")
	}

	yamlConfigA := `
ahoyapi: v2
commands:
  test-command:
    description: Testing example Command.
    usage: test-command a
    cmd: echo "test"
    hide: false
`
	yamlConfigB := `
ahoyapi: v2
commands:
  test-command:
    description: Testing example Command.
    usage: test-command b
    cmd: echo "test"
    hide: false
`
	_, err = file1.Write([]byte(yamlConfigA))
	if err != nil {
		t.Error("Error writing to file1.")
	}

	_, err = file2.Write([]byte(yamlConfigB))
	if err != nil {
		t.Error("Error writing to file2.")
	}

	actual = getSubCommands([]string{
		"./testing/a.ahoy.yml",
		"./testing/b.ahoy.yml",
	})

	if len(actual) != 1 {
		t.Error("Sourcedir:", AhoyConf.srcDir)
		t.Error("Failed: expect that two commands with the same name get merged into one.", actual)
	}

	if len(actual) > 0 && actual[0].Usage != "test-command b" {
		t.Error("Failed: expect that when multiple commands are merged, last one wins.", actual)
	}

	// Test commands with different names do not get merged.
	file3, err := os.Create("testing/c.ahoy.yml")
	if err != nil {
		t.Error("Something went wrong with the file creation - file3.")
	}

	//logger("fatal", "test")
	yamlConfigC := `
ahoyapi: v2
commands:
  test-new-command:
    description: Testing new example Command.
    usage: test-new-command a
    cmd: "echo new a.ahoy.yml"
    hide: false
`
	_, err = file3.Write([]byte(yamlConfigC))
	if err != nil {
		t.Error("Error writing to file3.")
	}

	actual = getSubCommands([]string{
		"./testing/a.ahoy.yml",
		"./testing/b.ahoy.yml",
		"./testing/c.ahoy.yml",
	})

	if len(actual) != 2 {
		fmt.Printf("x = %#v \n", actual)
		t.Error("Failed: expect unique commands to be captured separately.", "commands found", actual)
	}

	file1.Close()
	file2.Close()
	file3.Close()
	os.RemoveAll("testing")
}

func TestGetConfig(t *testing.T) {
	testFile, err := os.Create("test_getConfig.yml")

	if err != nil {
		t.Error("Something went wrong creating the test file.")
	}

	expected := Config{
		Usage:   "Test example usage.",
		AhoyAPI: "v2",
		Commands: map[string]Command{
			"test-command": Command{
				Description: "Testing example Command.",
				Usage:       "test-command",
				Cmd:         "echo 'Hello World'",
				Hide:        false,
				Imports: []string{
					"./path/a",
					"./path/b",
				},
			},
		},
	}
	testYaml, err := yaml.Marshal(expected)

	if err != nil {
		t.Error("Something went wrong mashelling the test object.")
	}

	testFile.Write([]byte(testYaml))

	config, err := getConfig("test_getConfig.yml")

	if err != nil {
		t.Error("Something went wrong trying to load the config file.")
	}

	if config.Usage != expected.Usage {
		t.Errorf("Expected config.Usage to be %s, but actaul is %s", expected.Usage, config.Usage)
	}

	if config.Commands["test-command"].Cmd != expected.Commands["test-command"].Cmd {
		t.Errorf("Expected config.Commands['test-command'].cmd to be %s, but actaul is %s", expected.Commands["test-command"].Cmd, config.Commands["test-command"].Cmd)
	}

	testFile.Close()
	os.Remove("test_getConfig.yml")
}

func TestGetConfigPath(t *testing.T) {
	// Passinng empty string.
	pwd, _ := os.Getwd()
	expected := pwd + "/.ahoy.yml"
	actual, _ := getConfigPath("")
	if expected != actual {
		t.Errorf("ahoy docker override-example: expected - %s; actual - %s", string(expected), string(actual))
	}

	// Passing known path works as expected
	expected = pwd + "/.ahoy.yml"
	actual, _ = getConfigPath(expected)

	if expected != actual {
		t.Errorf("ahoy docker override-example: expected - %s; actual - %s", string(expected), string(actual))
	}

	// TODO: Passing directory should return default
}

func TestGetConfigPathErrorOnBogusPath(t *testing.T) {
	_, err := getConfigPath("~/bogus/path")
	if err == nil {
		t.Error("getConfigPath did not fail when passed a bogus path.")
	}
}

func appRun(args []string) (string, error) {
	stdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	setupApp(args[1:])
	app.Run(args)

	w.Close()
	//@aashil thinks this reads from the command line
	out, _ := ioutil.ReadAll(r)
	os.Stdout = stdout
	return string(out), nil
}


<|start_filename|>flag.go<|end_filename|>
package main

import (
	"flag"
	"github.com/codegangsta/cli"
)

var globalFlags = []cli.Flag{
	cli.BoolFlag{
		Name:        "verbose, v",
		Usage:       "Output extra details like the commands to be run.",
		EnvVar:      "AHOY_VERBOSE",
		Destination: &verbose,
	},
	cli.StringFlag{
		Name:        "file, f",
		Usage:       "Use a specific ahoy file.",
		Destination: &sourcefile,
	},
	cli.BoolFlag{
		Name:  "help, h",
		Usage: "show help",
	},
	cli.BoolFlag{
		Name:  "version",
		Usage: "print the version",
	},
	cli.BoolFlag{
		Name: "generate-bash-completion",
	},
}

func flagSet(name string, flags []cli.Flag) *flag.FlagSet {
	set := flag.NewFlagSet(name, flag.ContinueOnError)

	for _, f := range flags {
		f.Apply(set)
	}
	return set
}

func initFlags(incomingFlags []string) {

	// Reset the sourcedir for when we're testing. Otherwise the global state
	// is preserved between the tests.
	AhoyConf.srcDir = ""

	// Grab the global flags first ourselves so we can customize the yaml file loaded.
	// Flags are only parsed once, so we need to do this before cli has the chance to?
	tempFlags := flagSet("tempFlags", globalFlags)
	tempFlags.Parse(incomingFlags)
}

func overrideFlags(app *cli.App) {
	app.Flags = globalFlags
	app.HideVersion = true
	app.HideHelp = true
}
