ソースを参照

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

already?! that was fast...
Lucas Stadler 11 年 前
コミット
16737815b1
共有15 個のファイルを変更した5 個の追加464 個の削除を含む
  1. 1 6
      go/.gitignore
  2. 4 32
      go/README.md
  3. 0 123
      go/detect/detect.go
  4. 0 6
      go/examples/hello.c
  5. 0 1
      go/examples/hello.coffee
  6. 0 7
      go/examples/hello.go
  7. 0 1
      go/examples/hello.hs
  8. 0 2
      go/examples/hello.idr
  9. 0 1
      go/examples/hello.jl
  10. 0 1
      go/examples/hello.rb
  11. 0 3
      go/examples/hello.rs
  12. 0 17
      go/examples/hello_web.go
  13. 0 11
      go/examples/hello_web.rb
  14. 0 53
      go/fileutil/fileutil.go
  15. 0 200
      go/qst.go

+ 1 - 6
go/.gitignore

@ -1,6 +1 @@
1
.go
2
3
qst
4
5
examples/hello
6
examples/hello_web
1
.go

+ 4 - 32
go/README.md

@ -14,38 +14,10 @@ I think.
14 14
15 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 20
intended to be run in unfamilar environments, you pass it a file or a
18 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,123 +0,0 @@
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,6 +0,0 @@
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 +0,0 @@
1
console.log "Hello, World!"

+ 0 - 7
go/examples/hello.go

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

+ 0 - 1
go/examples/hello.hs

@ -1 +0,0 @@
1
main = putStrLn "Hello, World!"

+ 0 - 2
go/examples/hello.idr

@ -1,2 +0,0 @@
1
main : IO ()
2
main = putStrLn "Hello, World!"

+ 0 - 1
go/examples/hello.jl

@ -1 +0,0 @@
1
println("Hello, World!")

+ 0 - 1
go/examples/hello.rb

@ -1 +0,0 @@
1
puts "Hello, World!"

+ 0 - 3
go/examples/hello.rs

@ -1,3 +0,0 @@
1
fn main() {
2
	println!("Hello, World!")
3
}

+ 0 - 17
go/examples/hello_web.go

@ -1,17 +0,0 @@
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,11 +0,0 @@
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,53 +0,0 @@
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,200 +0,0 @@
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
}