convert repo plugin to golang

This commit is contained in:
Michael Hobbs
2017-01-03 22:27:20 -08:00
parent 8d83803763
commit b6dffaabee
44 changed files with 2705 additions and 24 deletions

View File

@@ -14,3 +14,8 @@ indent_size = 4
insert_final_newline = true
indent_style = tab
indent_size = 4
[*.go]
insert_final_newline = true
indent_style = space
indent_size = 4

View File

@@ -9,6 +9,9 @@ DOKKU_LIB_ROOT ?= /var/lib/dokku
PLUGINS_PATH ?= ${DOKKU_LIB_ROOT}/plugins
CORE_PLUGINS_PATH ?= ${DOKKU_LIB_ROOT}/core-plugins
export GO_REPO_ROOT := /go/src/github.com/dokku/dokku
export BUILD_IMAGE := golang:1.7.1
# If the first argument is "vagrant-dokku"...
ifeq (vagrant-dokku,$(firstword $(MAKECMDGOALS)))
# use the rest as arguments for "vagrant-dokku"
@@ -23,7 +26,8 @@ else
BUILD_STACK_TARGETS = build-in-docker
endif
.PHONY: all apt-update install version copyfiles man-db plugins dependencies sshcommand plugn docker aufs stack count dokku-installer vagrant-acl-add vagrant-dokku
.PHONY: all apt-update install version copyfiles man-db plugins dependencies sshcommand plugn docker aufs stack count dokku-installer vagrant-acl-add vagrant-dokku go-build force
force :;
include tests.mk
include deb.mk
@@ -50,7 +54,16 @@ package_cloud:
packer:
packer build contrib/packer.json
go-build: force
basedir=$(PWD); \
for dir in plugins/*; do \
if [ -e $$dir/Makefile ]; then \
$(MAKE) -C $$dir || exit $$? ;\
fi ;\
done
copyfiles:
$(MAKE) go-build || exit 1
cp dokku /usr/local/bin/dokku
mkdir -p ${CORE_PLUGINS_PATH} ${PLUGINS_PATH}
rm -rf ${CORE_PLUGINS_PATH}/*
@@ -62,11 +75,13 @@ copyfiles:
rm -rf ${CORE_PLUGINS_PATH}/$$plugin && \
rm -rf ${PLUGINS_PATH}/$$plugin && \
cp -R plugins/$$plugin ${CORE_PLUGINS_PATH}/available && \
rm -rf ${CORE_PLUGINS_PATH}/available/$$plugin/src && \
ln -s ${CORE_PLUGINS_PATH}/available/$$plugin ${PLUGINS_PATH}/available; \
find /var/lib/dokku/ -xtype l -delete;\
PLUGIN_PATH=${CORE_PLUGINS_PATH} plugn enable $$plugin ;\
PLUGIN_PATH=${PLUGINS_PATH} plugn enable $$plugin ;\
done
find ./plugins/* -type f -executable -exec file -i '{}' \; | grep 'x-executable; charset=binary' | awk -F: '{ print $$1 }' | xargs rm -f
chown dokku:dokku -R ${PLUGINS_PATH} ${CORE_PLUGINS_PATH} || true
$(MAKE) addman

149
plugins/common/common.go Normal file
View File

@@ -0,0 +1,149 @@
package common
import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
sh "github.com/codeskyblue/go-sh"
)
// DokkuCmd represents a shell command to be run for dokku
type DokkuCmd struct {
Env map[string]string
Command *exec.Cmd
CommandString string
Args []string
ShowOutput bool
}
// NewDokkuCmd creates a new DokkuCmd
func NewDokkuCmd(command string) *DokkuCmd {
items := strings.Split(command, " ")
cmd := items[0]
args := items[1:]
return &DokkuCmd{
Command: exec.Command(cmd, args...),
CommandString: command,
Args: args,
ShowOutput: true,
}
}
// Execute is a lightweight wrapper around exec.Command
func (dc *DokkuCmd) Execute() bool {
env := os.Environ()
for k, v := range dc.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
dc.Command.Env = env
if dc.ShowOutput {
dc.Command.Stdout = os.Stdout
dc.Command.Stderr = os.Stderr
}
err := dc.Command.Run()
if err != nil {
return false
}
return true
}
// VerifyAppName verifies app name format and app existence"
func VerifyAppName(appName string) (err error) {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
appRoot := strings.Join([]string{dokkuRoot, appName}, "/")
_, err = os.Stat(appRoot)
if os.IsNotExist(err) {
return fmt.Errorf("App %s does not exist: %v\n", appName, err)
}
r, _ := regexp.Compile("^[a-z].*")
if !r.MatchString(appName) {
return fmt.Errorf("App name (%s) must begin with lowercase alphanumeric character\n", appName)
}
return err
}
// MustGetEnv returns env variable or fails if it's not set
func MustGetEnv(key string) string {
dokkuRoot := os.Getenv(key)
if dokkuRoot == "" {
LogFail(fmt.Sprintf("%s not set!", key))
}
return dokkuRoot
}
// LogFail is the failure log formatter
// prints text to stderr and exits with status 1
func LogFail(text string) {
fmt.Fprintln(os.Stderr, fmt.Sprintf("FAILED: %s", text))
os.Exit(1)
}
// GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented
func GetDeployingAppImageName(args ...string) (imageName string) {
argArray := make([]string, 3)
for idx, arg := range args {
argArray[idx] = arg
}
appName := argArray[0]
imageTag := argArray[1]
imageRepo := argArray[2]
if appName == "" {
LogFail("(GetDeployingAppImageName) APP must not be empty")
}
b, err := sh.Command("plugn", "trigger", "deployed-app-repository", appName).Output()
if err != nil {
LogFail(err.Error())
}
imageRemoteRepository := string(b[:])
b, err = sh.Command("plugn", "trigger", "deployed-app-image-tag", appName).Output()
if err != nil {
LogFail(err.Error())
}
newImageTag := string(b[:])
b, err = sh.Command("plugn", "trigger", "deployed-app-image-repo", appName).Output()
if err != nil {
LogFail(err.Error())
}
newImageRepo := string(b[:])
if newImageRepo != "" {
imageRepo = newImageRepo
}
if newImageTag != "" {
imageTag = newImageTag
}
if imageRepo == "" {
imageRepo = GetAppImageRepo(appName)
}
if imageTag == "" {
imageTag = "latest"
}
imageName = fmt.Sprintf("%s%s:%s", imageRemoteRepository, imageRepo, imageTag)
if !VerifyImage(imageName) {
LogFail(fmt.Sprintf("app image (%s) not found", imageName))
}
return
}
// GetAppImageRepo is the central definition of a dokku image repo pattern
func GetAppImageRepo(appName string) string {
return strings.Join([]string{"dokku", appName}, "/")
}
// VerifyImage returns true if docker image exists in local repo
func VerifyImage(image string) bool {
imageCmd := NewDokkuCmd(fmt.Sprintf("docker inspect %s", image))
imageCmd.ShowOutput = false
if imageCmd.Execute() {
return true
}
return false
}

View File

@@ -0,0 +1,38 @@
package common
import (
"os"
"testing"
. "github.com/onsi/gomega"
)
func TestGetEnv(t *testing.T) {
RegisterTestingT(t)
Expect(MustGetEnv("DOKKU_ROOT")).To(Equal("/home/dokku"))
}
func TestGetAppImageRepo(t *testing.T) {
RegisterTestingT(t)
Expect(GetAppImageRepo("testapp")).To(Equal("dokku/testapp"))
}
func TestVerifyImageInvalid(t *testing.T) {
RegisterTestingT(t)
Expect(VerifyImage("testapp")).To(Equal(false))
}
func TestVerifyAppNameInvalid(t *testing.T) {
RegisterTestingT(t)
err := VerifyAppName("1994testApp")
Expect(err).To(HaveOccurred())
}
func TestVerifyAppName(t *testing.T) {
RegisterTestingT(t)
dir := "/home/dokku/testApp"
os.MkdirAll(dir, 0644)
err := VerifyAppName("testApp")
Expect(err).NotTo(HaveOccurred())
os.RemoveAll(dir)
}

8
plugins/common/glide.lock generated Normal file
View File

@@ -0,0 +1,8 @@
hash: e438ca8cf1bdca3e9c984bb1ee70c0d58be9da52cbe13a079a7c6cbc14210a37
updated: 2017-01-03T17:17:27.973373328-08:00
imports:
- name: github.com/codegangsta/inject
version: 33e0aa1cb7c019ccc3fbe049a8262a6403d30504
- name: github.com/codeskyblue/go-sh
version: ceb46ec4630a726eeed51d0a70066c9e1102c48f
testImports: []

View File

@@ -0,0 +1,3 @@
package: .
import:
- package: github.com/codeskyblue/go-sh

View File

@@ -0,0 +1,2 @@
inject
inject.test

View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Jeremy Saenz
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,92 @@
# inject
--
import "github.com/codegangsta/inject"
Package inject provides utilities for mapping and injecting dependencies in
various ways.
Language Translations:
* [简体中文](translations/README_zh_cn.md)
## Usage
#### func InterfaceOf
```go
func InterfaceOf(value interface{}) reflect.Type
```
InterfaceOf dereferences a pointer to an Interface type. It panics if value is
not an pointer to an interface.
#### type Applicator
```go
type Applicator interface {
// Maps dependencies in the Type map to each field in the struct
// that is tagged with 'inject'. Returns an error if the injection
// fails.
Apply(interface{}) error
}
```
Applicator represents an interface for mapping dependencies to a struct.
#### type Injector
```go
type Injector interface {
Applicator
Invoker
TypeMapper
// SetParent sets the parent of the injector. If the injector cannot find a
// dependency in its Type map it will check its parent before returning an
// error.
SetParent(Injector)
}
```
Injector represents an interface for mapping and injecting dependencies into
structs and function arguments.
#### func New
```go
func New() Injector
```
New returns a new Injector.
#### type Invoker
```go
type Invoker interface {
// Invoke attempts to call the interface{} provided as a function,
// providing dependencies for function arguments based on Type. Returns
// a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
Invoke(interface{}) ([]reflect.Value, error)
}
```
Invoker represents an interface for calling functions via reflection.
#### type TypeMapper
```go
type TypeMapper interface {
// Maps the interface{} value based on its immediate type from reflect.TypeOf.
Map(interface{}) TypeMapper
// Maps the interface{} value based on the pointer of an Interface provided.
// This is really only useful for mapping a value as an interface, as interfaces
// cannot at this time be referenced directly without a pointer.
MapTo(interface{}, interface{}) TypeMapper
// Provides a possibility to directly insert a mapping based on type and value.
// This makes it possible to directly map type arguments not possible to instantiate
// with reflect like unidirectional channels.
Set(reflect.Type, reflect.Value) TypeMapper
// Returns the Value that is mapped to the current type. Returns a zeroed Value if
// the Type has not been mapped.
Get(reflect.Type) reflect.Value
}
```
TypeMapper represents an interface for mapping interface{} values based on type.

View File

@@ -0,0 +1,187 @@
// Package inject provides utilities for mapping and injecting dependencies in various ways.
package inject
import (
"fmt"
"reflect"
)
// Injector represents an interface for mapping and injecting dependencies into structs
// and function arguments.
type Injector interface {
Applicator
Invoker
TypeMapper
// SetParent sets the parent of the injector. If the injector cannot find a
// dependency in its Type map it will check its parent before returning an
// error.
SetParent(Injector)
}
// Applicator represents an interface for mapping dependencies to a struct.
type Applicator interface {
// Maps dependencies in the Type map to each field in the struct
// that is tagged with 'inject'. Returns an error if the injection
// fails.
Apply(interface{}) error
}
// Invoker represents an interface for calling functions via reflection.
type Invoker interface {
// Invoke attempts to call the interface{} provided as a function,
// providing dependencies for function arguments based on Type. Returns
// a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
Invoke(interface{}) ([]reflect.Value, error)
}
// TypeMapper represents an interface for mapping interface{} values based on type.
type TypeMapper interface {
// Maps the interface{} value based on its immediate type from reflect.TypeOf.
Map(interface{}) TypeMapper
// Maps the interface{} value based on the pointer of an Interface provided.
// This is really only useful for mapping a value as an interface, as interfaces
// cannot at this time be referenced directly without a pointer.
MapTo(interface{}, interface{}) TypeMapper
// Provides a possibility to directly insert a mapping based on type and value.
// This makes it possible to directly map type arguments not possible to instantiate
// with reflect like unidirectional channels.
Set(reflect.Type, reflect.Value) TypeMapper
// Returns the Value that is mapped to the current type. Returns a zeroed Value if
// the Type has not been mapped.
Get(reflect.Type) reflect.Value
}
type injector struct {
values map[reflect.Type]reflect.Value
parent Injector
}
// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not an pointer to an interface.
func InterfaceOf(value interface{}) reflect.Type {
t := reflect.TypeOf(value)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Interface {
panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
}
return t
}
// New returns a new Injector.
func New() Injector {
return &injector{
values: make(map[reflect.Type]reflect.Value),
}
}
// Invoke attempts to call the interface{} provided as a function,
// providing dependencies for function arguments based on Type.
// Returns a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
// It panics if f is not a function
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
t := reflect.TypeOf(f)
var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
for i := 0; i < t.NumIn(); i++ {
argType := t.In(i)
val := inj.Get(argType)
if !val.IsValid() {
return nil, fmt.Errorf("Value not found for type %v", argType)
}
in[i] = val
}
return reflect.ValueOf(f).Call(in), nil
}
// Maps dependencies in the Type map to each field in the struct
// that is tagged with 'inject'.
// Returns an error if the injection fails.
func (inj *injector) Apply(val interface{}) error {
v := reflect.ValueOf(val)
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil // Should not panic here ?
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
structField := t.Field(i)
if f.CanSet() && (structField.Tag == "inject" || structField.Tag.Get("inject") != "") {
ft := f.Type()
v := inj.Get(ft)
if !v.IsValid() {
return fmt.Errorf("Value not found for type %v", ft)
}
f.Set(v)
}
}
return nil
}
// Maps the concrete value of val to its dynamic type using reflect.TypeOf,
// It returns the TypeMapper registered in.
func (i *injector) Map(val interface{}) TypeMapper {
i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
return i
}
func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
return i
}
// Maps the given reflect.Type to the given reflect.Value and returns
// the Typemapper the mapping has been registered in.
func (i *injector) Set(typ reflect.Type, val reflect.Value) TypeMapper {
i.values[typ] = val
return i
}
func (i *injector) Get(t reflect.Type) reflect.Value {
val := i.values[t]
if val.IsValid() {
return val
}
// no concrete types found, try to find implementors
// if t is an interface
if t.Kind() == reflect.Interface {
for k, v := range i.values {
if k.Implements(t) {
val = v
break
}
}
}
// Still no type found, try to look it up on the parent
if !val.IsValid() && i.parent != nil {
val = i.parent.Get(t)
}
return val
}
func (i *injector) SetParent(parent Injector) {
i.parent = parent
}

View File

@@ -0,0 +1,159 @@
package inject_test
import (
"fmt"
"github.com/codegangsta/inject"
"reflect"
"testing"
)
type SpecialString interface {
}
type TestStruct struct {
Dep1 string `inject:"t" json:"-"`
Dep2 SpecialString `inject`
Dep3 string
}
type Greeter struct {
Name string
}
func (g *Greeter) String() string {
return "Hello, My name is" + g.Name
}
/* Test Helpers */
func expect(t *testing.T, a interface{}, b interface{}) {
if a != b {
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
}
}
func refute(t *testing.T, a interface{}, b interface{}) {
if a == b {
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
}
}
func Test_InjectorInvoke(t *testing.T) {
injector := inject.New()
expect(t, injector == nil, false)
dep := "some dependency"
injector.Map(dep)
dep2 := "another dep"
injector.MapTo(dep2, (*SpecialString)(nil))
dep3 := make(chan *SpecialString)
dep4 := make(chan *SpecialString)
typRecv := reflect.ChanOf(reflect.RecvDir, reflect.TypeOf(dep3).Elem())
typSend := reflect.ChanOf(reflect.SendDir, reflect.TypeOf(dep4).Elem())
injector.Set(typRecv, reflect.ValueOf(dep3))
injector.Set(typSend, reflect.ValueOf(dep4))
_, err := injector.Invoke(func(d1 string, d2 SpecialString, d3 <-chan *SpecialString, d4 chan<- *SpecialString) {
expect(t, d1, dep)
expect(t, d2, dep2)
expect(t, reflect.TypeOf(d3).Elem(), reflect.TypeOf(dep3).Elem())
expect(t, reflect.TypeOf(d4).Elem(), reflect.TypeOf(dep4).Elem())
expect(t, reflect.TypeOf(d3).ChanDir(), reflect.RecvDir)
expect(t, reflect.TypeOf(d4).ChanDir(), reflect.SendDir)
})
expect(t, err, nil)
}
func Test_InjectorInvokeReturnValues(t *testing.T) {
injector := inject.New()
expect(t, injector == nil, false)
dep := "some dependency"
injector.Map(dep)
dep2 := "another dep"
injector.MapTo(dep2, (*SpecialString)(nil))
result, err := injector.Invoke(func(d1 string, d2 SpecialString) string {
expect(t, d1, dep)
expect(t, d2, dep2)
return "Hello world"
})
expect(t, result[0].String(), "Hello world")
expect(t, err, nil)
}
func Test_InjectorApply(t *testing.T) {
injector := inject.New()
injector.Map("a dep").MapTo("another dep", (*SpecialString)(nil))
s := TestStruct{}
err := injector.Apply(&s)
expect(t, err, nil)
expect(t, s.Dep1, "a dep")
expect(t, s.Dep2, "another dep")
expect(t, s.Dep3, "")
}
func Test_InterfaceOf(t *testing.T) {
iType := inject.InterfaceOf((*SpecialString)(nil))
expect(t, iType.Kind(), reflect.Interface)
iType = inject.InterfaceOf((**SpecialString)(nil))
expect(t, iType.Kind(), reflect.Interface)
// Expecting nil
defer func() {
rec := recover()
refute(t, rec, nil)
}()
iType = inject.InterfaceOf((*testing.T)(nil))
}
func Test_InjectorSet(t *testing.T) {
injector := inject.New()
typ := reflect.TypeOf("string")
typSend := reflect.ChanOf(reflect.SendDir, typ)
typRecv := reflect.ChanOf(reflect.RecvDir, typ)
// instantiating unidirectional channels is not possible using reflect
// http://golang.org/src/pkg/reflect/value.go?s=60463:60504#L2064
chanRecv := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, typ), 0)
chanSend := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, typ), 0)
injector.Set(typSend, chanSend)
injector.Set(typRecv, chanRecv)
expect(t, injector.Get(typSend).IsValid(), true)
expect(t, injector.Get(typRecv).IsValid(), true)
expect(t, injector.Get(chanSend.Type()).IsValid(), false)
}
func Test_InjectorGet(t *testing.T) {
injector := inject.New()
injector.Map("some dependency")
expect(t, injector.Get(reflect.TypeOf("string")).IsValid(), true)
expect(t, injector.Get(reflect.TypeOf(11)).IsValid(), false)
}
func Test_InjectorSetParent(t *testing.T) {
injector := inject.New()
injector.MapTo("another dep", (*SpecialString)(nil))
injector2 := inject.New()
injector2.SetParent(injector)
expect(t, injector2.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid(), true)
}
func TestInjectImplementors(t *testing.T) {
injector := inject.New()
g := &Greeter{"Jeremy"}
injector.Map(g)
expect(t, injector.Get(inject.InterfaceOf((*fmt.Stringer)(nil))).IsValid(), true)
}

View File

@@ -0,0 +1,85 @@
# inject
--
import "github.com/codegangsta/inject"
inject包提供了多种对实体的映射和依赖注入方式。
## 用法
#### func InterfaceOf
```go
func InterfaceOf(value interface{}) reflect.Type
```
函数InterfaceOf返回指向接口类型的指针。如果传入的value值不是指向接口的指针将抛出一个panic异常。
#### type Applicator
```go
type Applicator interface {
// 在Type map中维持对结构体中每个域的引用并用'inject'来标记
// 如果注入失败将会返回一个error.
Apply(interface{}) error
}
```
Applicator接口表示到结构体的依赖映射关系。
#### type Injector
```go
type Injector interface {
Applicator
Invoker
TypeMapper
// SetParent用来设置父injector. 如果在当前injector的Type map中找不到依赖
// 将会继续从它的父injector中找直到返回error.
SetParent(Injector)
}
```
Injector接口表示对结构体、函数参数的映射和依赖注入。
#### func New
```go
func New() Injector
```
New创建并返回一个Injector.
#### type Invoker
```go
type Invoker interface {
// Invoke尝试将interface{}作为一个函数来调用并基于Type为函数提供参数。
// 它将返回reflect.Value的切片其中存放原函数的返回值。
// 如果注入失败则返回error.
Invoke(interface{}) ([]reflect.Value, error)
}
```
Invoker接口表示通过反射进行函数调用。
#### type TypeMapper
```go
type TypeMapper interface {
// 基于调用reflect.TypeOf得到的类型映射interface{}的值。
Map(interface{}) TypeMapper
// 基于提供的接口的指针映射interface{}的值。
// 该函数仅用来将一个值映射为接口,因为接口无法不通过指针而直接引用到。
MapTo(interface{}, interface{}) TypeMapper
// 为直接插入基于类型和值的map提供一种可能性。
// 它使得这一类直接映射成为可能:无法通过反射直接实例化的类型参数,如单向管道。
Set(reflect.Type, reflect.Value) TypeMapper
// 返回映射到当前类型的Value. 如果Type没被映射将返回对应的零值。
Get(reflect.Type) reflect.Value
}
```
TypeMapper接口用来表示基于类型到接口值的映射。
## 译者
张强 (qqbunny@yeah.net)

View File

@@ -0,0 +1,3 @@
#!/bin/bash
go get github.com/robertkrimen/godocdown/godocdown
godocdown > README.md

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,69 @@
## OLD README
First give you a full example, I will explain every command below.
session := sh.NewSession()
session.Env["PATH"] = "/usr/bin:/bin"
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Alias("ll", "ls", "-l")
session.ShowCMD = true // enable for debug
var err error
err = session.Call("ll", "/")
if err != nil {
log.Fatal(err)
}
ret, err := session.Capture("pwd", sh.Dir("/home")) # wraper of session.Call
if err != nil {
log.Fatal(err)
}
# ret is "/home\n"
fmt.Println(ret)
create a new Session
session := sh.NewSession()
use alias like this
session.Alias("ll", "ls", "-l") # like alias ll='ls -l'
set current env like this
session.Env["BUILD_ID"] = "123" # like export BUILD_ID=123
set current directory
session.Set(sh.Dir("/")) # like cd /
pipe is also supported
session.Command("echo", "hello\tworld").Command("cut", "-f2")
// output should be "world"
session.Run()
test, the build in command support
session.Test("d", "dir") // test dir
session.Test("f", "file) // test regular file
with `Alias Env Set Call Capture Command` a shell scripts can be easily converted into golang program. below is a shell script.
#!/bin/bash -
#
export PATH=/usr/bin:/bin
alias ll='ls -l'
cd /usr
if test -d "local"
then
ll local | awk '{print $1, $NF}'
fi
convert to golang, will be
s := sh.NewSession()
s.Env["PATH"] = "/usr/bin:/bin"
s.Set(sh.Dir("/usr"))
s.Alias("ll", "ls", "-l")
if s.Test("d", "local") {
s.Command("ll", "local").Command("awk", "{print $1, $NF}").Run()
}

View File

@@ -0,0 +1,85 @@
## go-sh
[![wercker status](https://app.wercker.com/status/009acbd4f00ccc6de7e2554e12a50d84/s "wercker status")](https://app.wercker.com/project/bykey/009acbd4f00ccc6de7e2554e12a50d84)
[![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/codeskyblue/go-sh)
*If you depend on the old api, see tag: v.0.1*
install: `go get github.com/codeskyblue/go-sh`
Pipe Example:
package main
import "github.com/codeskyblue/go-sh"
func main() {
sh.Command("echo", "hello\tworld").Command("cut", "-f2").Run()
}
Because I like os/exec, `go-sh` is very much modelled after it. However, `go-sh` provides a better experience.
These are some of its features:
* keep the variable environment (e.g. export)
* alias support (e.g. alias in shell)
* remember current dir
* pipe command
* shell build-in commands echo & test
* timeout support
Examples are important:
sh: echo hello
go: sh.Command("echo", "hello").Run()
sh: export BUILD_ID=123
go: s = sh.NewSession().SetEnv("BUILD_ID", "123")
sh: alias ll='ls -l'
go: s = sh.NewSession().Alias('ll', 'ls', '-l')
sh: (cd /; pwd)
go: sh.Command("pwd", sh.Dir("/")).Run()
sh: test -d data || mkdir data
go: if ! sh.Test("dir", "data") { sh.Command("mkdir", "data").Run() }
sh: cat first second | awk '{print $1}'
go: sh.Command("cat", "first", "second").Command("awk", "{print $1}").Run()
sh: count=$(echo "one two three" | wc -w)
go: count, err := sh.Echo("one two three").Command("wc", "-w").Output()
sh(in ubuntu): timeout 1s sleep 3
go: c := sh.Command("sleep", "3"); c.Start(); c.WaitTimeout(time.Second) # default SIGKILL
go: out, err := sh.Command("sleep", "3").SetTimeout(time.Second).Output() # set session timeout and get output)
sh: echo hello | cat
go: out, err := sh.Command("cat").SetInput("hello").Output()
sh: cat # read from stdin
go: out, err := sh.Command("cat").SetStdin(os.Stdin).Output()
If you need to keep env and dir, it is better to create a session
session := sh.NewSession()
session.SetEnv("BUILD_ID", "123")
session.SetDir("/")
# then call cmd
session.Command("echo", "hello").Run()
# set ShowCMD to true for easily debug
session.ShowCMD = true
for more information, it better to see docs.
[![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/codeskyblue/go-sh)
### contribute
If you love this project, starring it will encourage the coder. Pull requests are welcome.
support the author: [alipay](https://me.alipay.com/goskyblue)
### thanks
this project is based on <http://github.com/codegangsta/inject>. thanks for the author.
# the reason to use Go shell
Sometimes we need to write shell scripts, but shell scripts are not good at working cross platform, Go, on the other hand, is good at that. Is there a good way to use Go to write shell like scripts? Using go-sh we can do this now.

View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"log"
"github.com/codeskyblue/go-sh"
)
func main() {
sh.Command("echo", "hello").Run()
out, err := sh.Command("echo", "hello").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println("output is", string(out))
var a int
sh.Command("echo", "2").UnmarshalJSON(&a)
fmt.Println("a =", a)
s := sh.NewSession()
s.Alias("hi", "echo", "hi")
s.Command("hi", "boy").Run()
fmt.Print("pwd = ")
s.Command("pwd", sh.Dir("/")).Run()
if !sh.Test("dir", "data") {
sh.Command("echo", "mkdir", "data").Run()
}
sh.Command("echo", "hello", "world").
Command("awk", `{print "second arg is "$2}`).Run()
s.ShowCMD = true
s.Command("echo", "hello", "world").
Command("awk", `{print "second arg is "$2}`).Run()
s.SetEnv("BUILD_ID", "123").Command("bash", "-c", "echo $BUILD_ID").Run()
s.Command("bash", "-c", "echo current shell is $SHELL").Run()
}

View File

@@ -0,0 +1,7 @@
package main
import "github.com/codeskyblue/go-sh"
func main() {
sh.Command("less", "less.go").Run()
}

View File

@@ -0,0 +1,17 @@
package main
import (
"flag"
"fmt"
"github.com/codeskyblue/go-sh"
)
func main() {
flag.Parse()
if flag.NArg() != 1 {
fmt.Println("Usage: PROGRAM <file>")
return
}
sh.Command("tail", "-f", flag.Arg(0)).Run()
}

View File

@@ -0,0 +1,23 @@
package main
import (
"fmt"
"time"
sh "github.com/codeskyblue/go-sh"
)
func main() {
c := sh.Command("sleep", "3")
c.Start()
err := c.WaitTimeout(time.Second * 1)
if err != nil {
fmt.Printf("timeout should happend: %v\n", err)
}
// timeout should be a session
out, err := sh.Command("sleep", "2").SetTimeout(time.Second).Output()
fmt.Printf("output:(%s), err(%v)\n", string(out), err)
out, err = sh.Command("echo", "hello").SetTimeout(time.Second).Output()
fmt.Printf("output:(%s), err(%v)\n", string(out), err)
}

View File

@@ -0,0 +1,28 @@
package sh_test
import (
"fmt"
"github.com/codeskyblue/go-sh"
)
func ExampleCommand() {
out, err := sh.Command("echo", "hello").Output()
fmt.Println(string(out), err)
}
func ExampleCommandPipe() {
out, err := sh.Command("echo", "-n", "hi").Command("wc", "-c").Output()
fmt.Println(string(out), err)
}
func ExampleCommandSetDir() {
out, err := sh.Command("pwd", sh.Dir("/")).Output()
fmt.Println(string(out), err)
}
func ExampleTest() {
if sh.Test("dir", "mydir") {
fmt.Println("mydir exists")
}
}

View File

@@ -0,0 +1,148 @@
package sh
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"io"
"os"
"strings"
"syscall"
"time"
)
var ErrExecTimeout = errors.New("execute timeout")
// unmarshal shell output to decode json
func (s *Session) UnmarshalJSON(data interface{}) (err error) {
bufrw := bytes.NewBuffer(nil)
s.Stdout = bufrw
if err = s.Run(); err != nil {
return
}
return json.NewDecoder(bufrw).Decode(data)
}
// unmarshal command output into xml
func (s *Session) UnmarshalXML(data interface{}) (err error) {
bufrw := bytes.NewBuffer(nil)
s.Stdout = bufrw
if err = s.Run(); err != nil {
return
}
return xml.NewDecoder(bufrw).Decode(data)
}
// start command
func (s *Session) Start() (err error) {
s.started = true
var rd *io.PipeReader
var wr *io.PipeWriter
var length = len(s.cmds)
if s.ShowCMD {
var cmds = make([]string, 0, 4)
for _, cmd := range s.cmds {
cmds = append(cmds, strings.Join(cmd.Args, " "))
}
s.writePrompt(strings.Join(cmds, " | "))
}
for index, cmd := range s.cmds {
if index == 0 {
cmd.Stdin = s.Stdin
} else {
cmd.Stdin = rd
}
if index != length {
rd, wr = io.Pipe() // create pipe
cmd.Stdout = wr
cmd.Stderr = os.Stderr
}
if index == length-1 {
cmd.Stdout = s.Stdout
cmd.Stderr = s.Stderr
}
err = cmd.Start()
if err != nil {
return
}
}
return
}
// Should be call after Start()
// only catch the last command error
func (s *Session) Wait() (err error) {
for _, cmd := range s.cmds {
err = cmd.Wait()
wr, ok := cmd.Stdout.(*io.PipeWriter)
if ok {
wr.Close()
}
}
return err
}
func (s *Session) Kill(sig os.Signal) {
for _, cmd := range s.cmds {
if cmd.Process != nil {
cmd.Process.Signal(sig)
}
}
}
func (s *Session) WaitTimeout(timeout time.Duration) (err error) {
select {
case <-time.After(timeout):
s.Kill(syscall.SIGKILL)
return ErrExecTimeout
case err = <-Go(s.Wait):
return err
}
}
func Go(f func() error) chan error {
ch := make(chan error)
go func() {
ch <- f()
}()
return ch
}
func (s *Session) Run() (err error) {
if err = s.Start(); err != nil {
return
}
if s.timeout != time.Duration(0) {
return s.WaitTimeout(s.timeout)
}
return s.Wait()
}
func (s *Session) Output() (out []byte, err error) {
oldout := s.Stdout
defer func() {
s.Stdout = oldout
}()
stdout := bytes.NewBuffer(nil)
s.Stdout = stdout
err = s.Run()
out = stdout.Bytes()
return
}
func (s *Session) CombinedOutput() (out []byte, err error) {
oldout := s.Stdout
olderr := s.Stderr
defer func() {
s.Stdout = oldout
s.Stderr = olderr
}()
stdout := bytes.NewBuffer(nil)
s.Stdout = stdout
s.Stderr = stdout
err = s.Run()
out = stdout.Bytes()
return
}

View File

@@ -0,0 +1,126 @@
package sh
import (
"encoding/xml"
"io"
"os"
"os/exec"
"strings"
"testing"
"time"
)
func TestUnmarshalJSON(t *testing.T) {
var a int
s := NewSession()
s.ShowCMD = true
err := s.Command("echo", []string{"1"}).UnmarshalJSON(&a)
if err != nil {
t.Error(err)
}
if a != 1 {
t.Errorf("expect a tobe 1, but got %d", a)
}
}
func TestUnmarshalXML(t *testing.T) {
s := NewSession()
xmlSample := `<?xml version="1.0" encoding="utf-8"?>
<server version="1" />`
type server struct {
XMLName xml.Name `xml:"server"`
Version string `xml:"version,attr"`
}
data := &server{}
s.Command("echo", xmlSample).UnmarshalXML(data)
if data.Version != "1" {
t.Error(data)
}
}
func TestPipe(t *testing.T) {
s := NewSession()
s.ShowCMD = true
s.Call("echo", "hello")
err := s.Command("echo", "hi").Command("cat", "-n").Start()
if err != nil {
t.Error(err)
}
err = s.Wait()
if err != nil {
t.Error(err)
}
out, err := s.Command("echo", []string{"hello"}).Output()
if err != nil {
t.Error(err)
}
if string(out) != "hello\n" {
t.Error("capture wrong output:", out)
}
s.Command("echo", []string{"hello\tworld"}).Command("cut", []string{"-f2"}).Run()
}
func TestPipeCommand(t *testing.T) {
c1 := exec.Command("echo", "good")
rd, wr := io.Pipe()
c1.Stdout = wr
c2 := exec.Command("cat", "-n")
c2.Stdout = os.Stdout
c2.Stdin = rd
c1.Start()
c2.Start()
c1.Wait()
wc, ok := c1.Stdout.(io.WriteCloser)
if ok {
wc.Close()
}
c2.Wait()
}
func TestPipeInput(t *testing.T) {
s := NewSession()
s.ShowCMD = true
s.SetInput("first line\nsecond line\n")
out, err := s.Command("grep", "second").Output()
if err != nil {
t.Error(err)
}
if string(out) != "second line\n" {
t.Error("capture wrong output:", out)
}
}
func TestTimeout(t *testing.T) {
s := NewSession()
err := s.Command("sleep", "2").Start()
if err != nil {
t.Fatal(err)
}
err = s.WaitTimeout(time.Second)
if err != ErrExecTimeout {
t.Fatal(err)
}
}
func TestSetTimeout(t *testing.T) {
s := NewSession()
s.SetTimeout(time.Second)
defer s.SetTimeout(0)
err := s.Command("sleep", "2").Run()
if err != ErrExecTimeout {
t.Fatal(err)
}
}
func TestCombinedOutput(t *testing.T) {
s := NewSession()
bytes, err := s.Command("sh", "-c", "echo stderr >&2 ; echo stdout").CombinedOutput()
if err != nil {
t.Error(err)
}
stringOutput := string(bytes)
if !(strings.Contains(stringOutput, "stdout") && strings.Contains(stringOutput, "stderr")) {
t.Errorf("expect output from both output streams, got '%s'", strings.TrimSpace(stringOutput))
}
}

View File

@@ -0,0 +1,200 @@
/*
Package go-sh is intented to make shell call with golang more easily.
Some usage is more similar to os/exec, eg: Run(), Output(), Command(name, args...)
But with these similar function, pipe is added in and this package also got shell-session support.
Why I love golang so much, because the usage of golang is simple, but the power is unlimited. I want to make this pakcage got the sample style like golang.
// just like os/exec
sh.Command("echo", "hello").Run()
// support pipe
sh.Command("echo", "hello").Command("wc", "-c").Run()
// create a session to store dir and env
sh.NewSession().SetDir("/").Command("pwd")
// shell buildin command - "test"
sh.Test("dir", "mydir")
// like shell call: (cd /; pwd)
sh.Command("pwd", sh.Dir("/")) same with sh.Command(sh.Dir("/"), "pwd")
// output to json and xml easily
v := map[string] int {}
err = sh.Command("echo", `{"number": 1}`).UnmarshalJSON(&v)
*/
package sh
import (
"fmt"
"io"
"os"
"os/exec"
"reflect"
"strings"
"time"
"github.com/codegangsta/inject"
)
type Dir string
type Session struct {
inj inject.Injector
alias map[string][]string
cmds []*exec.Cmd
dir Dir
started bool
Env map[string]string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
ShowCMD bool // enable for debug
timeout time.Duration
}
func (s *Session) writePrompt(args ...interface{}) {
var ps1 = fmt.Sprintf("[golang-sh]$")
args = append([]interface{}{ps1}, args...)
fmt.Fprintln(s.Stderr, args...)
}
func NewSession() *Session {
env := make(map[string]string)
for _, key := range []string{"PATH"} {
env[key] = os.Getenv(key)
}
s := &Session{
inj: inject.New(),
alias: make(map[string][]string),
dir: Dir(""),
Stdin: strings.NewReader(""),
Stdout: os.Stdout,
Stderr: os.Stderr,
Env: env,
}
return s
}
func InteractiveSession() *Session {
s := NewSession()
s.SetStdin(os.Stdin)
return s
}
func Command(name string, a ...interface{}) *Session {
s := NewSession()
return s.Command(name, a...)
}
func Echo(in string) *Session {
s := NewSession()
return s.SetInput(in)
}
func (s *Session) Alias(alias, cmd string, args ...string) {
v := []string{cmd}
v = append(v, args...)
s.alias[alias] = v
}
func (s *Session) Command(name string, a ...interface{}) *Session {
var args = make([]string, 0)
var sType = reflect.TypeOf("")
// init cmd, args, dir, envs
// if not init, program may panic
s.inj.Map(name).Map(args).Map(s.dir).Map(map[string]string{})
for _, v := range a {
switch reflect.TypeOf(v) {
case sType:
args = append(args, v.(string))
default:
s.inj.Map(v)
}
}
if len(args) != 0 {
s.inj.Map(args)
}
s.inj.Invoke(s.appendCmd)
return s
}
// combine Command and Run
func (s *Session) Call(name string, a ...interface{}) error {
return s.Command(name, a...).Run()
}
/*
func (s *Session) Exec(cmd string, args ...string) error {
return s.Call(cmd, args)
}
*/
func (s *Session) SetEnv(key, value string) *Session {
s.Env[key] = value
return s
}
func (s *Session) SetDir(dir string) *Session {
s.dir = Dir(dir)
return s
}
func (s *Session) SetInput(in string) *Session {
s.Stdin = strings.NewReader(in)
return s
}
func (s *Session) SetStdin(r io.Reader) *Session {
s.Stdin = r
return s
}
func (s *Session) SetTimeout(d time.Duration) *Session {
s.timeout = d
return s
}
func newEnviron(env map[string]string, inherit bool) []string { //map[string]string {
environ := make([]string, 0, len(env))
if inherit {
for _, line := range os.Environ() {
for k, _ := range env {
if strings.HasPrefix(line, k+"=") {
goto CONTINUE
}
}
environ = append(environ, line)
CONTINUE:
}
}
for k, v := range env {
environ = append(environ, k+"="+v)
}
return environ
}
func (s *Session) appendCmd(cmd string, args []string, cwd Dir, env map[string]string) {
if s.started {
s.started = false
s.cmds = make([]*exec.Cmd, 0)
}
for k, v := range s.Env {
if _, ok := env[k]; !ok {
env[k] = v
}
}
environ := newEnviron(s.Env, true) // true: inherit sys-env
v, ok := s.alias[cmd]
if ok {
cmd = v[0]
args = append(v[1:], args...)
}
c := exec.Command(cmd, args...)
c.Env = environ
c.Dir = string(cwd)
s.cmds = append(s.cmds, c)
}

View File

@@ -0,0 +1,106 @@
package sh
import (
"fmt"
"log"
"runtime"
"strings"
"testing"
)
func TestAlias(t *testing.T) {
s := NewSession()
s.Alias("gr", "echo", "hi")
out, err := s.Command("gr", "sky").Output()
if err != nil {
t.Error(err)
}
if string(out) != "hi sky\n" {
t.Errorf("expect 'hi sky' but got:%s", string(out))
}
}
func ExampleSession_Command() {
s := NewSession()
out, err := s.Command("echo", "hello").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
// Output: hello
}
func ExampleSession_Command_pipe() {
s := NewSession()
out, err := s.Command("echo", "hello", "world").Command("awk", "{print $2}").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
// Output: world
}
func ExampleSession_Alias() {
s := NewSession()
s.Alias("alias_echo_hello", "echo", "hello")
out, err := s.Command("alias_echo_hello", "world").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
// Output: hello world
}
func TestEcho(t *testing.T) {
out, err := Echo("one two three").Command("wc", "-w").Output()
if err != nil {
t.Error(err)
}
if strings.TrimSpace(string(out)) != "3" {
t.Errorf("expect '3' but got:%s", string(out))
}
}
func TestSession(t *testing.T) {
if runtime.GOOS == "windows" {
t.Log("ignore test on windows")
return
}
session := NewSession()
session.ShowCMD = true
err := session.Call("pwd")
if err != nil {
t.Error(err)
}
out, err := session.SetDir("/").Command("pwd").Output()
if err != nil {
t.Error(err)
}
if string(out) != "/\n" {
t.Errorf("expect /, but got %s", string(out))
}
}
/*
#!/bin/bash -
#
export PATH=/usr/bin:/bin
alias ll='ls -l'
cd /usr
if test -d "local"
then
ll local | awk '{print $1, $NF}' | grep bin
fi
*/
func Example(t *testing.T) {
s := NewSession()
//s.ShowCMD = true
s.Env["PATH"] = "/usr/bin:/bin"
s.SetDir("/bin")
s.Alias("ll", "ls", "-l")
if s.Test("d", "local") {
//s.Command("ll", []string{"local"}).Command("awk", []string{"{print $1, $NF}"}).Command("grep", []string{"bin"}).Run()
s.Command("ll", "local").Command("awk", "{print $1, $NF}").Command("grep", "bin").Run()
}
}

View File

@@ -0,0 +1,64 @@
package sh
import (
"os"
"path/filepath"
)
func filetest(name string, modemask os.FileMode) (match bool, err error) {
fi, err := os.Stat(name)
if err != nil {
return
}
match = (fi.Mode() & modemask) == modemask
return
}
func (s *Session) pwd() string {
dir := string(s.dir)
if dir == "" {
dir, _ = os.Getwd()
}
return dir
}
func (s *Session) abspath(name string) string {
if filepath.IsAbs(name) {
return name
}
return filepath.Join(s.pwd(), name)
}
func init() {
//log.SetFlags(log.Lshortfile | log.LstdFlags)
}
// expression can be dir, file, link
func (s *Session) Test(expression string, argument string) bool {
var err error
var fi os.FileInfo
fi, err = os.Lstat(s.abspath(argument))
switch expression {
case "d", "dir":
return err == nil && fi.IsDir()
case "f", "file":
return err == nil && fi.Mode().IsRegular()
case "x", "executable":
/*
fmt.Println(expression, argument)
if err == nil {
fmt.Println(fi.Mode())
}
*/
return err == nil && fi.Mode()&os.FileMode(0100) != 0
case "L", "link":
return err == nil && fi.Mode()&os.ModeSymlink != 0
}
return false
}
// expression can be d,dir, f,file, link
func Test(exp string, arg string) bool {
s := NewSession()
return s.Test(exp, arg)
}

View File

@@ -0,0 +1,58 @@
package sh_test
import (
"testing"
"github.com/codeskyblue/go-sh"
)
var s = sh.NewSession()
type T struct{ *testing.T }
func NewT(t *testing.T) *T {
return &T{t}
}
func (t *T) checkTest(exp string, arg string, result bool) {
r := s.Test(exp, arg)
if r != result {
t.Errorf("test -%s %s, %v != %v", exp, arg, r, result)
}
}
func TestTest(i *testing.T) {
t := NewT(i)
t.checkTest("d", "../go-sh", true)
t.checkTest("d", "./yymm", false)
// file test
t.checkTest("f", "testdata/hello.txt", true)
t.checkTest("f", "testdata/xxxxx", false)
t.checkTest("f", "testdata/yymm", false)
// link test
t.checkTest("link", "testdata/linkfile", true)
t.checkTest("link", "testdata/xxxxxlinkfile", false)
t.checkTest("link", "testdata/hello.txt", false)
// executable test
t.checkTest("x", "testdata/executable", true)
t.checkTest("x", "testdata/xxxxx", false)
t.checkTest("x", "testdata/hello.txt", false)
}
func ExampleShellTest(t *testing.T) {
// test -L
sh.Test("link", "testdata/linkfile")
sh.Test("L", "testdata/linkfile")
// test -f
sh.Test("file", "testdata/file")
sh.Test("f", "testdata/file")
// test -x
sh.Test("executable", "testdata/binfile")
sh.Test("x", "testdata/binfile")
// test -d
sh.Test("dir", "testdata/dir")
sh.Test("d", "testdata/dir")
}

View File

View File

View File

@@ -0,0 +1 @@
hello.txt

View File

@@ -0,0 +1,28 @@
box: wercker/golang
# Build definition
build:
# The steps that will be executed on build
steps:
# Sets the go workspace and places you package
# at the right place in the workspace tree
- setup-go-workspace
# Gets the dependencies
- script:
name: go get
code: |
cd $WERCKER_SOURCE_DIR
go version
go get -t .
# Build the project
- script:
name: go build
code: |
go build .
# Test the project
- script:
name: go test
code: |
go test -v ./...

14
plugins/repo/Makefile Normal file
View File

@@ -0,0 +1,14 @@
.PHONY: build-in-docker build commands subcommands
build-in-docker: clean
docker run --rm \
-v $$PWD/../..:$(GO_REPO_ROOT) \
-w $(GO_REPO_ROOT)/plugins/repo \
$(BUILD_IMAGE) \
bash -c "make build" || exit $$?
build:
cd src && go build -a -o ../commands
clean:
rm -f commands

View File

@@ -0,0 +1,58 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
columnize "github.com/ryanuber/columnize"
)
const (
helpHeader = `Usage: dokku repo[:COMMAND]
Runs commands that interact with the app's repo
Additional commands:`
helpContent = `
repo:gc <app>, Runs 'git gc --aggressive' against the application's repo
repo:purge-cache <app>, Deletes the contents of the build cache stored in the repository
`
)
func main() {
flag.Usage = usage
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "repo:help":
usage()
case "help":
fmt.Print(helpContent)
case "repo:gc":
gitGC()
case "repo:purge-cache":
purgeCache()
default:
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
if err != nil {
fmt.Println("failed to parse DOKKU_NOT_IMPLEMENTED_EXIT")
dokkuNotImplementExitCode = 10
}
os.Exit(dokkuNotImplementExitCode)
}
}
func usage() {
config := columnize.DefaultConfig()
config.Delim = ","
config.Prefix = "\t"
config.Empty = ""
content := strings.Split(helpContent, "\n")[1:]
fmt.Println(helpHeader)
fmt.Println(columnize.Format(content, config))
}

29
plugins/repo/src/gc.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"flag"
"strings"
common "github.com/dokku/dokku/plugins/common"
)
// runs 'git gc --aggressive' against the application's repo
func gitGC() {
flag.Parse()
appName := flag.Arg(1)
if appName == "" {
common.LogFail("Please specify an app to run the command on")
}
err := common.VerifyAppName(appName)
if err != nil {
common.LogFail(err.Error())
}
appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/")
cmdEnv := map[string]string{
"GIT_DIR": appRoot,
}
gitGcCmd := common.NewDokkuCmd("git gc --aggressive")
gitGcCmd.Env = cmdEnv
gitGcCmd.Execute()
}

6
plugins/repo/src/glide.lock generated Normal file
View File

@@ -0,0 +1,6 @@
hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149
updated: 2017-01-03T17:16:50.97156327-08:00
imports:
- name: github.com/ryanuber/columnize
version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360
testImports: []

View File

@@ -0,0 +1,3 @@
package: .
import:
- package: github.com/ryanuber/columnize

View File

@@ -0,0 +1,36 @@
package main
import (
"flag"
"os"
"strings"
common "github.com/dokku/dokku/plugins/common"
)
// deletes the contents of the build cache stored in the repository
func purgeCache() {
flag.Parse()
appName := flag.Arg(1)
if appName == "" {
common.LogFail("Please specify an app to run the command on")
}
err := common.VerifyAppName(appName)
if err != nil {
common.LogFail(err.Error())
}
cacheDir := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName, "cache"}, "/")
dokkuGlobalRunArgs := common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS")
image := common.GetDeployingAppImageName(appName)
if info, _ := os.Stat(cacheDir); info != nil && info.IsDir() {
purgeCacheCmd := common.NewDokkuCmd(strings.Join([]string{"docker run --rm", dokkuGlobalRunArgs,
"-v", strings.Join([]string{cacheDir, ":/cache"}, ""), image,
`find /cache -depth -mindepth 1 -maxdepth 1 -exec rm -Rf {} ;`}, " "))
purgeCacheCmd.Execute()
err := os.MkdirAll(cacheDir, 0644)
if err != nil {
common.LogFail(err.Error())
}
}
}

View File

@@ -0,0 +1,3 @@
language: go
go:
- tip

View File

@@ -0,0 +1,20 @@
Copyright (c) 2016 Ryan Uber
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,69 @@
Columnize
=========
Easy column-formatted output for golang
[![Build Status](https://travis-ci.org/ryanuber/columnize.svg)](https://travis-ci.org/ryanuber/columnize)
[![GoDoc](https://godoc.org/github.com/ryanuber/columnize?status.svg)](https://godoc.org/github.com/ryanuber/columnize)
Columnize is a really small Go package that makes building CLI's a little bit
easier. In some CLI designs, you want to output a number similar items in a
human-readable way with nicely aligned columns. However, figuring out how wide
to make each column is a boring problem to solve and eats your valuable time.
Here is an example:
```go
package main
import (
"fmt"
"github.com/ryanuber/columnize"
)
func main() {
output := []string{
"Name | Gender | Age",
"Bob | Male | 38",
"Sally | Female | 26",
}
result := columnize.SimpleFormat(output)
fmt.Println(result)
}
```
As you can see, you just pass in a list of strings. And the result:
```
Name Gender Age
Bob Male 38
Sally Female 26
```
Columnize is tolerant of missing or empty fields, or even empty lines, so
passing in extra lines for spacing should show up as you would expect.
Configuration
=============
Columnize is configured using a `Config`, which can be obtained by calling the
`DefaultConfig()` method. You can then tweak the settings in the resulting
`Config`:
```
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = ""
config.Empty = ""
```
* `Delim` is the string by which columns of **input** are delimited
* `Glue` is the string by which columns of **output** are delimited
* `Prefix` is a string by which each line of **output** is prefixed
* `Empty` is a string used to replace blank values found in output
You can then pass the `Config` in using the `Format` method (signature below) to
have text formatted to your liking.
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.

View File

@@ -0,0 +1,178 @@
package columnize
import (
"bytes"
"fmt"
"strings"
)
// Config can be used to tune certain parameters which affect the way
// in which Columnize will format output text.
type Config struct {
// The string by which the lines of input will be split.
Delim string
// The string by which columns of output will be separated.
Glue string
// The string by which columns of output will be prefixed.
Prefix string
// A replacement string to replace empty fields
Empty string
}
// DefaultConfig returns a *Config with default values.
func DefaultConfig() *Config {
return &Config{
Delim: "|",
Glue: " ",
Prefix: "",
Empty: "",
}
}
// MergeConfig merges two config objects together and returns the resulting
// configuration. Values from the right take precedence over the left side.
func MergeConfig(a, b *Config) *Config {
var result Config = *a
// Return quickly if either side was nil
if a == nil || b == nil {
return &result
}
if b.Delim != "" {
result.Delim = b.Delim
}
if b.Glue != "" {
result.Glue = b.Glue
}
if b.Prefix != "" {
result.Prefix = b.Prefix
}
if b.Empty != "" {
result.Empty = b.Empty
}
return &result
}
// stringFormat, given a set of column widths and the number of columns in
// the current line, returns a sprintf-style format string which can be used
// to print output aligned properly with other lines using the same widths set.
func stringFormat(c *Config, widths []int, columns int) string {
// Create the buffer with an estimate of the length
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
// Start with the prefix, if any was given. The buffer will not return an
// error so it does not need to be handled
buf.WriteString(c.Prefix)
// Create the format string from the discovered widths
for i := 0; i < columns && i < len(widths); i++ {
if i == columns-1 {
buf.WriteString("%s\n")
} else {
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
}
}
return buf.String()
}
// elementsFromLine returns a list of elements, each representing a single
// item which will belong to a column of output.
func elementsFromLine(config *Config, line string) []interface{} {
seperated := strings.Split(line, config.Delim)
elements := make([]interface{}, len(seperated))
for i, field := range seperated {
value := strings.TrimSpace(field)
// Apply the empty value, if configured.
if value == "" && config.Empty != "" {
value = config.Empty
}
elements[i] = value
}
return elements
}
// runeLen calculates the number of visible "characters" in a string
func runeLen(s string) int {
l := 0
for _ = range s {
l++
}
return l
}
// widthsFromLines examines a list of strings and determines how wide each
// column should be considering all of the elements that need to be printed
// within it.
func widthsFromLines(config *Config, lines []string) []int {
widths := make([]int, 0, 8)
for _, line := range lines {
elems := elementsFromLine(config, line)
for i := 0; i < len(elems); i++ {
l := runeLen(elems[i].(string))
if len(widths) <= i {
widths = append(widths, l)
} else if widths[i] < l {
widths[i] = l
}
}
}
return widths
}
// Format is the public-facing interface that takes a list of strings and
// returns nicely aligned column-formatted text.
func Format(lines []string, config *Config) string {
conf := MergeConfig(DefaultConfig(), config)
widths := widthsFromLines(conf, lines)
// Estimate the buffer size
glueSize := len(conf.Glue)
var size int
for _, w := range widths {
size += w + glueSize
}
size *= len(lines)
// Create the buffer
buf := bytes.NewBuffer(make([]byte, 0, size))
// Create a cache for the string formats
fmtCache := make(map[int]string, 16)
// Create the formatted output using the format string
for _, line := range lines {
elems := elementsFromLine(conf, line)
// Get the string format using cache
numElems := len(elems)
stringfmt, ok := fmtCache[numElems]
if !ok {
stringfmt = stringFormat(conf, widths, numElems)
fmtCache[numElems] = stringfmt
}
fmt.Fprintf(buf, stringfmt, elems...)
}
// Get the string result
result := buf.String()
// Remove trailing newline without removing leading/trailing space
if n := len(result); n > 0 && result[n-1] == '\n' {
result = result[:n-1]
}
return result
}
// SimpleFormat is a convenience function to format text with the defaults.
func SimpleFormat(lines []string) string {
return Format(lines, nil)
}

View File

@@ -0,0 +1,306 @@
package columnize
import (
"fmt"
"testing"
crand "crypto/rand"
)
func TestListOfStringsInput(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyLinesOutput(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestLeadingSpacePreserved(t *testing.T) {
input := []string{
"| Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := " Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestColumnWidthCalculator(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"Longer than A | Longer than B | Longer than C",
"short | short | short",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "Longer than A Longer than B Longer than C\n"
expected += "short short short"
if output != expected {
printableProof := fmt.Sprintf("\nGot: %+q", output)
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
t.Fatalf("\n%s", printableProof)
}
}
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
"short | short | short",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
expected += "short short short"
if output != expected {
printableProof := fmt.Sprintf("\nGot: %+q", output)
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
t.Fatalf("\n%s", printableProof)
}
}
func BenchmarkColumnWidthCalculator(b *testing.B) {
// Generate the input
input := []string{
"UUID A | UUID B | UUID C | Column D | Column E",
}
format := "%s|%s|%s|%s"
short := "short"
uuid := func() string {
buf := make([]byte, 16)
if _, err := crand.Read(buf); err != nil {
panic(fmt.Errorf("failed to read random bytes: %v", err))
}
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
buf[0:4],
buf[4:6],
buf[6:8],
buf[8:10],
buf[10:16])
}
for i := 0; i < 1000; i++ {
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
input = append(input, l)
}
config := DefaultConfig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Format(input, config)
}
}
func TestVariedInputSpacing(t *testing.T) {
input := []string{
"Column A |Column B| Column C",
"x|y| z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestUnmatchedColumnCounts(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"Value A | Value B",
"Value A | Value B | Value C | Value D",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "Value A Value B\n"
expected += "Value A Value B Value C Value D"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternateDelimiter(t *testing.T) {
input := []string{
"Column | A % Column | B % Column | C",
"Value A % Value B % Value C",
}
config := DefaultConfig()
config.Delim = "%"
output := Format(input, config)
expected := "Column | A Column | B Column | C\n"
expected += "Value A Value B Value C"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternateSpacingString(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
config.Glue = " "
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestSimpleFormat(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
output := SimpleFormat(input)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternatePrefixString(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
config.Prefix = " "
output := Format(input, config)
expected := " Column A Column B Column C\n"
expected += " x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyFieldReplacement(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | | z",
}
config := DefaultConfig()
config.Empty = "<none>"
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x <none> z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyConfigValues(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := Config{}
output := Format(input, &config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestMergeConfig(t *testing.T) {
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
conf3 := &Config{Delim: "c", Prefix: "c"}
m := MergeConfig(conf1, conf2)
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, conf3)
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, nil)
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, &Config{})
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
}

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
repo_purge_cache() {
declare desc="deletes the contents of the build cache stored in the repository"
local cmd="repo:purge-cache"
[[ -z $2 ]] && dokku_log_fail "Please specify an app to run the command on"
local APP="$2";
verify_app_name "$APP"
local IMAGE=$(get_deploying_app_image_name "$APP"); local CACHE_DIR="$DOKKU_ROOT/$APP/cache"
if [[ -d $CACHE_DIR ]]; then
docker run "$DOKKU_GLOBAL_RUN_ARGS" --rm -v "$CACHE_DIR:/cache" "$IMAGE" find /cache -depth -mindepth 1 -maxdepth 1 -exec rm -Rf {} \; || true
mkdir -p "$CACHE_DIR" || true
fi
}
repo_purge_cache "$@"

View File

@@ -73,8 +73,18 @@ lint:
@echo linting...
@$(QUIET) find . -not -path '*/\.*' -not -path './debian/*' -type f | xargs file | grep text | awk -F ':' '{ print $$1 }' | xargs head -n1 | egrep -B1 "bash" | grep "==>" | awk '{ print $$2 }' | xargs shellcheck -e SC2034
unit-tests:
@echo running unit tests...
go-tests:
@echo running go unit tests...
docker run --rm -ti \
-e DOKKU_ROOT=/home/dokku \
-v $$PWD:$(GO_REPO_ROOT) \
-w $(GO_REPO_ROOT) \
$(BUILD_IMAGE) \
bash -c "go get github.com/onsi/gomega && \
go list ./... | grep -v /vendor/ | grep -v /tests/apps/ | xargs go test -v -p 1 -race" || exit $$?
unit-tests: go-tests
@echo running bats unit tests...
ifndef UNIT_TEST_BATCH
@$(QUIET) bats tests/unit
else