Selaa lähdekoodia

qst has grown up, it's now at heyLu/qst.

already?! that was fast...
Lucas Stadler 11 vuotta sitten
vanhempi
commit
16737815b1

+ 1 - 6
go/.gitignore

1
.go
2
3
qst
4
5
examples/hello
6
examples/hello_web
1
.go

+ 4 - 32
go/README.md

14
14
15
## qst - run things quickly (and easily)
15
## qst - run things quickly (and easily)
16
16
17
`qst` has already grown up, it now lives [in it's own place](https://github.com/heyLu/qst).
18
You can get it using `go get github.com/heyLu/qst`.
19
17
intended to be run in unfamilar environments, you pass it a file or a
20
intended to be run in unfamilar environments, you pass it a file or a
18
directory and it tries to detect what it is and how to run it.
21
directory and it tries to detect what it is and how to run it.
19
22
20
- run `qst` or `qst -h` to see options and support project types
21
- `qst hello_world.go`: compiles and runs `hello_world.go`, rerunning
22
	after it exits or the file is saved
23
24
	quite fun for small things, just throw some code in a file, have `qst`
25
	watch and restart when appropriate.
26
- `qst -phase=test ...` runs the tests for projects that support it
27
28
### Building it yourself
29
30
	# set up $GOPATH as desired
31
	$ export GOPATH=$PWD/.go         # choose whatever you want
32
	$ go build qst
33
	...
34
	$ ./qst -h
35
	Usage: qst <file>
36
	...
37
	$ ./qst examples/hello_web.rb
38
	...
39
	^C
40
	$ ./qst examples/hello_web.go
41
	...
42
43
Try changing something in the files, it's fun. :)
44
45
### Ideas/todo
46
47
- watch many files (select by globbing)
48
- sometimes restarts twice after one save?
49
- more project types
50
- tests (how? lots of shellscripts could do it, but would be very
51
	cumbersome. current "architecture" doesn't allow mocking.)
23
run `qst .` to run anything.

+ 0 - 123
go/detect/detect.go

1
// Guess project "type" from the files present.
2
package detect
3
4
import (
5
	"errors"
6
	"path"
7
8
	"../fileutil"
9
)
10
11
type Project struct {
12
	Id       string
13
	Commands Commands
14
	Detect   Matcher
15
}
16
17
type Matcher func(string) bool
18
19
type Commands map[string]string
20
21
var ProjectTypes = []*Project{
22
	&Project{"c/default", Commands{"run": "gcc -o $(basename {file} .c) {file} && ./$(basename {file} .c)"},
23
		matchPattern("*.c")},
24
	&Project{"clojure/leiningen", Commands{"build": "lein uberjar", "run": "lein run", "test": "lein test"},
25
		matchFile("project.clj")},
26
	&Project{"coffeescript/default", Commands{"run": "coffee {file}"}, matchPattern("*.coffee")},
27
	&Project{"docker/fig", Commands{"build": "fig build", "run": "fig up"}, matchFile("fig.yml")},
28
	&Project{"docker/default", Commands{"build": "docker build ."}, matchFile("Dockerfile")},
29
	&Project{"executable", Commands{"run": "{file}"}, executableDefault},
30
	&Project{"go/default", Commands{"build": "go build {file}", "run": "go build $(basename {file}) && ./$(basename {file} .go)",
31
		"test": "go test"}, matchPattern("*.go")},
32
	&Project{"haskell/cabal", Commands{"build": "cabal build", "run": "cabal run", "test": "cabal test"},
33
		matchPattern("*.cabal")},
34
	&Project{"haskell/default", Commands{"run": "runhaskell {file}"}, haskellDefault},
35
	&Project{"idris/default", Commands{"run": "idris -o $(basename {file} .idr) {file} && ./$(basename {file} .idr)"},
36
		matchPattern("*.idr")},
37
	&Project{"java/maven", Commands{"build": "mvn compile", "test": "mvn compile test"}, matchFile("pom.xml")},
38
	&Project{"javascript/npm", Commands{"build": "npm install", "run": "npm start", "test": "npm test"},
39
		matchFile("package.json")},
40
	&Project{"javascript/meteor", Commands{"run": "meteor"}, matchFile(".meteor/.id")},
41
	&Project{"javascript/default", Commands{"run": "node {file}"}, matchPattern("*.js")},
42
	&Project{"julia/default", Commands{"run": "julia {file}"}, matchPattern("*.jl")},
43
	&Project{"python/django", Commands{"build": "python manage.py syncdb", "run": "python manage.py runserver",
44
		"test": "python manage.py test"}, matchFile("manage.py")},
45
	&Project{"python/default", Commands{"run": "python {file}"}, matchPattern("*.py")},
46
	&Project{"ruby/rails", Commands{"build": "bundle exec rake db:migrate", "run": "rails server",
47
		"test": "bundle exec rake test"}, matchFile("bin/rails")},
48
	&Project{"ruby/rake", Commands{"run": "rake", "test": "rake test"}, matchFile("Rakefile")},
49
	&Project{"ruby/default", Commands{"run": "ruby {file}"}, matchPattern("*.rb")},
50
	&Project{"rust/cargo", Commands{"build": "cargo build", "run": "cargo run", "test": "cargo test"},
51
		matchFile("Cargo.toml")},
52
	&Project{"rust/default", Commands{"run": "rustc {file} && ./$(basename {file} .rs)"}, matchPattern("*.rs")},
53
	&Project{"cmake", Commands{"build": "mkdir .build && cd .build && cmake .. && make"}, matchFile("CMakeLists.txt")},
54
	&Project{"make", Commands{"run": "make", "test": "make test"}, matchFile("Makefile")},
55
	&Project{"procfile", Commands{"run": "$(sed -n 's/^web: //p' Procfile)"}, matchFile("Procfile")},
56
}
57
58
func Detect(file string) (*Project, error) {
59
	for _, project := range ProjectTypes {
60
		if project.Detect(file) {
61
			return project, nil
62
		}
63
	}
64
65
	return nil, errors.New("no project type matches")
66
}
67
68
func DetectAll(file string) []*Project {
69
	projects := make([]*Project, 0, len(ProjectTypes))
70
71
	for _, project := range ProjectTypes {
72
		if project.Detect(file) {
73
			n := len(projects)
74
			projects = projects[0 : n+1]
75
			projects[n] = project
76
		}
77
	}
78
79
	return projects
80
}
81
82
func GetById(id string) *Project {
83
	for _, project := range ProjectTypes {
84
		if project.Id == id {
85
			return project
86
		}
87
	}
88
	return nil
89
}
90
91
func matchingFileOrDir(file string, pattern string) bool {
92
	if fileutil.IsFile(file) {
93
		_, f := path.Split(file)
94
		isMatch, _ := path.Match(pattern, f)
95
		return isMatch
96
	} else {
97
		return fileutil.MatchExists(path.Join(path.Dir(file), pattern))
98
	}
99
}
100
101
func hasFile(fileOrDir string, file string) bool {
102
	return fileutil.IsFile(fileutil.Join(fileOrDir, file))
103
}
104
105
func matchPattern(ext string) Matcher {
106
	return func(file string) bool {
107
		return matchingFileOrDir(file, ext)
108
	}
109
}
110
111
func matchFile(fileName string) Matcher {
112
	return func(file string) bool {
113
		return hasFile(file, fileName)
114
	}
115
}
116
117
func executableDefault(file string) bool {
118
	return fileutil.IsExecutable(file)
119
}
120
121
func haskellDefault(file string) bool {
122
	return matchingFileOrDir(file, "*.hs") || matchingFileOrDir(file, "*.lhs")
123
}

+ 0 - 6
go/examples/hello.c

1
#include <stdio.h>
2
3
int main(int argc, char **argv) {
4
	printf("Hello, World!\n");
5
	return 0;
6
}

+ 0 - 1
go/examples/hello.coffee

1
console.log "Hello, World!"

+ 0 - 7
go/examples/hello.go

1
package main
2
3
import "fmt"
4
5
func main() {
6
	fmt.Println("Hello, World!")
7
}

+ 0 - 1
go/examples/hello.hs

1
main = putStrLn "Hello, World!"

+ 0 - 2
go/examples/hello.idr

1
main : IO ()
2
main = putStrLn "Hello, World!"

+ 0 - 1
go/examples/hello.jl

1
println("Hello, World!")

+ 0 - 1
go/examples/hello.rb

1
puts "Hello, World!"

+ 0 - 3
go/examples/hello.rs

1
fn main() {
2
	println!("Hello, World!")
3
}

+ 0 - 17
go/examples/hello_web.go

1
package main
2
3
import "fmt"
4
import "net/http"
5
6
func main() {
7
	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
8
		name := req.URL.Path[1:]
9
		if name == "" {
10
			name = "World"
11
		}
12
		w.Write([]byte(fmt.Sprintf("Hello, %s!", name)))
13
	})
14
15
	fmt.Println("Running on :8080")
16
	http.ListenAndServe(":8080", nil)
17
}

+ 0 - 11
go/examples/hello_web.rb

1
require 'webrick'
2
3
server = WEBrick::HTTPServer.new :Port => 8080
4
5
server.mount_proc '/' do |req, res|
6
  name = req.path == '/' ? "World" : req.path[1..-1]
7
  res.body = "Hello, #{name}!"
8
end
9
10
trap 'INT' do server.shutdown end
11
server.start

+ 0 - 53
go/fileutil/fileutil.go

1
package fileutil
2
3
import (
4
	"os"
5
	"path"
6
	"path/filepath"
7
)
8
9
func IsFile(path string) bool {
10
	info, err := os.Stat(path)
11
	if err != nil {
12
		return false
13
	}
14
	return !info.IsDir()
15
}
16
17
func IsDir(path string) bool {
18
	info, err := os.Stat(path)
19
	if err != nil {
20
		return false
21
	}
22
	return info.IsDir()
23
}
24
25
func MatchExists(glob string) bool {
26
	matches, _ := filepath.Glob(glob)
27
	return len(matches) > 0
28
}
29
30
func Join(fileOrDir string, elem ...string) string {
31
	dir := fileOrDir
32
	if IsFile(fileOrDir) {
33
		dir = path.Dir(fileOrDir)
34
	}
35
	return path.Join(dir, path.Join(elem...))
36
}
37
38
func IsExecutable(file string) bool {
39
	info, err := os.Stat(file)
40
	if err != nil {
41
		return false
42
	}
43
44
	isExecutable := info.Mode() & 0111
45
	return isExecutable != 0 && !info.IsDir()
46
}
47
48
func Dir(file string) string {
49
	if IsDir(file) {
50
		return file
51
	}
52
	return path.Dir(file)
53
}

+ 0 - 200
go/qst.go

1
// qst - run things quickly
2
//
3
// Given a file or directory, guesses the project type and runs
4
// it for you. Restarts on changes. Intended for small experiments
5
// and working with unfamilar build systems.
6
package main
7
8
import (
9
	"errors"
10
	"flag"
11
	"fmt"
12
	"log"
13
	"os"
14
	"os/exec"
15
	"os/signal"
16
	"path/filepath"
17
	"strings"
18
	"syscall"
19
	"time"
20
21
	"./detect"
22
	"./fileutil"
23
)
24
25
var delay = flag.Duration("delay", 1*time.Second, "time to wait until restart")
26
var autoRestart = flag.Bool("autorestart", true, "automatically restart after command exists")
27
var command = flag.String("command", "", "command to run ({file} will be substituted)")
28
var projectType = flag.String("type", "", "project type to use (autodetected if not present)")
29
var phase = flag.String("phase", "run", "which phase to run (build, run or test)")
30
var justDetect = flag.Bool("detect", false, "detect the project type and exit")
31
32
func main() {
33
	flag.Usage = func() {
34
		fmt.Fprintf(os.Stderr, "Usage: %s <file>\n\n", os.Args[0])
35
		flag.PrintDefaults()
36
		fmt.Fprintf(os.Stderr, "\nSupported project types: \n")
37
		for _, project := range detect.ProjectTypes {
38
			paddedId := fmt.Sprintf("%s%s", project.Id, strings.Repeat(" ", 30-len(project.Id)))
39
			fmt.Fprintf(os.Stderr, "\t%s- %v\n", paddedId, project.Commands[*phase])
40
		}
41
	}
42
43
	flag.Parse()
44
	args := flag.Args()
45
46
	if len(args) < 1 {
47
		flag.Usage()
48
		os.Exit(1)
49
	}
50
51
	file := args[0]
52
53
	if *justDetect {
54
		projects := detect.DetectAll(file)
55
		if len(projects) == 0 {
56
			log.Fatal("unkown project type")
57
		}
58
		for _, project := range projects {
59
			fmt.Println(project.Id)
60
		}
61
		os.Exit(0)
62
	}
63
64
	var cmd string
65
	if !flagEmpty(command) {
66
		cmd = *command
67
	} else {
68
		project, err := detect.Detect(file)
69
		if !flagEmpty(projectType) {
70
			project = detect.GetById(*projectType)
71
			if project == nil {
72
				log.Fatalf("unknown type: `%s'", *projectType)
73
			} else if !project.Detect(file) {
74
				log.Fatalf("%s doesn't match type %s!", file, *projectType)
75
			}
76
		}
77
		if err != nil {
78
			log.Fatal("error: ", err)
79
		}
80
		log.Printf("detected a %s project", project.Id)
81
		projectCmd, found := project.Commands[*phase]
82
		if !found {
83
			log.Fatalf("%s doesn't support `%s'", project.Id, *phase)
84
		}
85
		cmd = projectCmd
86
	}
87
	file, _ = filepath.Abs(file)
88
	cmd = strings.Replace(cmd, "{file}", file, -1)
89
	if err := os.Chdir(fileutil.Dir(file)); err != nil {
90
		log.Fatal(err)
91
	}
92
	log.Printf("command to run: `%s'", cmd)
93
94
	runner := MakeRunner(cmd, *autoRestart)
95
	go runCmd(file, runner)
96
97
	c := make(chan os.Signal, 1)
98
	signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGTERM)
99
	s := <-c
100
	log.Printf("got signal: %s, exiting...", s)
101
	runner.Stop()
102
}
103
104
func flagEmpty(stringFlag *string) bool {
105
	return stringFlag == nil || strings.TrimSpace(*stringFlag) == ""
106
}
107
108
func runCmd(file string, runner *Runner) {
109
	runner.Start()
110
	lastMtime := time.Now()
111
	for {
112
		info, err := os.Stat(file)
113
		if os.IsNotExist(err) {
114
			log.Fatalf("`%s' disappeared, exiting", file)
115
		}
116
117
		mtime := info.ModTime()
118
		if mtime.After(lastMtime) {
119
			log.Printf("`%s' changed, trying to restart", file)
120
			runner.Restart()
121
		}
122
123
		lastMtime = mtime
124
		time.Sleep(1 * time.Second)
125
	}
126
}
127
128
type Runner struct {
129
	cmd         *exec.Cmd
130
	shellCmd    string
131
	started     bool
132
	autoRestart bool
133
	restarting  bool
134
}
135
136
func MakeRunner(shellCmd string, autoRestart bool) *Runner {
137
	return &Runner{nil, shellCmd, false, autoRestart, false}
138
}
139
140
func (r *Runner) Start() error {
141
	if r.started {
142
		return errors.New("already started, use Restart()")
143
	}
144
145
	r.started = true
146
	go func() {
147
		for {
148
			log.Printf("[runner] starting command")
149
			r.cmd = exec.Command("sh", "-c", r.shellCmd)
150
			r.cmd.Stderr = os.Stderr
151
			r.cmd.Stdout = os.Stdout
152
			r.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
153
			err := r.cmd.Run()
154
			var result interface{}
155
			if err != nil {
156
				result = err
157
			} else {
158
				result = r.cmd.ProcessState
159
			}
160
			log.Printf("[runner] finished: %s", result)
161
162
			time.Sleep(*delay)
163
			if !r.restarting && !r.autoRestart {
164
				r.started = false
165
				break
166
			}
167
168
			r.restarting = false
169
		}
170
	}()
171
172
	return nil
173
}
174
175
func (r *Runner) Kill() error {
176
	pgid, err := syscall.Getpgid(r.cmd.Process.Pid)
177
	if err == nil {
178
		syscall.Kill(-pgid, syscall.SIGTERM)
179
	}
180
	return err
181
}
182
183
func (r *Runner) Restart() error {
184
	r.restarting = true
185
	if r.started {
186
		return r.Kill()
187
	} else {
188
		return r.Start()
189
	}
190
}
191
192
func (r *Runner) Stop() {
193
	r.autoRestart = false
194
	r.Kill()
195
}
196
197
func isFile(file string) bool {
198
	info, err := os.Stat(file)
199
	return err == nil && !info.IsDir()
200
}