├── VERSION ├── run.png ├── how-it-works.png ├── post-commit ├── .travis.yml ├── Dockerfile ├── description-pak ├── run.conf ├── .gitignore ├── utils ├── utils_test.go ├── utils.go ├── common_test.go ├── config_test.go ├── config.go ├── options_test.go ├── common.go └── options.go ├── test └── flock_proc.go ├── flock ├── flock_test.go └── flock.go ├── LICENSE ├── Makefile ├── man └── run.1 ├── README.md └── run.go /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.6 -------------------------------------------------------------------------------- /run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runscripts/run/HEAD/run.png -------------------------------------------------------------------------------- /how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runscripts/run/HEAD/how-it-works.png -------------------------------------------------------------------------------- /post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION=`cat VERSION` 4 | 5 | if [ "`git diff HEAD~1 HEAD VERSION`" != "" ]; then 6 | git tag $VERSION 7 | echo "Created a new tag" $VERSION 8 | fi 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | - 1.4 6 | - tip 7 | 8 | script: 9 | - go build 10 | - go test -v 11 | - cd utils 12 | - go build 13 | - go test -v 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4 2 | MAINTAINER tobe tobeg3oogle@gmail.com 3 | 4 | RUN apt-get update -y 5 | 6 | # Install run 7 | ADD . /go/src/github.com/runscripts/run 8 | WORKDIR /go/src/github.com/runscripts/run 9 | RUN make install 10 | 11 | CMD run -------------------------------------------------------------------------------- /description-pak: -------------------------------------------------------------------------------- 1 | Run is the missing manager for developers. 2 | 3 | It helps to manage and re-use scripts more easily. After installing run, you get 4 | the command run. Please run pt-summary and it will download the well-known 5 | pt-summary to run. If you push your scripts to GitHub, you can simply use it 6 | like run github:runscripts/scripts/pt-summary. 7 | -------------------------------------------------------------------------------- /run.conf: -------------------------------------------------------------------------------- 1 | [sources] 2 | default: https://raw.githubusercontent.com/runscripts/scripts/master/%s 3 | github: https://raw.githubusercontent.com/%s/%s/master/%s 4 | bitbucket: https://bitbucket.org/%s/%s/raw/master/%s 5 | gitcafe: https://gitcafe.com/%s/%s/blob/master/%s 6 | gogs: https://try.gogs.io/%s/%s/raw/master/%s 7 | https: https:%s 8 | http: http:%s 9 | 10 | # tobe: https://raw.githubusercontent.com/tobegit3hub/scripts/master/%s -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Executable files 2 | runscripts 3 | run 4 | 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | *.swp 30 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestFetch(t *testing.T) { 6 | err := Fetch("https://raw.githubusercontent.com/runscripts/run/master/.gitignore", 7 | "/tmp/.gitignore") 8 | if err != nil { 9 | t.Error(err) 10 | } 11 | err = Fetch("https://raw.githubusercontent.com/runscripts/run/master/gitignore", 12 | "/tmp/gitignore") 13 | if err == nil { 14 | t.Errorf("GET nonexistent URI should return error") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/flock_proc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/runscripts/run/flock" 8 | ) 9 | 10 | // This program is run by flock_test.go to verify the lock effect. 11 | func main() { 12 | path := "/tmp/flock_test.lock" 13 | if os.Args[1] == "1" { 14 | time.Sleep(time.Millisecond * 100) 15 | } 16 | if err := flock.Flock(path); err != nil { 17 | panic(err) 18 | } 19 | if os.Args[1] == "0" { 20 | time.Sleep(time.Millisecond * 200) 21 | if err := flock.Funlock(path); err != nil { 22 | panic(err) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Retrieve a file via HTTP GET. 10 | func Fetch(url string, path string) error { 11 | response, err := http.Get(url) 12 | if err != nil { 13 | return err 14 | } 15 | if response.StatusCode != 200 { 16 | return Errorf("%s: %s", response.Status, url) 17 | } 18 | 19 | defer response.Body.Close() 20 | body, err := ioutil.ReadAll(response.Body) 21 | if err != nil { 22 | return err 23 | } 24 | if strings.HasPrefix(url, MASTER_URL) { 25 | // When fetching run.conf, etc. 26 | return ioutil.WriteFile(path, body, 0644) 27 | } else { 28 | // When fetching scripts. 29 | return ioutil.WriteFile(path, body, 0777) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /flock/flock_test.go: -------------------------------------------------------------------------------- 1 | package flock 2 | 3 | import ( 4 | "os/exec" 5 | "testing" 6 | ) 7 | 8 | func TestFlock(t *testing.T) { 9 | path := "/tmp/flock_test.lock" 10 | err := Flock(path) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | err = Funlock(path) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | cmd0 := exec.Command("go", "run", "../test/flock_proc.go", "0") 19 | err = cmd0.Start() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | cmd1 := exec.Command("go", "run", "../test/flock_proc.go", "1") 24 | err = cmd1.Start() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | err = cmd1.Wait() 29 | if err == nil { 30 | t.Errorf("%s shouldn't be locked twice", path) 31 | } 32 | err = cmd0.Wait() 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /utils/common_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestLog(t *testing.T) { 6 | LogError("Testing LogError...\n") 7 | LogError("Testing %s...\n", "LogError") 8 | LogInfo("Testing LogInfo...\n") 9 | LogInfo("Testing %s...\n", "LogInfo") 10 | } 11 | 12 | func TestErrorf(t *testing.T) { 13 | if Errorf("Testing") == nil || Errorf("Testing %s", "Errorf") == nil { 14 | t.Errorf("Returned error shouldn't be nil") 15 | } 16 | } 17 | 18 | func TestStrToSha1(t *testing.T) { 19 | if StrToSha1("Hello") != "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0" { 20 | println(StrToSha1("Hello")) 21 | t.Error("Wrong encryption result") 22 | } 23 | } 24 | 25 | func TestFileExists(t *testing.T) { 26 | if !FileExists("./common.go") { 27 | t.Errorf("How does common.go not exist?") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /utils/config_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestNewConfig(t *testing.T) { 6 | config, err := NewConfig("../run.conf") 7 | if err != nil { 8 | t.Error(err) 9 | } 10 | if config.Sources["http"] != "http:%s" { 11 | t.Errorf("Parse result is not correct") 12 | } 13 | } 14 | 15 | func TestNewConfigFromString(t *testing.T) { 16 | str := ` 17 | [sources] 18 | # ignore 19 | default: default_url 20 | space: space_url ` 21 | config, err := NewConfigFromString(str) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | if config.Sources["default"] != "default_url" { 26 | t.Errorf("'default' source is not parsed correctly") 27 | } else if config.Sources["space"] != "space_url" { 28 | t.Errorf("'space' source is not parsed correctly") 29 | } else if _, ok := config.Sources["ignore"]; ok { 30 | t.Errorf("'ignore' source should be ignored") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /flock/flock.go: -------------------------------------------------------------------------------- 1 | package flock 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | var lockFile *os.File 9 | 10 | // Lock the file. 11 | func Flock(path string) error { 12 | return fcntlFlock(syscall.F_WRLCK, path) 13 | } 14 | 15 | // Unlock the file. 16 | func Funlock(path string) error { 17 | err := fcntlFlock(syscall.F_UNLCK) 18 | if err != nil { 19 | return err 20 | } else { 21 | return lockFile.Close() 22 | } 23 | } 24 | 25 | // Control the lock of file. 26 | func fcntlFlock(lockType int16, path ...string) error { 27 | var err error 28 | if lockType != syscall.F_UNLCK { 29 | mode := syscall.O_CREAT | syscall.O_WRONLY 30 | mask := syscall.Umask(0) 31 | lockFile, err = os.OpenFile(path[0], mode, 0666) 32 | syscall.Umask(mask) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | 38 | lock := syscall.Flock_t{ 39 | Start: 0, 40 | Len: 1, 41 | Type: lockType, 42 | Whence: int16(os.SEEK_SET), 43 | } 44 | return syscall.FcntlFlock(lockFile.Fd(), syscall.F_SETLK, &lock) 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 runscripts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "strings" 7 | ) 8 | 9 | // Configuration object. 10 | type Config struct { 11 | Sources map[string]string 12 | // Future options can be added here. 13 | } 14 | 15 | // Read the configuration on default or specified path. 16 | func NewConfig(path ...string) (*Config, error) { 17 | var content []byte 18 | var err error 19 | // NewConfig(path) would be only called in testing. 20 | if len(path) > 0 { 21 | content, err = ioutil.ReadFile(path[0]) 22 | } else { 23 | content, err = ioutil.ReadFile(CONFIG_PATH) 24 | } 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return NewConfigFromString(string(content)) 30 | } 31 | 32 | // Parse the string to Config object. 33 | func NewConfigFromString(str string) (*Config, error) { 34 | config := Config{} 35 | config.Sources = make(map[string]string) 36 | 37 | reader := strings.NewReader(str) 38 | scanner := bufio.NewScanner(reader) 39 | option := config.Sources 40 | for scanner.Scan() { 41 | line := strings.TrimSpace(scanner.Text()) 42 | if len(line) == 0 || line[0] == '#' { 43 | continue 44 | } else if line[0] == '[' { 45 | switch line { 46 | case "[sources]": 47 | option = config.Sources 48 | default: 49 | return nil, Errorf("%s: unknown option: %s", CONFIG_PATH, line) 50 | } 51 | } else { 52 | tokens := strings.SplitN(line, ":", 2) 53 | if len(tokens) != 2 { 54 | return nil, Errorf("%s: incorret format: %s", CONFIG_PATH, line) 55 | } 56 | scope := strings.TrimSpace(tokens[0]) 57 | if !IsScopeNameValid(scope) { 58 | return nil, Errorf("%s: invalid scope name: %s", CONFIG_PATH, scope) 59 | } 60 | option[scope] = strings.TrimSpace(tokens[1]) 61 | } 62 | } 63 | 64 | if err := scanner.Err(); err != nil { 65 | return nil, err 66 | } else { 67 | return &config, nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Install 2 | # sudo GOPATH=$GOPATH make install 3 | # 4 | # Release 5 | # NOTICE: Backup your run.conf before run the following command 6 | # sudo GOPATH=$GOPATH make packages VERSION="X.Y.Z" 7 | # 8 | # Other 9 | # make test 10 | # sudo make clean 11 | # sudo make purge 12 | # sudo GOPATH=$GOPATH make deb VERSION="X.Y.Z" 13 | 14 | .PHONY: help test install clean purge packages deb 15 | 16 | OS=$(shell uname) 17 | 18 | ifeq ($(OS), Darwin) 19 | RUN_CONF=/usr/local/etc/run.conf 20 | else 21 | RUN_CONF=/etc/run.conf 22 | endif 23 | 24 | ifeq ($(OS), Darwin) 25 | DATA_DIR=/usr/local/var/run 26 | else 27 | DATA_DIR=/usr/local/run 28 | endif 29 | 30 | ifeq ($(OS), Darwin) 31 | RUN_BIN=/usr/local/bin/run 32 | else 33 | RUN_BIN=/usr/bin/run 34 | endif 35 | 36 | MAN_PAGE=/usr/share/man/man1/run.1.gz 37 | 38 | DEB_DIR=packages/deb 39 | 40 | BUILD=go build 41 | MAIN=run.go 42 | 43 | PACKAGES=linux_amd64 linux_386 linux_arm \ 44 | darwin_amd64 darwin_386 \ 45 | freebsd_amd64 freebsd_386 46 | 47 | DEB_FLAG=-y --install=no --fstrans=yes --nodoc --backup=no \ 48 | --maintainer="wizawu@gmail.com" --pkglicense=MIT \ 49 | --pkgversion=`date +"%Y%m%d"` --pkgrelease=$(VERSION) \ 50 | --pkgaltsource="https://github.com/runscripts/run" 51 | 52 | help: 53 | echo 'sudo GOPATH=$$GOPATH make install' 54 | 55 | test: 56 | cd flock && go test -cover -v 57 | cd utils && go test -cover -v 58 | 59 | install: 60 | $(_OS) $(_ARCH) $(BUILD) -o $(RUN_BIN) $(MAIN) 61 | [ -e $(RUN_CONF) ] || cp run.conf $(RUN_CONF) 62 | mkdir -p $(DATA_DIR) && chmod 777 $(DATA_DIR) 63 | cp VERSION $(DATA_DIR) 64 | gzip -c man/run.1 > $(MAN_PAGE) 65 | 66 | clean: 67 | rm -f $(RUN_BIN) 68 | rm -rf $(DATA_DIR) 69 | rm -f $(MAN_PAGE) 70 | 71 | purge: clean 72 | rm -f $(RUN_CONF) 73 | 74 | packages: deb $(PACKAGES) 75 | 76 | $(PACKAGES): 77 | echo $@ | awk -F_ '{print "mkdir -p packages/"$$1"_"$$2}' | bash 78 | echo $@ | awk -F_ '{print "CGO_ENABLED=0 GOOS="$$1" GOARCH="$$2" $(BUILD) -o packages/"$$1"_"$$2"/run $(MAIN)"}' | bash 79 | 80 | deb: 81 | [ -n "$(VERSION)" ] && [ `whoami` = 'root' ] 82 | make purge 83 | mkdir -p $(DEB_DIR) && rm -rf $(DEB_DIR)/*.deb 84 | checkinstall $(DEB_FLAG) --arch="amd64" make install _OS="GOOS=linux" _ARCH="GOARCH=amd64" 85 | checkinstall $(DEB_FLAG) --arch="386" make install _OS="GOOS=linux" _ARCH="GOARCH=386" 86 | mv *.deb $(DEB_DIR) 87 | -------------------------------------------------------------------------------- /man/run.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" Title: Run 3 | .\" Author: wizawu@gmail.com 4 | .\" Date: 2014 5 | .\" Language: English 6 | .\" 7 | .TH RUN 1 8 | 9 | .SH NAME 10 | \fBRun\fP \- The missing script manager for developers. 11 | 12 | .SH SYNOPSIS 13 | .B run 14 | [\fIOPTIONS\fP] [\fISCOPE:\fP]\fISCRIPT\fP 15 | 16 | .SH DESCRIPTION 17 | Run is the missing manager for developers. 18 | 19 | It helps to manage and re-use scripts more easily. After installing run, you get 20 | the command run. Please run pt-summary and it will download the well-known 21 | pt-summary to run. If you push your scripts to GitHub, you can simply use it 22 | like run github:runscripts/scripts/pt-summary. 23 | 24 | .SH OPTIONS 25 | 26 | .TP 27 | \fB-c\fP, \fB\-\-clean\fP 28 | Clean out all scripts cached in local. 29 | 30 | .TP 31 | \fB-h\fP, \fB\-\-help\fP 32 | Show a brief help message, then exit. 33 | 34 | .TP 35 | \fB-i\ INTERPRETER\fP 36 | You can specify an interpreter (e.g., bash, python) here. 37 | 38 | .TP 39 | \fB-I\fP, \fB\-\-init\fP 40 | Create configuration and cache directory. 41 | 42 | .TP 43 | \fB-u\fP, \fB\-\-update\fP 44 | Force to update the script before run it. 45 | 46 | .TP 47 | \fB-v\fP, \fB\-\-view\fP 48 | View the content of script, then exit. 49 | 50 | .TP 51 | \fB-V\fP, \fB\-\-version\fP 52 | Output version information, then exit. 53 | 54 | .SH FILES 55 | 56 | .TP 57 | \fI/etc/run.conf\fP (Linux, FreeBSD, etc), \fI/usr/local/etc/run.conf\fP (Mac OS) 58 | System-wide configuration file. 59 | 60 | .TP 61 | \fI/usr/local/run\fP 62 | Directory for caching scripts. 63 | 64 | .SH EXAMPLES 65 | 66 | .TP 67 | \fBExample 1\fP 68 | run pt-summary 69 | 70 | .PP 71 | This will use the \fBdefault\fP scope in \fIrun.conf\fP: 72 | .TP 73 | \fI\fP 74 | default:\ https://raw.githubusercontent.com/runscripts/scripts/master/%s 75 | .PP 76 | And replace \fB%s\fP with \fBpt-summary\fP. 77 | .PP 78 | 79 | .TP 80 | \fBExample 2\fP 81 | run github:runscripts/scripts/pt-summary 82 | 83 | .PP 84 | This will use the \fBgithub\fP scope in \fIrun.conf\fP: 85 | .TP 86 | \fI\fP 87 | github: https://raw.githubusercontent.com/%s/%s/master/%s 88 | .PP 89 | And replace three \fB%s\fP with \fBrunscripts\fP, \fBscripts\fP and 90 | \fBpt-summary\fP sequentially. 91 | 92 | .SH COPYRIGHT 93 | Copyright 2014 runscripts.org. 94 | 95 | .SH BUGS 96 | Please report bugs at https://github.com/runscripts/run/issues. 97 | -------------------------------------------------------------------------------- /utils/options_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestIsScopeNameValid(t *testing.T) { 9 | if IsScopeNameValid("test%1") { 10 | t.Errorf("Failed to detect invalid characters") 11 | } 12 | if IsScopeNameValid("-test") { 13 | t.Errorf("First character should not be '-'") 14 | } 15 | if IsScopeNameValid(".test") { 16 | t.Errorf("First character should not be '.'") 17 | } 18 | if !IsScopeNameValid("_4.t-X") { 19 | t.Errorf("This scope name should be valid") 20 | } 21 | } 22 | 23 | func TestNewOptions(t *testing.T) { 24 | config, err := NewConfig("../run.conf") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | fakeArgs := []string{ 30 | "run", "-c", "-h", "-i", "lua", "-I", "-u", "-v", "-V", 31 | } 32 | os.Args = fakeArgs 33 | options, err := NewOptions(config) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if !(options.Clean && options.Help && options.Interpreter == "lua" && 38 | options.Init && options.Update && options.View && options.Version) { 39 | t.Errorf("Incorrect parse of short arguments") 40 | } 41 | 42 | fakeArgs = []string{ 43 | "run", "--clean", "--help", "--init", "--update", 44 | "--view", "--version", 45 | } 46 | os.Args = fakeArgs 47 | options, err = NewOptions(config) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | if !(options.Clean && options.Help && options.Init && options.Update && 52 | options.View && options.Version) { 53 | t.Errorf("Incorrect parse of long arguments") 54 | } 55 | 56 | fakeArgs = []string{ 57 | "run", "bitbucket:run/scripts/redis/start", "-d", "test.conf", 58 | } 59 | os.Args = fakeArgs 60 | options, err = NewOptions(config) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if options.Scope != "bitbucket" { 65 | t.Errorf("Incorrect scope") 66 | } 67 | if options.Fields[0] != "run" || options.Fields[1] != "scripts" || 68 | options.Fields[2] != "redis/start" { 69 | t.Errorf("Incorrect fields") 70 | } 71 | if options.Args[0] != "-d" || options.Args[1] != "test.conf" { 72 | t.Errorf("Incorrect arguments") 73 | } 74 | if options.URL != "https://bitbucket.org/run/scripts/raw/master/redis/start" { 75 | t.Errorf("Incorrect URL") 76 | } 77 | if options.CacheID != StrToSha1("run/scripts/redis/start") { 78 | t.Errorf("Incorrect CacheID") 79 | } 80 | if options.Script != "start" { 81 | t.Errorf("Incorrect script name") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | "syscall" 11 | ) 12 | 13 | // Default configuration settings. 14 | var CONFIG_PATH = "" 15 | var DATA_DIR = "" 16 | 17 | const ( 18 | MASTER_URL = "https://raw.githubusercontent.com/runscripts/run/master/" 19 | ) 20 | 21 | // Determine the CONFIG_PATH according to OS type. 22 | func SetConfigPath() { 23 | if runtime.GOOS == "darwin" { 24 | CONFIG_PATH = "/usr/local/etc/run.conf" 25 | } else { 26 | CONFIG_PATH = "/etc/run.conf" 27 | } 28 | } 29 | 30 | func SetDataDir() { 31 | if runtime.GOOS == "darwin" { 32 | DATA_DIR = "/usr/local/var/run" 33 | } else { 34 | DATA_DIR = "/usr/local/run" 35 | } 36 | } 37 | 38 | // Determine if run is installed. 39 | func IsRunInstalled() bool { 40 | return FileExists(CONFIG_PATH) && FileExists(DATA_DIR) 41 | } 42 | 43 | // Log error message. 44 | func LogError(format string, args ...interface{}) { 45 | if len(args) > 0 { 46 | fmt.Fprintf(os.Stderr, format, args...) 47 | } else { 48 | fmt.Fprintf(os.Stderr, format) 49 | } 50 | } 51 | 52 | // Log info message. 53 | func LogInfo(format string, args ...interface{}) { 54 | if len(args) > 0 { 55 | fmt.Printf(format, args...) 56 | } else { 57 | fmt.Printf(format) 58 | } 59 | } 60 | 61 | // Return formatted error. 62 | func Errorf(format string, args ...interface{}) error { 63 | if len(args) > 0 { 64 | return fmt.Errorf(format, args...) 65 | } else { 66 | return fmt.Errorf(format) 67 | } 68 | } 69 | 70 | // Print error and exit program. 71 | func ExitError(err error) { 72 | LogError("%v\n", err) 73 | os.Exit(1) 74 | } 75 | 76 | // Convert string into hash string. 77 | func StrToSha1(str string) string { 78 | sum := [20]byte(sha1.Sum([]byte(str))) 79 | return hex.EncodeToString(sum[:]) 80 | } 81 | 82 | // Execute the command to replace current process. 83 | func Exec(args []string) { 84 | path, err := exec.LookPath(args[0]) 85 | if err != nil { 86 | ExitError(err) 87 | } 88 | err = syscall.Exec(path, args, os.Environ()) 89 | if err != nil { 90 | ExitError(err) 91 | } 92 | } 93 | 94 | // Determine if the file exists. 95 | func FileExists(file string) bool { 96 | if _, err := os.Stat(file); os.IsNotExist(err) { 97 | return false 98 | } else { 99 | return true 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | [![TravisCI status](https://secure.travis-ci.org/runscripts/run.png)](http://travis-ci.org/runscripts/run) [![GoDoc](https://godoc.org/github.com/runscripts/run?status.svg)](https://godoc.org/github.com/runscripts/run) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/runscripts/run?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ## Introduction 6 | 7 | **Run** is the script manager for developers. 8 | 9 | * It helps to manage scripts with git, svn or hg. 10 | * It helps to re-use all your scripts naturally. 11 | * It is highly integrated with GitHub and others. 12 | * If you're writing scrips, please try it now! 13 | 14 | ## How It Works 15 | 16 | ![](how-it-works.png) 17 | 18 | After installing run, we can execute `run pt-summary` or `run github:runscripts/scripts/pt-summary`. 19 | 20 | The command `run` will download the well-known [pt-summary](http://www.percona.com/doc/percona-toolkit/2.1/pt-summary.html) from GitHub and run locally. 21 | 22 | You can manage your scripts with `svn`, `git` or `hg` in GitHub, Bitbucket, GitLab or anywhere. 23 | 24 | ## Install 25 | 26 | * From Scratch (Go 1.3+) 27 | 28 | ``` 29 | sudo GOPATH=$GOPATH make install 30 | ``` 31 | 32 | * From Binary 33 | 34 | Operating System | Architectures 35 | ---------------- | ------------- 36 | Linux | [386](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/linux_386/run), [amd64](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/linux_amd64/run), [arm](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/linux_arm/run) 37 | Mac OS | [386](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/darwin_386/run), [amd64](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/darwin_amd64/run), [brew](https://github.com/tobegit3hub/homebrew-run) 38 | FreeBSD | [386](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/freebsd_386/run), [amd64](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/freebsd_amd64/run) 39 | Debian/Ubuntu | [386](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/deb/run_20141224-0.3.6_386.deb), [amd64](https://raw.githubusercontent.com/runscripts/run-release/master/0.3.6/deb/run_20141224-0.3.6_amd64.deb) 40 | 41 | Download the binary according to your OS and place it in **$PATH** (like /usr/bin/). Then execute `sudo run --init` or `sudo dpkg -i run.deb`. 42 | 43 | ## Usage 44 | 45 | Watch the [one-minute video](https://www.youtube.com/watch?v=WXUcJvrZP6M) before you're using it. 46 | 47 | ``` 48 | Usage: 49 | run [OPTION] [SCOPE:]SCRIPT 50 | 51 | Options: 52 | -c, --clean clean out all scripts cached in local 53 | -h, --help show this help message, then exit 54 | -i INTERPRETER run script with interpreter(e.g., bash, python) 55 | -I, --init create configuration and cache directory 56 | -u, --update force to update the script before run 57 | -v, --view view the content of script, then exit 58 | -V, --version output version information, then exit 59 | 60 | Examples: 61 | run pt-summary 62 | run github:runscripts/scripts/pt-summary 63 | 64 | Report bugs to . 65 | ``` 66 | 67 | ## Scripts 68 | 69 | At beginning, we can try `run pt-summary` and checkout official scripts in [runscripts/scripts](https://github.com/runscripts/scripts). 70 | 71 | Now let's manage all the scripts with version control tools and play with this script manager. 72 | 73 | Feel free to send pull-request to official scripts and contribute to [runscripts.org](http://runscripts.org/). 74 | -------------------------------------------------------------------------------- /utils/options.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | const VALID_SCOPE_CHARS = "-_." + "1234567890" + 9 | "abcdefghijklmnopqrstuvwxyz" + 10 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 11 | 12 | // Determine if a scope name contains invalid characters. 13 | func IsScopeNameValid(scope string) bool { 14 | for _, c := range scope { 15 | if strings.IndexRune(VALID_SCOPE_CHARS, c) < 0 { 16 | return false 17 | } 18 | } 19 | return scope[0] != '-' && scope[0] != '.' 20 | } 21 | 22 | // All the options when run the script. 23 | type Options struct { 24 | Program string 25 | Clean bool 26 | Help bool 27 | Interpreter string 28 | Init bool 29 | Update bool 30 | View bool 31 | Version bool 32 | Scope string 33 | Fields []string 34 | Args []string 35 | URL string 36 | CacheID string 37 | Script string 38 | } 39 | 40 | // Give configuration and set runtime options. 41 | func NewOptions(config *Config) (*Options, error) { 42 | options := Options{ 43 | Program: os.Args[0], 44 | Clean: false, 45 | Help: false, 46 | Interpreter: "", 47 | Init: false, 48 | Update: false, 49 | View: false, 50 | Version: false, 51 | Scope: "default", 52 | Fields: nil, 53 | Args: []string{}, 54 | } 55 | 56 | // Parse command parameters to options. 57 | i := 1 58 | length := len(os.Args) 59 | for ; i < length; i++ { 60 | opt := os.Args[i] 61 | if opt[0] != '-' { 62 | break 63 | } 64 | switch opt { 65 | case "-c", "--clean": 66 | options.Clean = true 67 | case "-h", "--help": 68 | options.Help = true 69 | case "-i": 70 | if i+1 == length || os.Args[i+1][0] == '-' { 71 | return nil, Errorf("Missing interpreter (e.g., bash, python) after -i") 72 | } else { 73 | options.Interpreter = os.Args[i+1] 74 | i += 1 75 | } 76 | case "-I", "--init": 77 | options.Init = true 78 | case "-u", "--update": 79 | options.Update = true 80 | case "-v", "--view": 81 | options.View = true 82 | case "-V", "--version": 83 | options.Version = true 84 | default: 85 | return nil, Errorf("Unknown option %s", opt) 86 | } 87 | } 88 | 89 | // Parse full script name to options. 90 | if i < length { 91 | // Parse scope. 92 | var fields string 93 | opt := os.Args[i] 94 | j := strings.Index(opt, ":") 95 | if j >= 0 { 96 | options.Scope = opt[:j] 97 | if !IsScopeNameValid(options.Scope) { 98 | return nil, Errorf("Scope name %s is invalid", options.Scope) 99 | } 100 | fields = opt[j+1:] 101 | } else { 102 | fields = opt 103 | } 104 | // Get scope pattern from configuration. 105 | pattern, ok := (*config).Sources[options.Scope] 106 | if !ok { 107 | return nil, Errorf( 108 | "%s: [sources] '%s' scope does not exist", 109 | CONFIG_PATH, options.Scope, 110 | ) 111 | } 112 | // Examine whether scope pattern and fields can be matched. 113 | nReplace := strings.Count(pattern, "%s") 114 | if nReplace == 0 { 115 | return nil, Errorf( 116 | "%s: [sources] '%s' scope does not contain %%s", 117 | CONFIG_PATH, options.Scope, 118 | ) 119 | } else if nReplace > strings.Count(fields, "/")+1 { 120 | return nil, Errorf( 121 | "%s: [sources] '%s' scope contains %d %%s", 122 | CONFIG_PATH, options.Scope, nReplace, 123 | ) 124 | } 125 | // Parse fields and url. 126 | options.Fields = strings.SplitN(fields, "/", nReplace) 127 | for _, f := range options.Fields { 128 | pattern = strings.Replace(pattern, "%s", f, 1) 129 | } 130 | options.URL = pattern 131 | // Parse args of the script. 132 | if i+1 < length { 133 | options.Args = os.Args[i+1:] 134 | } 135 | // Parse script name and its cache id. 136 | options.Script = fields[strings.LastIndex(fields, "/")+1:] 137 | options.CacheID = StrToSha1(fields) 138 | } 139 | 140 | return &options, nil 141 | } 142 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "syscall" 8 | 9 | "github.com/runscripts/run/flock" 10 | "github.com/runscripts/run/utils" 11 | ) 12 | 13 | // Show this help message. 14 | func help() { 15 | utils.LogInfo( 16 | `Usage: 17 | run [OPTION] [SCOPE:]SCRIPT 18 | 19 | Options: 20 | -h, --help show this help message run, then exit 21 | -V, --version output version information, then exit 22 | -v, --view view the content of script, then exit 23 | -u, --update force to update the script before run 24 | -c, --clean clean out all scripts cached in local 25 | -i INTERPRETER run with interpreter(e.g., bash, python) 26 | -I, --init create configuration and cache directory 27 | 28 | Examples: 29 | run pt-summary 30 | run github:runscripts/scripts/pt-summary 31 | 32 | Report bugs to .`, 33 | ) 34 | utils.LogInfo("\n") 35 | } 36 | 37 | // Initialize and exit if -I or --init is given. 38 | func initialize() { 39 | for _, arg := range os.Args { 40 | if arg == "-I" || arg == "--init" { 41 | if utils.IsRunInstalled() { 42 | utils.LogInfo("Run is already installed\n") 43 | } else { 44 | if os.Geteuid() != 0 { 45 | utils.LogError("Root privilege is required\n") 46 | os.Exit(1) 47 | } 48 | // Create script cache directory. 49 | err := os.MkdirAll(utils.DATA_DIR, 0777) 50 | if err != nil { 51 | utils.ExitError(err) 52 | } 53 | // Download run.conf, VERSION and run.1 from master branch. 54 | err = utils.Fetch(utils.MASTER_URL+"run.conf", utils.CONFIG_PATH) 55 | if err != nil { 56 | utils.ExitError(err) 57 | } 58 | err = utils.Fetch(utils.MASTER_URL+"VERSION", utils.DATA_DIR+"/VERSION") 59 | if err != nil { 60 | utils.ExitError(err) 61 | } 62 | err = utils.Fetch(utils.MASTER_URL+"man/run.1", "/usr/share/man/man1/run.1.gz") 63 | if err != nil { 64 | utils.ExitError(err) 65 | } 66 | } 67 | os.Exit(0) 68 | } 69 | } 70 | } 71 | 72 | // Main function of the command run. 73 | func main() { 74 | mask := syscall.Umask(0) 75 | defer syscall.Umask(mask) 76 | utils.SetConfigPath() 77 | utils.SetDataDir() 78 | initialize() 79 | 80 | // If run is not installed. 81 | if utils.IsRunInstalled() == false { 82 | utils.LogError("Run is not installed yet. You need to 'run --init' as root.\n") 83 | os.Exit(1) 84 | } 85 | 86 | // Show help message if no parameter given. 87 | if len(os.Args) == 1 { 88 | help() 89 | return 90 | } 91 | 92 | // Parse configuration and runtime options. 93 | config, err := utils.NewConfig() 94 | if err != nil { 95 | utils.ExitError(err) 96 | } 97 | options, err := utils.NewOptions(config) 98 | if err != nil { 99 | utils.ExitError(err) 100 | } 101 | 102 | // If print help message. 103 | if options.Help { 104 | help() 105 | return 106 | } 107 | 108 | // If output version information. 109 | if options.Version { 110 | version, err := ioutil.ReadFile(utils.DATA_DIR + "/VERSION") 111 | if err != nil { 112 | utils.ExitError(err) 113 | } 114 | utils.LogInfo("Run version %s\n", version) 115 | return 116 | } 117 | 118 | // If clean out scripts. 119 | if options.Clean { 120 | utils.LogInfo("Do you want to clear out the script cache? [Y/n] ") 121 | var answer string 122 | fmt.Scanln(&answer) 123 | if answer == "Y" || answer == "y" { 124 | // rm -rf $DATA_DIR/* will remove VERSION. Use $DATA_DIR/*/ instead. 125 | utils.Exec([]string{"sh", "-x", "-c", "rm -rf " + utils.DATA_DIR + "/*/"}) 126 | } 127 | return 128 | } 129 | 130 | // If not script given. 131 | if options.Fields == nil { 132 | utils.LogError("The script to run is not specified\n") 133 | os.Exit(1) 134 | } 135 | 136 | // Ensure the cache directory has been created. 137 | cacheID := options.CacheID 138 | cacheDir := utils.DATA_DIR + "/" + options.Scope + "/" + cacheID 139 | err = os.MkdirAll(cacheDir, 0777) 140 | if err != nil { 141 | utils.ExitError(err) 142 | } 143 | 144 | // Lock the script. 145 | lockPath := cacheDir + ".lock" 146 | err = flock.Flock(lockPath) 147 | if err != nil { 148 | utils.LogError("%s: %v\n", lockPath, err) 149 | os.Exit(1) 150 | } 151 | 152 | // Download the script. 153 | scriptPath := cacheDir + "/" + options.Script 154 | _, err = os.Stat(scriptPath) 155 | if os.IsNotExist(err) || options.Update { 156 | err = utils.Fetch(options.URL, scriptPath) 157 | if err != nil { 158 | utils.ExitError(err) 159 | } 160 | } 161 | 162 | // If view the script. 163 | if options.View { 164 | flock.Funlock(lockPath) 165 | utils.Exec([]string{"cat", scriptPath}) 166 | } 167 | 168 | // Run the script. 169 | flock.Funlock(lockPath) 170 | if options.Interpreter == "" { 171 | utils.Exec(append([]string{scriptPath}, options.Args...)) 172 | } else { 173 | utils.Exec(append([]string{options.Interpreter, scriptPath}, options.Args...)) 174 | } 175 | } 176 | --------------------------------------------------------------------------------