cmd/construct: start work on eclier frontend
This commit is contained in:
parent
a6fce981ef
commit
18a2049013
|
@ -1,6 +1,18 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ThomasRooney/gexpect"
|
||||
packages = ["."]
|
||||
revision = "5482f03509440585d13d8f648989e05903001842"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Xe/eclier"
|
||||
packages = ["."]
|
||||
revision = "b586327df9d5a5e5023fa1338f44941745136d68"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Xe/gopreload"
|
||||
|
@ -22,6 +34,15 @@
|
|||
packages = ["."]
|
||||
revision = "62b230097e9c9534ca2074782b25d738c4b68964"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Xe/x"
|
||||
packages = [
|
||||
"tools/glue/libs/gluaexpect",
|
||||
"tools/glue/libs/gluasimplebox"
|
||||
]
|
||||
revision = "860ea0dedb8beb93b60717510eabca2ef5ffe150"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/aclements/go-moremath"
|
||||
|
@ -43,6 +64,12 @@
|
|||
revision = "5f10fee965225ac1eecdc234c09daf5cd9e7f7b6"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ailncode/gluaxmlpath"
|
||||
packages = ["."]
|
||||
revision = "6ce478ecb4a60c4fc8929838e0b21b7fb7ca7440"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
|
@ -160,6 +187,18 @@
|
|||
revision = "7cd7992b3bc86f920394f8de92c13900da1a46b7"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluahttp"
|
||||
packages = ["."]
|
||||
revision = "b4bfe0c50fea948dcbf3966e120996d6607bbd89"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluaurl"
|
||||
packages = ["."]
|
||||
revision = "31cbb9bef199454415879f2e6d609d1136d60cad"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/bbolt"
|
||||
packages = ["."]
|
||||
|
@ -354,6 +393,12 @@
|
|||
packages = ["."]
|
||||
revision = "683f49123a33db61abfb241b7ac5e4af4dc54d55"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/howeyc/gopass"
|
||||
packages = ["."]
|
||||
revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
packages = ["."]
|
||||
|
@ -380,6 +425,12 @@
|
|||
packages = ["."]
|
||||
revision = "ae77be60afb1dcacde03767a8c37337fad28ac14"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
packages = ["."]
|
||||
revision = "cd60e84ee657ff3dc51de0b4f55dd299a3e136f2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/klauspost/cpuid"
|
||||
packages = ["."]
|
||||
|
@ -392,27 +443,66 @@
|
|||
revision = "6bb6130ff6a76a904c1841707d65603aec9cc288"
|
||||
version = "v1.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaenv"
|
||||
packages = ["."]
|
||||
revision = "2888db6bbe38923d59c42e443895875cc8ce0820"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluafs"
|
||||
packages = ["."]
|
||||
revision = "01391ed2d7ab89dc80157605b073403f960aa223"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaquestion"
|
||||
packages = ["."]
|
||||
revision = "311437c29ba54d027ad2af383661725ae2bfdcdc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluassh"
|
||||
packages = ["."]
|
||||
revision = "2a7bd48d7568de8230c87ac1ef4a4c481e45814d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluatemplate"
|
||||
packages = ["."]
|
||||
revision = "d9e2c9d6b00f069a9da377a9ac529c827c1c7d71"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluayaml"
|
||||
packages = ["."]
|
||||
revision = "6fe413d49d73d785510ecf1529991ab0573e96c7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/fs"
|
||||
packages = ["."]
|
||||
revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/pretty"
|
||||
packages = ["."]
|
||||
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/kr/pty"
|
||||
packages = ["."]
|
||||
revision = "282ce0e5322c82529687d609ee670fac7c7d917c"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/text"
|
||||
packages = ["."]
|
||||
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid"
|
||||
]
|
||||
revision = "27ea5d92de30060e7121ddd543fe14e9a327e0cc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lucas-clemente/aes12"
|
||||
|
@ -463,18 +553,6 @@
|
|||
revision = "ab3ca2f6f85577d7ec82e0a6df721147a2e737f9"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattes/migrate"
|
||||
packages = [
|
||||
".",
|
||||
"database",
|
||||
"database/postgres",
|
||||
"source",
|
||||
"source/go-bindata"
|
||||
]
|
||||
revision = "035c07716cd373d88456ec4d701402df52584cb4"
|
||||
version = "v3.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
|
@ -559,12 +637,30 @@
|
|||
packages = ["."]
|
||||
revision = "96aac992fc8b1a4c83841a6c3e7178d20d989625"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluaflag"
|
||||
packages = ["."]
|
||||
revision = "078088de689148194436293886e8e39809167332"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluash"
|
||||
packages = ["."]
|
||||
revision = "e145c563986f0b91f740a758a84bca46c163aec7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/sftp"
|
||||
packages = ["."]
|
||||
revision = "f6a9258a0f570c3a76681b897b6ded57cb0dfa88"
|
||||
version = "1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/posener/complete"
|
||||
packages = [
|
||||
|
@ -639,6 +735,35 @@
|
|||
revision = "ebec7ef2574b42a7088cd7751176483e0a27d458"
|
||||
version = "v1.0.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yookoala/realpath"
|
||||
packages = ["."]
|
||||
revision = "d19ef9c409d9817c1e685775e53d361b03eabbc8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluamapper"
|
||||
packages = ["."]
|
||||
revision = "d836955830e75240d46ce9f0e6d148d94f2e1d3a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluare"
|
||||
packages = ["."]
|
||||
revision = "d7c94f1a80ede93a621ed100866e6d4745ca8c22"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gopher-lua"
|
||||
packages = [
|
||||
".",
|
||||
"ast",
|
||||
"parse",
|
||||
"pm"
|
||||
]
|
||||
revision = "7d7bc8747e3f614c5c587729a341fe7d8903cdb8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/zclconf/go-cty"
|
||||
|
@ -669,7 +794,10 @@
|
|||
"blowfish",
|
||||
"cast5",
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"hkdf",
|
||||
"internal/chacha20",
|
||||
"nacl/secretbox",
|
||||
"openpgp",
|
||||
"openpgp/armor",
|
||||
|
@ -681,6 +809,9 @@
|
|||
"poly1305",
|
||||
"salsa20",
|
||||
"salsa20/salsa",
|
||||
"ssh",
|
||||
"ssh/agent",
|
||||
"ssh/terminal",
|
||||
"tea",
|
||||
"twofish",
|
||||
"xtea"
|
||||
|
@ -710,7 +841,10 @@
|
|||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "2c42eef0765b9837fbdab12011af7830f55f88f0"
|
||||
|
||||
[[projects]]
|
||||
|
@ -777,9 +911,33 @@
|
|||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/xmlpath.v2"
|
||||
packages = ["."]
|
||||
revision = "860cbeca3ebcc600db0b213c0e83ad6ce91f5739"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-json"
|
||||
packages = ["."]
|
||||
revision = "1aab82196e3b418b56866938f28b6a693f2c6b18"
|
||||
|
||||
[[projects]]
|
||||
name = "layeh.com/gopher-luar"
|
||||
packages = ["."]
|
||||
revision = "7b2b96926970546e504881ee7364c1127508eb4e"
|
||||
version = "v1.0.2"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "8cd1d7d7f5e846cb0b8d6bf78e6a7ecd32c6e24dd18c886ac99eaeb1b3109aff"
|
||||
inputs-digest = "e0d2f0d7da737ab5517f0ff1c22588a2f835a8f37ef6b67e491ffd6e1e0defc3"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Xe/eclier"
|
||||
"github.com/Xe/x/tools/glue/libs/gluaexpect"
|
||||
"github.com/Xe/x/tools/glue/libs/gluasimplebox"
|
||||
"github.com/ailncode/gluaxmlpath"
|
||||
"github.com/cjoudrey/gluahttp"
|
||||
"github.com/cjoudrey/gluaurl"
|
||||
"github.com/kohkimakimoto/gluaenv"
|
||||
"github.com/kohkimakimoto/gluafs"
|
||||
"github.com/kohkimakimoto/gluaquestion"
|
||||
"github.com/kohkimakimoto/gluassh"
|
||||
"github.com/kohkimakimoto/gluatemplate"
|
||||
"github.com/kohkimakimoto/gluayaml"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/otm/gluaflag"
|
||||
"github.com/otm/gluash"
|
||||
"github.com/yuin/gluare"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
json "layeh.com/gopher-json"
|
||||
)
|
||||
|
||||
var hDir string
|
||||
var cfgHome *string
|
||||
var netrcFile *string
|
||||
|
||||
func init() {
|
||||
dir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hDir = dir
|
||||
|
||||
cfgHome = flag.String("home", filepath.Join(hDir, ".construct"), "construct's home directory")
|
||||
netrcFile = flag.String("netrc", filepath.Join(hDir, ".netrc"), "location of netrc file to use for authentication")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
pluginLoc := filepath.Join(*cfgHome, "plugins")
|
||||
scriptsLoc := filepath.Join(*cfgHome, "local", "scripts")
|
||||
|
||||
os.MkdirAll(pluginLoc, 0755)
|
||||
os.MkdirAll(scriptsLoc, 0755)
|
||||
fout, err := os.Create(*netrcFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fout.Close()
|
||||
|
||||
opts := []eclier.RouterOption{
|
||||
eclier.WithGluaCreationHook(preload),
|
||||
eclier.WithScriptHome(scriptsLoc),
|
||||
}
|
||||
|
||||
err = filepath.Walk(pluginLoc, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
opts = append(opts, eclier.WithScriptHome(info.Name()))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := eclier.NewRouter(opts...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
r.Run(ctx, flag.Args())
|
||||
}
|
||||
|
||||
func preload(L *lua.LState) {
|
||||
L.PreloadModule("re", gluare.Loader)
|
||||
L.PreloadModule("sh", gluash.Loader)
|
||||
L.PreloadModule("fs", gluafs.Loader)
|
||||
L.PreloadModule("env", gluaenv.Loader)
|
||||
L.PreloadModule("yaml", gluayaml.Loader)
|
||||
L.PreloadModule("question", gluaquestion.Loader)
|
||||
L.PreloadModule("ssh", gluassh.Loader)
|
||||
L.PreloadModule("http", gluahttp.NewHttpModule(&http.Client{}).Loader)
|
||||
L.PreloadModule("flag", gluaflag.Loader)
|
||||
L.PreloadModule("template", gluatemplate.Loader)
|
||||
L.PreloadModule("url", gluaurl.Loader)
|
||||
gluaexpect.Preload(L)
|
||||
gluasimplebox.Preload(L)
|
||||
gluaxmlpath.Preload(L)
|
||||
json.Preload(L)
|
||||
}
|
54
mage.go
54
mage.go
|
@ -14,11 +14,8 @@ import (
|
|||
"github.com/magefile/mage/mg"
|
||||
)
|
||||
|
||||
var (
|
||||
wd string
|
||||
arches []string
|
||||
bins []string
|
||||
)
|
||||
var wd string
|
||||
var arches []string
|
||||
|
||||
func init() {
|
||||
lwd, err := os.Getwd()
|
||||
|
@ -26,15 +23,7 @@ func init() {
|
|||
|
||||
wd = lwd
|
||||
|
||||
arches = []string{"amd64", "ppc64le", "arm64"}
|
||||
bins = []string{
|
||||
"mage",
|
||||
"route-httpagent",
|
||||
"route-cli",
|
||||
"routed",
|
||||
"route-runmigrations",
|
||||
"terraform-provider-route",
|
||||
}
|
||||
arches = []string{"amd64", "ppc64", "386", "arm", "arm64"}
|
||||
}
|
||||
|
||||
const pkgBase = "git.xeserv.us/xena/route/"
|
||||
|
@ -47,7 +36,7 @@ func buildBins(goos, goarch string) {
|
|||
|
||||
os.MkdirAll(filepath.Join(d, goos, goarch), 0777)
|
||||
|
||||
for _, pkg := range bins {
|
||||
for _, pkg := range []string{"route-httpagent", "route-cli", "routed", "terraform-provider-route"} {
|
||||
env := []string{"GOOS=" + goos, "GOARCH=" + goarch}
|
||||
goBuild(ctx, env, filepath.Join(d, goos, goarch), "cmd/"+pkg)
|
||||
goInstall(ctx, env, "cmd/"+pkg)
|
||||
|
@ -73,12 +62,9 @@ func Docker() {
|
|||
ver, err := gitTag()
|
||||
qod.ANE(err)
|
||||
|
||||
shouldWork(ctx, nil, wd, "docker", "build", "-t", "xena/route-core", ".")
|
||||
|
||||
run := filepath.Join(wd, "run")
|
||||
|
||||
shouldWork(ctx, nil, run, "docker", "build", "-t", "xena/routed:"+ver, "-f", "Dockerfile.routed", ".")
|
||||
shouldWork(ctx, nil, run, "docker", "build", "-t", "xena/route-httpagent:"+ver, "-f", "Dockerfile.agent", ".")
|
||||
shouldWork(ctx, nil, wd, "docker", "build", "-t", "xena/route-core", "-f", "Dockerfile.core", ".")
|
||||
shouldWork(ctx, nil, wd, "docker", "build", "-t", "xena/routed:"+ver, "-f", "Dockerfile.routed", ".")
|
||||
shouldWork(ctx, nil, wd, "docker", "build", "-t", "xena/route-httpagent:"+ver, "-f", "Dockerfile.agent", ".")
|
||||
}
|
||||
|
||||
// Linux builds binaries for linux
|
||||
|
@ -102,6 +88,10 @@ func Darwin() {
|
|||
// Build builds the binaries for route and routed.
|
||||
func Build() {
|
||||
buildBins(runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
Plugin()
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin builds all of the plugins for programs wanting to augment themselves with route.
|
||||
|
@ -154,6 +144,13 @@ func Package() {
|
|||
}
|
||||
}
|
||||
|
||||
// Version is the version as git reports.
|
||||
func Version() {
|
||||
ver, err := gitTag()
|
||||
qod.ANE(err)
|
||||
qod.Printlnf("route-%s", ver)
|
||||
}
|
||||
|
||||
// Test runs all of the functional and unit tests for the project.
|
||||
func Test() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
@ -168,7 +165,7 @@ func Tools(ctx context.Context) {
|
|||
"github.com/golang/dep/cmd/dep",
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/twitchtv/twirp/protoc-gen-twirp",
|
||||
"github.com/jteeuwen/go-bindata/go-bindata",
|
||||
"github.com/Xe/twirp-codegens/cmd/protoc-gen-twirp_eclier",
|
||||
}
|
||||
|
||||
for _, t := range tools {
|
||||
|
@ -178,26 +175,17 @@ func Tools(ctx context.Context) {
|
|||
|
||||
// Generate runs code generators and the like.
|
||||
func Generate(ctx context.Context) {
|
||||
protoDir := filepath.Join(wd, "proto")
|
||||
databaseDir := filepath.Join(wd, "internal", "database", "migrations")
|
||||
|
||||
os.Mkdir(filepath.Join(databaseDir, "dmigrations"), 0777)
|
||||
dir := filepath.Join(wd, "proto")
|
||||
|
||||
Tools(ctx)
|
||||
|
||||
shouldWork(ctx, nil, protoDir, "sh", "./regen.sh")
|
||||
shouldWork(ctx, nil, databaseDir, "go-bindata", "-pkg", "dmigrations", "-o", "../dmigrations/bindata.go", ".")
|
||||
shouldWork(ctx, nil, dir, "sh", "./regen.sh")
|
||||
}
|
||||
|
||||
// Vars shows the various variables that this magefile uses.
|
||||
func Vars() {
|
||||
ver, err := gitTag()
|
||||
qod.ANE(err)
|
||||
|
||||
qod.Printlnf("arches:\t%v", arches)
|
||||
qod.Printlnf("bins:\t%v", bins)
|
||||
qod.Printlnf("goarch:\t%s", runtime.GOARCH)
|
||||
qod.Printlnf("goos:\t%s", runtime.GOOS)
|
||||
qod.Printlnf("wd:\t%s", wd)
|
||||
qod.Printlnf("ver:\t%s", ver)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "backends:kill"
|
||||
script.help = "Executes method kill on service backends for twirp package xeserv.us.route.backends"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for BackendId
|
||||
fs:string("id", "", "value for message arg id")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.backends.kill(flags)
|
||||
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "backends:list"
|
||||
script.help = "Executes method list on service backends for twirp package xeserv.us.route.backends"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for BackendSelector
|
||||
fs:string("domain", "", "value for message arg domain")
|
||||
fs:string("user", "", "value for message arg user")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.backends.list(flags)
|
||||
|
||||
print("bs:\t\t" .. tostring(resp.bs))
|
||||
print("backends:\t\t" .. tostring(resp.backends))
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "routes:delete"
|
||||
script.help = "Executes method delete on service routes for twirp package xeserv.us.route.routes"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Route
|
||||
fs:string("id", "", "value for message arg id")
|
||||
fs:string("creator", "", "value for message arg creator")
|
||||
fs:string("host", "", "value for message arg host")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.routes.delete(flags)
|
||||
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "routes:get"
|
||||
script.help = "Executes method get on service routes for twirp package xeserv.us.route.routes"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for GetRouteRequest
|
||||
fs:string("unused", "", "value for message arg unused")
|
||||
fs:string("id", "", "value for message arg id")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.routes.get(flags)
|
||||
|
||||
print("id:\t\t" .. tostring(resp.id))
|
||||
print("creator:\t\t" .. tostring(resp.creator))
|
||||
print("host:\t\t" .. tostring(resp.host))
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "routes:get_all"
|
||||
script.help = "Executes method get_all on service routes for twirp package xeserv.us.route.routes"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Nil
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.routes.get_all(flags)
|
||||
|
||||
print("routes:\t\t" .. tostring(resp.routes))
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "routes:put"
|
||||
script.help = "Executes method put on service routes for twirp package xeserv.us.route.routes"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Route
|
||||
fs:string("id", "", "value for message arg id")
|
||||
fs:string("creator", "", "value for message arg creator")
|
||||
fs:string("host", "", "value for message arg host")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.routes.put(flags)
|
||||
|
||||
print("id:\t\t" .. tostring(resp.id))
|
||||
print("creator:\t\t" .. tostring(resp.creator))
|
||||
print("host:\t\t" .. tostring(resp.host))
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "tokens:deactivate"
|
||||
script.help = "Executes method deactivate on service tokens for twirp package xeserv.us.route.tokens"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Token
|
||||
fs:string("id", "", "value for message arg id")
|
||||
fs:string("body", "", "value for message arg body")
|
||||
fs:strings("scopes", "", "value for message arg scopes")
|
||||
fs:bool("active", "", "value for message arg active")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.tokens.deactivate(flags)
|
||||
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "tokens:delete"
|
||||
script.help = "Executes method delete on service tokens for twirp package xeserv.us.route.tokens"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Token
|
||||
fs:string("id", "", "value for message arg id")
|
||||
fs:string("body", "", "value for message arg body")
|
||||
fs:strings("scopes", "", "value for message arg scopes")
|
||||
fs:bool("active", "", "value for message arg active")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.tokens.delete(flags)
|
||||
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "tokens:get"
|
||||
script.help = "Executes method get on service tokens for twirp package xeserv.us.route.tokens"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for GetTokenRequest
|
||||
fs:string("token", "", "value for message arg token")
|
||||
fs:string("id", "", "value for message arg id")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.tokens.get(flags)
|
||||
|
||||
print("id:\t\t" .. tostring(resp.id))
|
||||
print("body:\t\t" .. tostring(resp.body))
|
||||
print("scopes:\t\t" .. tostring(resp.scopes))
|
||||
print("active:\t\t" .. tostring(resp.active))
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "tokens:get_all"
|
||||
script.help = "Executes method get_all on service tokens for twirp package xeserv.us.route.tokens"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Nil
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.tokens.get_all(flags)
|
||||
|
||||
print("tokens:\t\t" .. tostring(resp.tokens))
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
-- Code generated by protoc-gen-twirp_eclier v5.0.0DO NOT EDIT
|
||||
-- source: route.proto
|
||||
|
||||
script.verb = "tokens:put"
|
||||
script.help = "Executes method put on service tokens for twirp package xeserv.us.route.tokens"
|
||||
script.author = "machine-generated"
|
||||
script.version = "v5.0.0"
|
||||
script.usage = ""
|
||||
|
||||
local flag = require "flag"
|
||||
local svc = require "svc"
|
||||
|
||||
local fs = flag.new()
|
||||
|
||||
-- flags for Token
|
||||
fs:string("id", "", "value for message arg id")
|
||||
fs:string("body", "", "value for message arg body")
|
||||
fs:strings("scopes", "", "value for message arg scopes")
|
||||
fs:bool("active", "", "value for message arg active")
|
||||
|
||||
script.usage = fs:usage()
|
||||
|
||||
function run(arg)
|
||||
if arg[1] == "-help" or arg[1] == "--help" then
|
||||
print(fs:usage())
|
||||
return
|
||||
end
|
||||
|
||||
arg[0] = script.verb
|
||||
local flags = fs:parse(arg)
|
||||
|
||||
local resp = svc.tokens.put(flags)
|
||||
|
||||
print("id:\t\t" .. tostring(resp.id))
|
||||
print("body:\t\t" .. tostring(resp.body))
|
||||
print("scopes:\t\t" .. tostring(resp.scopes))
|
||||
print("active:\t\t" .. tostring(resp.active))
|
||||
end
|
|
@ -3,4 +3,5 @@
|
|||
protoc -I. \
|
||||
--go_out=:. \
|
||||
--twirp_out=. \
|
||||
--twirp_eclier_out=./eclier \
|
||||
route.proto
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Copyright (C) 2014 Thomas Rooney
|
||||
|
||||
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.
|
|
@ -0,0 +1,64 @@
|
|||
# Gexpect
|
||||
|
||||
Gexpect is a pure golang expect-like module.
|
||||
|
||||
It makes it simpler to create and control other terminal applications.
|
||||
|
||||
child, err := gexpect.Spawn("python")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
child.Expect(">>>")
|
||||
child.SendLine("print 'Hello World'")
|
||||
child.Interact()
|
||||
child.Close()
|
||||
|
||||
## Examples
|
||||
|
||||
`Spawn` handles the argument parsing from a string
|
||||
|
||||
child.Spawn("/bin/sh -c 'echo \"my complicated command\" | tee log | cat > log2'")
|
||||
child.ReadLine() // ReadLine() (string, error)
|
||||
child.ReadUntil(' ') // ReadUntil(delim byte) ([]byte, error)
|
||||
|
||||
`ReadLine`, `ReadUntil` and `SendLine` send strings from/to `stdout/stdin` respectively
|
||||
|
||||
child, _ := gexpect.Spawn("cat")
|
||||
child.SendLine("echoing process_stdin") // SendLine(command string) (error)
|
||||
msg, _ := child.ReadLine() // msg = echoing process_stdin
|
||||
|
||||
`Wait` and `Close` allow for graceful and ungraceful termination.
|
||||
|
||||
child.Wait() // Waits until the child terminates naturally.
|
||||
child.Close() // Sends a kill command
|
||||
|
||||
`AsyncInteractChannels` spawns two go routines to pipe into and from `stdout`/`stdin`, allowing for some usecases to be a little simpler.
|
||||
|
||||
child, _ := gexpect.Spawn("sh")
|
||||
sender, receiver := child.AsyncInteractChannels()
|
||||
sender <- "echo Hello World\n" // Send to stdin
|
||||
line, open := <- receiver // Recieve a line from stdout/stderr
|
||||
// When the subprocess stops (e.g. with child.Close()) , receiver is closed
|
||||
if open {
|
||||
fmt.Printf("Received %s", line)
|
||||
}
|
||||
|
||||
`ExpectRegex` uses golang's internal regex engine to wait until a match from the process with the given regular expression (or an error on process termination with no match).
|
||||
|
||||
child, _ := gexpect.Spawn("echo accb")
|
||||
match, _ := child.ExpectRegex("a..b")
|
||||
// (match=true)
|
||||
|
||||
`ExpectRegexFind` allows for groups to be extracted from process stdout. The first element is an array of containing the total matched text, followed by each subexpression group match.
|
||||
|
||||
child, _ := gexpect.Spawn("echo 123 456 789")
|
||||
result, _ := child.ExpectRegexFind("\d+ (\d+) (\d+)")
|
||||
// result = []string{"123 456 789", "456", "789"}
|
||||
|
||||
See `gexpect_test.go` and the `examples` folder for full syntax
|
||||
|
||||
## Credits
|
||||
|
||||
github.com/kballard/go-shellquote
|
||||
github.com/kr/pty
|
||||
KMP Algorithm: "http://blog.databigbang.com/searching-for-substrings-in-streams-a-slight-modification-of-the-knuth-morris-pratt-algorithm-in-haxe/"
|
|
@ -0,0 +1,449 @@
|
|||
// +build !windows
|
||||
|
||||
package gexpect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
shell "github.com/kballard/go-shellquote"
|
||||
"github.com/kr/pty"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptySearch = errors.New("empty search string")
|
||||
)
|
||||
|
||||
type ExpectSubprocess struct {
|
||||
Cmd *exec.Cmd
|
||||
buf *buffer
|
||||
outputBuffer []byte
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
f *os.File
|
||||
b bytes.Buffer
|
||||
collect bool
|
||||
|
||||
collection bytes.Buffer
|
||||
}
|
||||
|
||||
func (buf *buffer) StartCollecting() {
|
||||
buf.collect = true
|
||||
}
|
||||
|
||||
func (buf *buffer) StopCollecting() (result string) {
|
||||
result = string(buf.collection.Bytes())
|
||||
buf.collect = false
|
||||
buf.collection.Reset()
|
||||
return result
|
||||
}
|
||||
|
||||
func (buf *buffer) Read(chunk []byte) (int, error) {
|
||||
nread := 0
|
||||
if buf.b.Len() > 0 {
|
||||
n, err := buf.b.Read(chunk)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if n == len(chunk) {
|
||||
return n, nil
|
||||
}
|
||||
nread = n
|
||||
}
|
||||
fn, err := buf.f.Read(chunk[nread:])
|
||||
return fn + nread, err
|
||||
}
|
||||
|
||||
func (buf *buffer) ReadRune() (r rune, size int, err error) {
|
||||
l := buf.b.Len()
|
||||
|
||||
chunk := make([]byte, utf8.UTFMax)
|
||||
if l > 0 {
|
||||
n, err := buf.b.Read(chunk)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if utf8.FullRune(chunk[:n]) {
|
||||
r, rL := utf8.DecodeRune(chunk)
|
||||
if n > rL {
|
||||
buf.PutBack(chunk[rL:n])
|
||||
}
|
||||
if buf.collect {
|
||||
buf.collection.WriteRune(r)
|
||||
}
|
||||
return r, rL, nil
|
||||
}
|
||||
}
|
||||
// else add bytes from the file, then try that
|
||||
for l < utf8.UTFMax {
|
||||
fn, err := buf.f.Read(chunk[l : l+1])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
l = l + fn
|
||||
|
||||
if utf8.FullRune(chunk[:l]) {
|
||||
r, rL := utf8.DecodeRune(chunk)
|
||||
if buf.collect {
|
||||
buf.collection.WriteRune(r)
|
||||
}
|
||||
return r, rL, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, errors.New("File is not a valid UTF=8 encoding")
|
||||
}
|
||||
|
||||
func (buf *buffer) PutBack(chunk []byte) {
|
||||
if len(chunk) == 0 {
|
||||
return
|
||||
}
|
||||
if buf.b.Len() == 0 {
|
||||
buf.b.Write(chunk)
|
||||
return
|
||||
}
|
||||
d := make([]byte, 0, len(chunk)+buf.b.Len())
|
||||
d = append(d, chunk...)
|
||||
d = append(d, buf.b.Bytes()...)
|
||||
buf.b.Reset()
|
||||
buf.b.Write(d)
|
||||
}
|
||||
|
||||
func SpawnAtDirectory(command string, directory string) (*ExpectSubprocess, error) {
|
||||
expect, err := _spawn(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expect.Cmd.Dir = directory
|
||||
return _start(expect)
|
||||
}
|
||||
|
||||
func Command(command string) (*ExpectSubprocess, error) {
|
||||
expect, err := _spawn(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return expect, nil
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Start() error {
|
||||
_, err := _start(expect)
|
||||
return err
|
||||
}
|
||||
|
||||
func Spawn(command string) (*ExpectSubprocess, error) {
|
||||
expect, err := _spawn(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return _start(expect)
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Close() error {
|
||||
if err := expect.Cmd.Process.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expect.buf.f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) AsyncInteractChannels() (send chan string, receive chan string) {
|
||||
receive = make(chan string)
|
||||
send = make(chan string)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
str, err := expect.ReadLine()
|
||||
if err != nil {
|
||||
close(receive)
|
||||
return
|
||||
}
|
||||
receive <- str
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case sendCommand, exists := <-send:
|
||||
{
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
err := expect.Send(sendCommand)
|
||||
if err != nil {
|
||||
receive <- "gexpect Error: " + err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ExpectRegex(regex string) (bool, error) {
|
||||
return regexp.MatchReader(regex, expect.buf)
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) expectRegexFind(regex string, output bool) ([]string, string, error) {
|
||||
re, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
expect.buf.StartCollecting()
|
||||
pairs := re.FindReaderSubmatchIndex(expect.buf)
|
||||
stringIndexedInto := expect.buf.StopCollecting()
|
||||
l := len(pairs)
|
||||
numPairs := l / 2
|
||||
result := make([]string, numPairs)
|
||||
for i := 0; i < numPairs; i += 1 {
|
||||
result[i] = stringIndexedInto[pairs[i*2]:pairs[i*2+1]]
|
||||
}
|
||||
// convert indexes to strings
|
||||
|
||||
if len(result) == 0 {
|
||||
err = fmt.Errorf("ExpectRegex didn't find regex '%v'.", regex)
|
||||
} else {
|
||||
// The number in pairs[1] is an index of a first
|
||||
// character outside the whole match
|
||||
putBackIdx := pairs[1]
|
||||
if len(stringIndexedInto) > putBackIdx {
|
||||
stringToPutBack := stringIndexedInto[putBackIdx:]
|
||||
stringIndexedInto = stringIndexedInto[:putBackIdx]
|
||||
expect.buf.PutBack([]byte(stringToPutBack))
|
||||
}
|
||||
}
|
||||
return result, stringIndexedInto, err
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) expectTimeoutRegexFind(regex string, timeout time.Duration) (result []string, out string, err error) {
|
||||
t := make(chan bool)
|
||||
go func() {
|
||||
result, out, err = expect.ExpectRegexFindWithOutput(regex)
|
||||
t <- false
|
||||
}()
|
||||
go func() {
|
||||
time.Sleep(timeout)
|
||||
err = fmt.Errorf("ExpectRegex timed out after %v finding '%v'.\nOutput:\n%s", timeout, regex, expect.Collect())
|
||||
t <- true
|
||||
}()
|
||||
<-t
|
||||
return result, out, err
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ExpectRegexFind(regex string) ([]string, error) {
|
||||
result, _, err := expect.expectRegexFind(regex, false)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ExpectTimeoutRegexFind(regex string, timeout time.Duration) ([]string, error) {
|
||||
result, _, err := expect.expectTimeoutRegexFind(regex, timeout)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ExpectRegexFindWithOutput(regex string) ([]string, string, error) {
|
||||
return expect.expectRegexFind(regex, true)
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ExpectTimeoutRegexFindWithOutput(regex string, timeout time.Duration) ([]string, string, error) {
|
||||
return expect.expectTimeoutRegexFind(regex, timeout)
|
||||
}
|
||||
|
||||
func buildKMPTable(searchString string) []int {
|
||||
pos := 2
|
||||
cnd := 0
|
||||
length := len(searchString)
|
||||
|
||||
var table []int
|
||||
if length < 2 {
|
||||
length = 2
|
||||
}
|
||||
|
||||
table = make([]int, length)
|
||||
table[0] = -1
|
||||
table[1] = 0
|
||||
|
||||
for pos < len(searchString) {
|
||||
if searchString[pos-1] == searchString[cnd] {
|
||||
cnd += 1
|
||||
table[pos] = cnd
|
||||
pos += 1
|
||||
} else if cnd > 0 {
|
||||
cnd = table[cnd]
|
||||
} else {
|
||||
table[pos] = 0
|
||||
pos += 1
|
||||
}
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ExpectTimeout(searchString string, timeout time.Duration) (e error) {
|
||||
result := make(chan error)
|
||||
go func() {
|
||||
result <- expect.Expect(searchString)
|
||||
}()
|
||||
select {
|
||||
case e = <-result:
|
||||
case <-time.After(timeout):
|
||||
e = fmt.Errorf("Expect timed out after %v waiting for '%v'.\nOutput:\n%s", timeout, searchString, expect.Collect())
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Expect(searchString string) (e error) {
|
||||
target := len(searchString)
|
||||
if target < 1 {
|
||||
return ErrEmptySearch
|
||||
}
|
||||
chunk := make([]byte, target*2)
|
||||
if expect.outputBuffer != nil {
|
||||
expect.outputBuffer = expect.outputBuffer[0:]
|
||||
}
|
||||
m := 0
|
||||
i := 0
|
||||
// Build KMP Table
|
||||
table := buildKMPTable(searchString)
|
||||
|
||||
for {
|
||||
n, err := expect.buf.Read(chunk)
|
||||
if n == 0 && err != nil {
|
||||
return err
|
||||
}
|
||||
if expect.outputBuffer != nil {
|
||||
expect.outputBuffer = append(expect.outputBuffer, chunk[:n]...)
|
||||
}
|
||||
offset := m + i
|
||||
for m+i-offset < n {
|
||||
if searchString[i] == chunk[m+i-offset] {
|
||||
i += 1
|
||||
if i == target {
|
||||
unreadIndex := m + i - offset
|
||||
if len(chunk) > unreadIndex {
|
||||
expect.buf.PutBack(chunk[unreadIndex:n])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
m += i - table[i]
|
||||
if table[i] > -1 {
|
||||
i = table[i]
|
||||
} else {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Send(command string) error {
|
||||
_, err := io.WriteString(expect.buf.f, command)
|
||||
return err
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Capture() {
|
||||
if expect.outputBuffer == nil {
|
||||
expect.outputBuffer = make([]byte, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Collect() []byte {
|
||||
collectOutput := make([]byte, len(expect.outputBuffer))
|
||||
copy(collectOutput, expect.outputBuffer)
|
||||
expect.outputBuffer = nil
|
||||
return collectOutput
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) SendLine(command string) error {
|
||||
_, err := io.WriteString(expect.buf.f, command+"\r\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Interact() {
|
||||
defer expect.Cmd.Wait()
|
||||
io.Copy(os.Stdout, &expect.buf.b)
|
||||
go io.Copy(os.Stdout, expect.buf.f)
|
||||
go io.Copy(expect.buf.f, os.Stdin)
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ReadUntil(delim byte) ([]byte, error) {
|
||||
join := make([]byte, 0, 512)
|
||||
chunk := make([]byte, 255)
|
||||
|
||||
for {
|
||||
n, err := expect.buf.Read(chunk)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if chunk[i] == delim {
|
||||
if len(chunk) > i+1 {
|
||||
expect.buf.PutBack(chunk[i+1:n])
|
||||
}
|
||||
return join, nil
|
||||
} else {
|
||||
join = append(join, chunk[i])
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return join, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) Wait() error {
|
||||
return expect.Cmd.Wait()
|
||||
}
|
||||
|
||||
func (expect *ExpectSubprocess) ReadLine() (string, error) {
|
||||
str, err := expect.ReadUntil('\n')
|
||||
return string(str), err
|
||||
}
|
||||
|
||||
func _start(expect *ExpectSubprocess) (*ExpectSubprocess, error) {
|
||||
f, err := pty.Start(expect.Cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expect.buf.f = f
|
||||
|
||||
return expect, nil
|
||||
}
|
||||
|
||||
func _spawn(command string) (*ExpectSubprocess, error) {
|
||||
wrapper := new(ExpectSubprocess)
|
||||
|
||||
wrapper.outputBuffer = nil
|
||||
|
||||
splitArgs, err := shell.Split(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numArguments := len(splitArgs) - 1
|
||||
if numArguments < 0 {
|
||||
return nil, errors.New("gexpect: No command given to spawn")
|
||||
}
|
||||
path, err := exec.LookPath(splitArgs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if numArguments >= 1 {
|
||||
wrapper.Cmd = exec.Command(path, splitArgs[1:]...)
|
||||
} else {
|
||||
wrapper.Cmd = exec.Command(path)
|
||||
}
|
||||
wrapper.buf = new(buffer)
|
||||
|
||||
return wrapper, nil
|
||||
}
|
|
@ -0,0 +1,419 @@
|
|||
// +build !windows
|
||||
|
||||
package gexpect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEmptySearchString(t *testing.T) {
|
||||
t.Logf("Testing empty search string...")
|
||||
child, err := Spawn("echo Hello World")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Expect("")
|
||||
if err != ErrEmptySearch {
|
||||
t.Fatalf("Expected empty search error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelloWorld(t *testing.T) {
|
||||
t.Logf("Testing Hello World... ")
|
||||
child, err := Spawn("echo \"Hello World\"")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Expect("Hello World")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoubleHelloWorld(t *testing.T) {
|
||||
t.Logf("Testing Double Hello World... ")
|
||||
child, err := Spawn(`sh -c "echo Hello World ; echo Hello ; echo Hi"`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Expect("Hello World")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Expect("Hello")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Expect("Hi")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelloWorldFailureCase(t *testing.T) {
|
||||
t.Logf("Testing Hello World Failure case... ")
|
||||
child, err := Spawn("echo \"Hello World\"")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Expect("YOU WILL NEVER FIND ME")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Fatal("Expected an error for TestHelloWorldFailureCase")
|
||||
}
|
||||
|
||||
func TestBiChannel(t *testing.T) {
|
||||
|
||||
t.Logf("Testing BiChannel screen... ")
|
||||
child, err := Spawn("cat")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sender, receiver := child.AsyncInteractChannels()
|
||||
wait := func(str string) {
|
||||
for {
|
||||
msg, open := <-receiver
|
||||
if !open {
|
||||
return
|
||||
}
|
||||
if strings.Contains(msg, str) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endlChar := fmt.Sprintln("")
|
||||
sender <- fmt.Sprintf("echo%v", endlChar)
|
||||
wait("echo")
|
||||
sender <- fmt.Sprintf("echo2%v", endlChar)
|
||||
wait("echo2")
|
||||
child.Close()
|
||||
child.Wait()
|
||||
}
|
||||
|
||||
func TestCommandStart(t *testing.T) {
|
||||
t.Logf("Testing Command... ")
|
||||
|
||||
// Doing this allows you to modify the cmd struct prior to execution, for example to add environment variables
|
||||
child, err := Command("echo 'Hello World'")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
child.Start()
|
||||
child.Expect("Hello World")
|
||||
}
|
||||
|
||||
var regexMatchTests = []struct {
|
||||
re string
|
||||
good string
|
||||
bad string
|
||||
}{
|
||||
{`a`, `a`, `b`},
|
||||
{`.b`, `ab`, `ac`},
|
||||
{`a+hello`, `aaaahello`, `bhello`},
|
||||
{`(hello|world)`, `hello`, `unknown`},
|
||||
{`(hello|world)`, `world`, `unknown`},
|
||||
{"\u00a9", "\u00a9", `unknown`}, // 2 bytes long unicode character "copyright sign"
|
||||
}
|
||||
|
||||
func TestRegexMatch(t *testing.T) {
|
||||
t.Logf("Testing Regular Expression Matching... ")
|
||||
for _, tt := range regexMatchTests {
|
||||
runTest := func(input string) bool {
|
||||
var match bool
|
||||
child, err := Spawn("echo \"" + input + "\"")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
match, err = child.ExpectRegex(tt.re)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return match
|
||||
}
|
||||
if !runTest(tt.good) {
|
||||
t.Errorf("Regex Not matching [%#q] with pattern [%#q]", tt.good, tt.re)
|
||||
}
|
||||
if runTest(tt.bad) {
|
||||
t.Errorf("Regex Matching [%#q] with pattern [%#q]", tt.bad, tt.re)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var regexFindTests = []struct {
|
||||
re string
|
||||
input string
|
||||
matches []string
|
||||
}{
|
||||
{`he(l)lo wo(r)ld`, `hello world`, []string{"hello world", "l", "r"}},
|
||||
{`(a)`, `a`, []string{"a", "a"}},
|
||||
{`so.. (hello|world)`, `so.. hello`, []string{"so.. hello", "hello"}},
|
||||
{`(a+)hello`, `aaaahello`, []string{"aaaahello", "aaaa"}},
|
||||
{`\d+ (\d+) (\d+)`, `123 456 789`, []string{"123 456 789", "456", "789"}},
|
||||
{`\d+ (\d+) (\d+)`, "\u00a9 123 456 789 \u00a9", []string{"123 456 789", "456", "789"}}, // check unicode characters
|
||||
}
|
||||
|
||||
func TestRegexFind(t *testing.T) {
|
||||
t.Logf("Testing Regular Expression Search... ")
|
||||
for _, tt := range regexFindTests {
|
||||
runTest := func(input string) []string {
|
||||
child, err := Spawn("echo \"" + input + "\"")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
matches, err := child.ExpectRegexFind(tt.re)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
matches := runTest(tt.input)
|
||||
if len(matches) != len(tt.matches) {
|
||||
t.Fatalf("Regex not producing the expected number of patterns.. got[%d] ([%s]) expected[%d] ([%s])",
|
||||
len(matches), strings.Join(matches, ","),
|
||||
len(tt.matches), strings.Join(tt.matches, ","))
|
||||
}
|
||||
for i, _ := range matches {
|
||||
if matches[i] != tt.matches[i] {
|
||||
t.Errorf("Regex Expected group [%s] and got group [%s] with pattern [%#q] and input [%s]",
|
||||
tt.matches[i], matches[i], tt.re, tt.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLine(t *testing.T) {
|
||||
t.Logf("Testing ReadLine...")
|
||||
|
||||
child, err := Spawn("echo \"foo\nbar\"")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := child.ReadLine()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s != "foo\r" {
|
||||
t.Fatalf("expected 'foo\\r', got '%s'", s)
|
||||
}
|
||||
s, err = child.ReadLine()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s != "bar\r" {
|
||||
t.Fatalf("expected 'bar\\r', got '%s'", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexWithOutput(t *testing.T) {
|
||||
t.Logf("Testing Regular Expression search with output...")
|
||||
|
||||
s := "You will not find me"
|
||||
p, err := Spawn("echo -n " + s)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot exec rkt: %v", err)
|
||||
}
|
||||
searchPattern := `I should not find you`
|
||||
result, out, err := p.ExpectRegexFindWithOutput(searchPattern)
|
||||
if err == nil {
|
||||
t.Fatalf("Shouldn't have found `%v` in `%v`", searchPattern, out)
|
||||
}
|
||||
if s != out {
|
||||
t.Fatalf("Child output didn't match: %s", out)
|
||||
}
|
||||
|
||||
err = p.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("Child didn't terminate correctly: %v", err)
|
||||
}
|
||||
|
||||
p, err = Spawn("echo You will find me")
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot exec rkt: %v", err)
|
||||
}
|
||||
searchPattern = `.*(You will).*`
|
||||
result, out, err = p.ExpectRegexFindWithOutput(searchPattern)
|
||||
if err != nil || result[1] != "You will" {
|
||||
t.Fatalf("Did not find pattern `%v` in `%v'\n", searchPattern, out)
|
||||
}
|
||||
err = p.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("Child didn't terminate correctly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexTimeoutWithOutput(t *testing.T) {
|
||||
t.Logf("Testing Regular Expression search with timeout and output...")
|
||||
|
||||
seconds := 2
|
||||
timeout := time.Duration(seconds-1) * time.Second
|
||||
|
||||
p, err := Spawn(fmt.Sprintf("sh -c 'sleep %d && echo You find me'", seconds))
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot exec rkt: %v", err)
|
||||
}
|
||||
searchPattern := `find me`
|
||||
result, out, err := p.ExpectTimeoutRegexFindWithOutput(searchPattern, timeout)
|
||||
if err == nil {
|
||||
t.Fatalf("Shouldn't have finished call with result: %v", result)
|
||||
}
|
||||
|
||||
seconds = 2
|
||||
timeout = time.Duration(seconds+1) * time.Second
|
||||
|
||||
p, err = Spawn(fmt.Sprintf("sh -c 'sleep %d && echo You find me'", seconds))
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot exec rkt: %v", err)
|
||||
}
|
||||
searchPattern = `find me`
|
||||
result, out, err = p.ExpectTimeoutRegexFindWithOutput(searchPattern, timeout)
|
||||
if err != nil {
|
||||
t.Fatalf("Didn't find %v in output: %v", searchPattern, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexFindNoExcessBytes(t *testing.T) {
|
||||
t.Logf("Testing Regular Expressions returning output with no excess strings")
|
||||
repeats := 50
|
||||
tests := []struct {
|
||||
desc string
|
||||
loopBody string
|
||||
searchPattern string
|
||||
expectFullTmpl string
|
||||
unmatchedData string
|
||||
}{
|
||||
{
|
||||
desc: `matching lines line by line with $ at the end of the regexp`,
|
||||
loopBody: `echo "prefix: ${i} line"`,
|
||||
searchPattern: `(?m)^prefix:\s+(\d+) line\s??$`,
|
||||
expectFullTmpl: `prefix: %d line`,
|
||||
unmatchedData: "\n",
|
||||
// the "$" char at the end of regexp does not
|
||||
// match the \n, so it is left as an unmatched
|
||||
// data
|
||||
},
|
||||
{
|
||||
desc: `matching lines line by line with \n at the end of the regexp`,
|
||||
loopBody: `echo "prefix: ${i} line"`,
|
||||
searchPattern: `(?m)^prefix:\s+(\d+) line\s??\n`,
|
||||
expectFullTmpl: `prefix: %d line`,
|
||||
unmatchedData: "",
|
||||
},
|
||||
{
|
||||
desc: `matching chunks in single line chunk by chunk`,
|
||||
loopBody: `printf "a ${i} b"`,
|
||||
searchPattern: `a\s+(\d+)\s+b`,
|
||||
expectFullTmpl: `a %d b`,
|
||||
unmatchedData: "",
|
||||
},
|
||||
}
|
||||
seqCmd := fmt.Sprintf("`seq 1 %d`", repeats)
|
||||
shCmdTmpl := fmt.Sprintf(`sh -c 'for i in %s; do %%s; done'`, seqCmd)
|
||||
for _, tt := range tests {
|
||||
t.Logf("Test: %s", tt.desc)
|
||||
shCmd := fmt.Sprintf(shCmdTmpl, tt.loopBody)
|
||||
t.Logf("Running command: %s", shCmd)
|
||||
p, err := Spawn(shCmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot exec shell script: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := p.Wait(); err != nil {
|
||||
t.Fatalf("shell script didn't terminate correctly: %v", err)
|
||||
}
|
||||
}()
|
||||
for i := 1; i <= repeats; i++ {
|
||||
matches, output, err := p.ExpectRegexFindWithOutput(tt.searchPattern)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get the match number %d: %v", i, err)
|
||||
}
|
||||
if len(matches) != 2 {
|
||||
t.Fatalf("Expected only 2 matches, got %d", len(matches))
|
||||
}
|
||||
full := strings.TrimSpace(matches[0])
|
||||
expFull := fmt.Sprintf(tt.expectFullTmpl, i)
|
||||
partial := matches[1]
|
||||
expPartial := fmt.Sprintf("%d", i)
|
||||
if full != expFull {
|
||||
t.Fatalf("Did not the expected full match %q, got %q", expFull, full)
|
||||
}
|
||||
if partial != expPartial {
|
||||
t.Fatalf("Did not the expected partial match %q, got %q", expPartial, partial)
|
||||
}
|
||||
// The output variable usually contains the
|
||||
// unmatched data followed by the whole match.
|
||||
// The first line is special as it has no data
|
||||
// preceding it.
|
||||
var expectedOutput string
|
||||
if i == 1 || tt.unmatchedData == "" {
|
||||
expectedOutput = matches[0]
|
||||
} else {
|
||||
expectedOutput = fmt.Sprintf("%s%s", tt.unmatchedData, matches[0])
|
||||
}
|
||||
if output != expectedOutput {
|
||||
t.Fatalf("The collected output %q should be the same as the whole match %q", output, expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferReadRune(t *testing.T) {
|
||||
tests := []struct {
|
||||
bufferContent []byte
|
||||
fileContent []byte
|
||||
expectedRune rune
|
||||
}{
|
||||
// unicode "copyright char" is \u00a9 is encoded as two bytes in utf8 0xc2 0xa9
|
||||
{[]byte{0xc2, 0xa9}, []byte{}, '\u00a9'}, // whole rune is already in buffer.b
|
||||
{[]byte{0xc2}, []byte{0xa9}, '\u00a9'}, // half of is in the buffer.b and another half still in buffer.f (file)
|
||||
{[]byte{}, []byte{0xc2, 0xa9}, '\u00a9'}, // whole rune is the file
|
||||
// some random noise in the end of file
|
||||
{[]byte{0xc2, 0xa9}, []byte{0x20, 0x20, 0x20, 0x20}, '\u00a9'},
|
||||
{[]byte{0xc2}, []byte{0xa9, 0x20, 0x20, 0x20, 0x20}, '\u00a9'},
|
||||
{[]byte{}, []byte{0xc2, 0xa9, 0x20, 0x20, 0x20, 0x20}, '\u00a9'},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
|
||||
// prepare tmp file with fileContent
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := f.Write(tt.fileContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != len(tt.fileContent) {
|
||||
t.Fatal("expected fileContent written to temp file")
|
||||
}
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// new buffer
|
||||
buf := buffer{f: f, b: *bytes.NewBuffer(tt.bufferContent)}
|
||||
|
||||
// call ReadRune
|
||||
r, size, err := buf.ReadRune()
|
||||
|
||||
if r != tt.expectedRune {
|
||||
t.Fatalf("#%d: expected rune %+q but go is %+q", i, tt.expectedRune, r)
|
||||
}
|
||||
|
||||
if size != len(string(tt.expectedRune)) {
|
||||
t.Fatalf("#%d: expected rune %d bytes long but got just %d bytes long", i, len(string(tt.expectedRune)), size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
.DS_Store
|
||||
doc
|
|
@ -0,0 +1,223 @@
|
|||
memo = "b40e77e679fec09015bfda27b7d1fc37f4ba6240cbe95cf9fb88b5c56e40ebdf"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ThomasRooney/gexpect"
|
||||
packages = ["."]
|
||||
revision = "5482f03509440585d13d8f648989e05903001842"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Xe/x"
|
||||
packages = ["tools/glue/libs/gluaexpect","tools/glue/libs/gluasimplebox"]
|
||||
revision = "d0ebe3970f361daa31a135f1e0c7304eb1442f61"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ailncode/gluaxmlpath"
|
||||
packages = ["."]
|
||||
revision = "6ce478ecb4a60c4fc8929838e0b21b7fb7ca7440"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/brandur/simplebox"
|
||||
packages = ["."]
|
||||
revision = "84e9865bb03ad38c464043bf5382ce8c68ca5f0c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluahttp"
|
||||
packages = ["."]
|
||||
revision = "b4bfe0c50fea948dcbf3966e120996d6607bbd89"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluaurl"
|
||||
packages = ["."]
|
||||
revision = "31cbb9bef199454415879f2e6d609d1136d60cad"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cyberdelia/heroku-go"
|
||||
packages = ["v3"]
|
||||
revision = "bb8b6b1e9656ec0728638961f8e8b4211fee735d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dickeyxxx/netrc"
|
||||
packages = ["."]
|
||||
revision = "3acf1b3de25d89c7688c63bb45f6b07f566555ec"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/google/go-querystring"
|
||||
packages = ["query"]
|
||||
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/howeyc/gopass"
|
||||
packages = ["."]
|
||||
revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
packages = ["."]
|
||||
revision = "d8ec1a69a250a17bb0e419c386eac1f3711dc142"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaenv"
|
||||
packages = ["."]
|
||||
revision = "2888db6bbe38923d59c42e443895875cc8ce0820"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluafs"
|
||||
packages = ["."]
|
||||
revision = "01391ed2d7ab89dc80157605b073403f960aa223"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaquestion"
|
||||
packages = ["."]
|
||||
revision = "311437c29ba54d027ad2af383661725ae2bfdcdc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluassh"
|
||||
packages = ["."]
|
||||
revision = "2a7bd48d7568de8230c87ac1ef4a4c481e45814d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluatemplate"
|
||||
packages = ["."]
|
||||
revision = "d9e2c9d6b00f069a9da377a9ac529c827c1c7d71"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluayaml"
|
||||
packages = ["."]
|
||||
revision = "6fe413d49d73d785510ecf1529991ab0573e96c7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/fs"
|
||||
packages = ["."]
|
||||
revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/pty"
|
||||
packages = ["."]
|
||||
revision = "ce7fa45920dc37a92de8377972e52bc55ffa8d57"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
revision = "737072b4e32b7a5018b4a7125da8d12de90e8045"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
revision = "cc8532a8e9a55ea36402aa21efdf403a60d34096"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
packages = ["."]
|
||||
revision = "44e365d423f4f06769182abfeeae2b91be9d529b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluaflag"
|
||||
packages = ["."]
|
||||
revision = "078088de689148194436293886e8e39809167332"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluash"
|
||||
packages = ["."]
|
||||
revision = "e145c563986f0b91f740a758a84bca46c163aec7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pborman/uuid"
|
||||
packages = ["."]
|
||||
revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pkg/sftp"
|
||||
packages = ["."]
|
||||
revision = "e84cc8c755ca39b7b64f510fe1fffc1b51f210a5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yookoala/realpath"
|
||||
packages = ["."]
|
||||
revision = "c416d99ab5ed256fa30c1f3bab73152deb59bb69"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluamapper"
|
||||
packages = ["."]
|
||||
revision = "d836955830e75240d46ce9f0e6d148d94f2e1d3a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluare"
|
||||
packages = ["."]
|
||||
revision = "8e2742cd1bf2b904720ac66eca3c2091b2ea0720"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gopher-lua"
|
||||
packages = [".","ast","parse","pm"]
|
||||
revision = "33ebc07735566cd0c3c4b69e2839d522cc389852"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["curve25519","ed25519","ed25519/internal/edwards25519","nacl/secretbox","poly1305","salsa20/salsa","ssh","ssh/agent","ssh/terminal"]
|
||||
revision = "dd85ac7e6a88fc6ca420478e934de5f1a42dd3c6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["html","html/atom"]
|
||||
revision = "66aacef3dd8a676686c7ae3716979581e8b03c47"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "9ccfe848b9db8435a24c424abbc07a921adf1df5"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/xmlpath.v2"
|
||||
packages = ["."]
|
||||
revision = "860cbeca3ebcc600db0b213c0e83ad6ce91f5739"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-json"
|
||||
packages = ["."]
|
||||
revision = "c128cc74278be889c4381681712931976fe0d88b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-luar"
|
||||
packages = ["."]
|
||||
revision = "80196fe2abc5682963fc7a5261f5a5d77509938b"
|
|
@ -0,0 +1,72 @@
|
|||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/Xe/x"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/ailncode/gluaxmlpath"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluahttp"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluaurl"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/dickeyxxx/netrc"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaenv"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluafs"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaquestion"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluassh"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluatemplate"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluayaml"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluaflag"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluash"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluare"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gopher-lua"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-json"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-luar"
|
|
@ -0,0 +1,121 @@
|
|||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
|
@ -0,0 +1,38 @@
|
|||
# eclier
|
||||
|
||||
Pronounced like eclair
|
||||
|
||||
The core of a command line application allowing for trivial user extension.
|
||||
|
||||
Every command and subcommand is its own `.lua` file that is either shipped as
|
||||
part of the built-in cartridge of commands or a plugin that the user installs.
|
||||
|
||||
The core contains the following:
|
||||
|
||||
- A module loading system for preparing different commands for use
|
||||
- The core subcommand router
|
||||
|
||||
## How to write plugins
|
||||
|
||||
Create a new file in the script home named after the plugin subcommand, for
|
||||
example: `scripts/hello.lua`:
|
||||
|
||||
```lua
|
||||
script.verb = "hello"
|
||||
script.help = "prints everyone's favorite hello world message"
|
||||
script.author = "Xe" -- put your github username here
|
||||
script.version = "1.0"
|
||||
script.usage = ""
|
||||
|
||||
function(run)
|
||||
print "Hello, world!"
|
||||
end
|
||||
```
|
||||
|
||||
And then run it using the example shell cli:
|
||||
|
||||
```console
|
||||
~/go/src/github.com/Xe/eclier:master λ go run ./example/main.go hello
|
||||
Hello, world!
|
||||
```
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package eclier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Constants for built-in commands.
|
||||
const (
|
||||
BuiltinScriptPath = "<built-in>"
|
||||
BuiltinAuthor = "<built-in>"
|
||||
BuiltinVersion = "<built-in>"
|
||||
)
|
||||
|
||||
type pluginCommand struct {
|
||||
r *Router
|
||||
fs *flag.FlagSet
|
||||
|
||||
dontShowBuiltin *bool
|
||||
}
|
||||
|
||||
// Close is a no-op.
|
||||
func (p *pluginCommand) Close() error { return nil }
|
||||
|
||||
// Init sets up the flags for this command.
|
||||
func (p *pluginCommand) Init() {
|
||||
p.fs = flag.NewFlagSet(p.Verb(), flag.ExitOnError)
|
||||
|
||||
p.dontShowBuiltin = p.fs.Bool("no-builtin", false, "if set, don't show built-in commands")
|
||||
}
|
||||
|
||||
// ScriptPath returns the built-in script path.
|
||||
func (p *pluginCommand) ScriptPath() string { return BuiltinScriptPath }
|
||||
|
||||
// Verb returns the command verb.
|
||||
func (p *pluginCommand) Verb() string { return "plugins" }
|
||||
|
||||
// Help returns the command help
|
||||
func (p *pluginCommand) Help() string {
|
||||
return `plugin lists all of the loaded commands and their script paths.`
|
||||
}
|
||||
|
||||
func (p *pluginCommand) Usage() string {
|
||||
return ` -no-builtin
|
||||
if set, don't show built-in commands`
|
||||
}
|
||||
|
||||
func (p *pluginCommand) Author() string { return BuiltinAuthor }
|
||||
|
||||
func (p *pluginCommand) Version() string { return BuiltinVersion }
|
||||
|
||||
// Run executes the command.
|
||||
func (p *pluginCommand) Run(ctx context.Context, arg []string) error {
|
||||
p.fs.Parse(arg)
|
||||
|
||||
for _, c := range p.r.cmds {
|
||||
if c.ScriptPath() == BuiltinScriptPath && *p.dontShowBuiltin {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("%s\t%s\n", c.Verb(), c.ScriptPath())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBuiltinCommand makes it easier to write core commands for eclier.
|
||||
func NewBuiltinCommand(verb, help, usage string, doer func(context.Context, []string) error) Command {
|
||||
return &commandFunc{
|
||||
verb: verb,
|
||||
help: help,
|
||||
usage: usage,
|
||||
doer: doer,
|
||||
}
|
||||
}
|
||||
|
||||
// commandFunc is a simple alias for creating builtin commands.
|
||||
type commandFunc struct {
|
||||
verb string
|
||||
help string
|
||||
usage string
|
||||
doer func(context.Context, []string) error
|
||||
}
|
||||
|
||||
// Close deallocates resources set up by the initialization of the command.
|
||||
func (c *commandFunc) Close() error { return nil }
|
||||
|
||||
// Init is a no-op.
|
||||
func (c *commandFunc) Init() {}
|
||||
|
||||
// ScriptPath returns the built-in script path.
|
||||
func (c *commandFunc) ScriptPath() string { return BuiltinScriptPath }
|
||||
|
||||
// Verb returns the command verb.
|
||||
func (c *commandFunc) Verb() string { return c.verb }
|
||||
|
||||
// Help returns the command help.
|
||||
func (c *commandFunc) Help() string { return c.help }
|
||||
|
||||
// Usage returns the command usage.
|
||||
func (c *commandFunc) Usage() string { return c.usage }
|
||||
|
||||
// Author returns the built-in author.
|
||||
func (c *commandFunc) Author() string { return BuiltinAuthor }
|
||||
|
||||
// Version returns the built-in version.
|
||||
func (c *commandFunc) Version() string { return BuiltinVersion }
|
||||
|
||||
// Run runs the command handler.
|
||||
func (c *commandFunc) Run(ctx context.Context, arg []string) error {
|
||||
return c.doer(ctx, arg)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package eclier
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Command is an individual subcommand.
|
||||
type Command interface {
|
||||
Close() error
|
||||
Init()
|
||||
ScriptPath() string
|
||||
Verb() string
|
||||
Help() string
|
||||
Usage() string
|
||||
Author() string
|
||||
Version() string
|
||||
Run(ctx context.Context, arg []string) error
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
file = {
|
||||
"./internal/gluanetrc/netrc.lua",
|
||||
"./internal/gluaheroku/heroku.lua",
|
||||
}
|
||||
|
||||
title = "eclier lua libraries"
|
||||
project = "eclier"
|
||||
description = "The lua libraries created for eclier demos and common utility."
|
|
@ -0,0 +1,140 @@
|
|||
package eclier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// Router is the main subcommand router for eclier. At a high level what this is
|
||||
// doing is similar to http.ServeMux, but for CLI commands instead of HTTP handlers.
|
||||
type Router struct {
|
||||
lock sync.Mutex
|
||||
cmds map[string]Command
|
||||
|
||||
// configured data
|
||||
gluaCreationHook func(*lua.LState)
|
||||
scriptHomes []string
|
||||
cartridge map[string]string
|
||||
}
|
||||
|
||||
// NewRouter creates a new instance of Router and sets it up for use.
|
||||
func NewRouter(opts ...RouterOption) (*Router, error) {
|
||||
r := &Router{
|
||||
cmds: map[string]Command{},
|
||||
cartridge: map[string]string{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
// scan r.scriptHome for lua scripts, load them into their own lua states and
|
||||
// make a wrapper around them for the Command type.
|
||||
|
||||
for _, home := range r.scriptHomes {
|
||||
err := filepath.Walk(home, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("error in arg: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(info.Name(), ".lua") {
|
||||
fname := filepath.Join(home, info.Name())
|
||||
fin, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
c := newGluaCommand(r.gluaCreationHook, fname, fin)
|
||||
|
||||
r.cmds[c.Verb()] = c
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var helpCommand Command = NewBuiltinCommand("help", "shows help for subcommands", "[subcommand]", func(ctx context.Context, arg []string) error {
|
||||
if len(arg) == 0 {
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Verb", "Author", "Version", "Help"})
|
||||
|
||||
for _, cmd := range r.cmds {
|
||||
table.Append([]string{cmd.Verb(), cmd.Author(), cmd.Version(), cmd.Help()})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd, ok := r.cmds[arg[0]]
|
||||
if !ok {
|
||||
fmt.Printf("can't find help for %s", arg[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
fmt.Printf("Verb: %s\nAuthor: %s\nVersion: %s\nHelp: %s\nUsage: %s %s\n", cmd.Verb(), cmd.Author(), cmd.Version(), cmd.Help(), cmd.Verb(), cmd.Usage())
|
||||
return nil
|
||||
})
|
||||
|
||||
r.cmds["plugins"] = &pluginCommand{r: r}
|
||||
r.cmds["help"] = helpCommand
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Run executes a single command given in slot 0 of the argument array.
|
||||
func (r *Router) Run(ctx context.Context, arg []string) error {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
if len(arg) == 0 {
|
||||
fmt.Printf("please specify a subcommand, such as `%s help`\n", filepath.Base(os.Args[0]))
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cmd := arg[0]
|
||||
arg = arg[1:]
|
||||
|
||||
ci, ok := r.cmds[cmd]
|
||||
if !ok {
|
||||
fmt.Printf("No such command %s could be run.\n", cmd)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
ci.Init()
|
||||
return ci.Run(ctx, arg)
|
||||
}
|
||||
|
||||
// RouterOption is a functional option for Router.
|
||||
type RouterOption func(*Router)
|
||||
|
||||
// WithScriptHome sets the router's script home to the given directory. This is
|
||||
// where lua files will be walked and parsed.
|
||||
func WithScriptHome(dir string) RouterOption {
|
||||
return func(r *Router) {
|
||||
r.scriptHomes = append(r.scriptHomes, dir)
|
||||
}
|
||||
}
|
||||
|
||||
// WithGluaCreationHook adds a custom bit of code that runs every time a new
|
||||
// gopher-lua LState is created. This allows users of this library to register
|
||||
// custom libraries to the pile of states.
|
||||
func WithGluaCreationHook(hook func(*lua.LState)) RouterOption {
|
||||
return func(r *Router) {
|
||||
r.gluaCreationHook = hook
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package eclier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
type Script struct {
|
||||
Verb string
|
||||
Help string
|
||||
Usage string
|
||||
Author string
|
||||
Version string
|
||||
}
|
||||
|
||||
type gluaCommand struct {
|
||||
sync.Mutex
|
||||
script *Script
|
||||
L *lua.LState
|
||||
|
||||
filename string
|
||||
}
|
||||
|
||||
func newGluaCommand(preload func(*lua.LState), filename string, r io.Reader) Command {
|
||||
L := lua.NewState()
|
||||
preload(L)
|
||||
|
||||
script := &Script{}
|
||||
L.SetGlobal("script", luar.New(L, script))
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = L.DoString(string(data))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &gluaCommand{script: script, L: L, filename: filename}
|
||||
}
|
||||
|
||||
func (g *gluaCommand) Close() error {
|
||||
g.L.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *gluaCommand) Init() {}
|
||||
|
||||
func (g *gluaCommand) ScriptPath() string { return g.filename }
|
||||
|
||||
func (g *gluaCommand) Verb() string { return g.script.Verb }
|
||||
|
||||
func (g *gluaCommand) Help() string { return g.script.Help }
|
||||
|
||||
func (g *gluaCommand) Usage() string { return g.script.Usage }
|
||||
|
||||
func (g *gluaCommand) Author() string { return g.script.Author }
|
||||
|
||||
func (g *gluaCommand) Version() string { return g.script.Version }
|
||||
|
||||
func (g *gluaCommand) Run(ctx context.Context, arg []string) error {
|
||||
runf := g.L.GetGlobal("run")
|
||||
|
||||
if runf.Type() == lua.LTNil {
|
||||
return errors.New("no global function run in this script")
|
||||
}
|
||||
|
||||
tab := g.L.NewTable()
|
||||
|
||||
for _, a := range arg {
|
||||
tab.Append(lua.LString(a))
|
||||
}
|
||||
|
||||
err := g.L.CallByParam(lua.P{
|
||||
Fn: runf,
|
||||
NRet: 0,
|
||||
Protect: false,
|
||||
}, tab)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,661 @@
|
|||
## `json`
|
||||
|
||||
```lua
|
||||
local json = require "json"
|
||||
```
|
||||
|
||||
Json encoder/decoder
|
||||
|
||||
The following functions are exposed by the library:
|
||||
|
||||
decode(string): Decodes a JSON string. Returns nil and an error string if
|
||||
the string could not be decoded.
|
||||
encode(value): Encodes a value into a JSON string. Returns nil and an error
|
||||
string if the value could not be encoded.
|
||||
|
||||
## `xmlpath`
|
||||
|
||||
```lua
|
||||
local xmlpath = require "xmlpath"
|
||||
```
|
||||
|
||||
XMLPath style iteration
|
||||
|
||||
xml ="<bookist><book>x1</book><book>x2</book><book>x3</book></booklist>"
|
||||
local xmlpath = require("xmlpath")
|
||||
node,err = xmlpath.loadxml(xml)
|
||||
path,err = xmlpath.compile("//book")
|
||||
|
||||
it = path:iter(node)
|
||||
for k,v in pairs(it) do
|
||||
print(k,v:string())
|
||||
end
|
||||
|
||||
## `http`
|
||||
|
||||
```lua
|
||||
local http = require("http")
|
||||
```
|
||||
|
||||
HTTP client library
|
||||
|
||||
### API
|
||||
|
||||
- [`http.delete(url [, options])`](#httpdeleteurl--options)
|
||||
- [`http.get(url [, options])`](#httpgeturl--options)
|
||||
- [`http.head(url [, options])`](#httpheadurl--options)
|
||||
- [`http.patch(url [, options])`](#httppatchurl--options)
|
||||
- [`http.post(url [, options])`](#httpposturl--options)
|
||||
- [`http.put(url [, options])`](#httpputurl--options)
|
||||
- [`http.request(method, url [, options])`](#httprequestmethod-url--options)
|
||||
- [`http.request_batch(requests)`](#httprequest_batchrequests)
|
||||
- [`http.response`](#httpresponse)
|
||||
|
||||
#### http.delete(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.get(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.head(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.patch(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.post(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.put(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.request(method, url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| method | String | The HTTP request method |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
#### http.request_batch(requests)
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------- | ----- | ----------- |
|
||||
| requests | Table | A table of requests to send. Each request item is by itself a table containing [http.request](#httprequestmethod-url--options) parameters for the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[[http.response](#httpresponse)] or ([[http.response](#httpresponse)], [error message])
|
||||
|
||||
#### http.response
|
||||
|
||||
The `http.response` table contains information about a completed HTTP request.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------ | ----------- |
|
||||
| body | String | The HTTP response body |
|
||||
| body_size | Number | The size of the HTTP reponse body in bytes |
|
||||
| headers | Table | The HTTP response headers |
|
||||
| cookies | Table | The cookies sent by the server in the HTTP response |
|
||||
| status_code | Number | The HTTP response status code |
|
||||
| url | String | The final URL the request ended pointing to after redirects |
|
||||
|
||||
## `url`
|
||||
|
||||
```lua
|
||||
local url = require "url"
|
||||
```
|
||||
|
||||
URL parsing library
|
||||
|
||||
### API
|
||||
|
||||
- [`url.parse(url)`](#urlparseurl)
|
||||
- [`url.build(options)`](#urlbuildoptions)
|
||||
- [`url.build_query_string(query_params)`](#urlbuild_query_stringquery_params)
|
||||
- [`url.resolve(from, to)`](#urlresolvefrom-to)
|
||||
|
||||
#### url.parse(url)
|
||||
|
||||
Parse URL into a table of key/value components.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL to parsed |
|
||||
|
||||
**Returns**
|
||||
|
||||
Table with parsed URL or (nil, error message)
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------- | ------ | ----------- |
|
||||
| scheme | String | Scheme of the URL |
|
||||
| username | String | Username |
|
||||
| password | String | Password |
|
||||
| host | String | Host and port of the URL |
|
||||
| path | String | Path |
|
||||
| query | String | Query string |
|
||||
| fragment | String | Fragment |
|
||||
|
||||
#### url.build(options)
|
||||
|
||||
Assemble a URL string from a table of URL components.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ----- | ----------- |
|
||||
| options | Table | Table with URL components, see [`url.parse`](#urlparseurl) for list of valid components |
|
||||
|
||||
**Returns**
|
||||
|
||||
String
|
||||
|
||||
#### url.build_query_string(query_params)
|
||||
|
||||
Assemble table of query string parameters into a string.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------ | ----- | ----------- |
|
||||
| query_params | Table | Table with query parameters |
|
||||
|
||||
**Returns**
|
||||
|
||||
String
|
||||
|
||||
#### url.resolve(from, to)
|
||||
|
||||
Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ------ | ----------- |
|
||||
| from | String | base URL |
|
||||
| to | String | href URL |
|
||||
|
||||
**Returns**
|
||||
|
||||
String or (nil, error message)
|
||||
|
||||
## `env`
|
||||
|
||||
```lua
|
||||
local env = require "env"
|
||||
```
|
||||
|
||||
Environment manipulation
|
||||
|
||||
### API
|
||||
|
||||
#### `env.set(key, value)`
|
||||
|
||||
Same `os.setenv`
|
||||
|
||||
#### `env.get(key)`
|
||||
|
||||
Same `os.getenv`
|
||||
|
||||
#### `env.loadfile(file)`
|
||||
|
||||
Loads environment variables from a file. The file is as the following:
|
||||
|
||||
```
|
||||
AAA=BBB
|
||||
CCC=DDD
|
||||
```
|
||||
|
||||
If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
## `fs`
|
||||
|
||||
```lua
|
||||
local fs = require "fs"
|
||||
```
|
||||
|
||||
Filesystem manipulation
|
||||
|
||||
### API
|
||||
|
||||
#### `fs.exists(file)`
|
||||
|
||||
Returns true if the file exists.
|
||||
|
||||
#### `fs.read(file)`
|
||||
|
||||
Reads file content and return it. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.write(file, content, [mode])`
|
||||
|
||||
Writes content to the file. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.mkdir(path, [mode, recursive])`
|
||||
|
||||
Create directory. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.remove(path, [recursive])`
|
||||
|
||||
Remove path. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.symlink(target, link)`
|
||||
|
||||
Create symbolic link. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.dirname(path)`
|
||||
|
||||
Returns all but the last element of path.
|
||||
|
||||
#### `fs.basename(path)`
|
||||
|
||||
Returns the last element of path.
|
||||
|
||||
#### `fs.realpath(path)`
|
||||
|
||||
Returns the real path of a given path in the os. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.getcwd()`
|
||||
|
||||
Returns the current working directory. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.chdir(path)`
|
||||
|
||||
Changes the current working directory. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.file()`
|
||||
|
||||
Returns the script file path. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.dir()`
|
||||
|
||||
Returns the directory path that is parent of the script file. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `fs.glob(pattern, function)`
|
||||
|
||||
Run the callback function with the files matching pattern. See below example:
|
||||
|
||||
```lua
|
||||
local fs = require("fs")
|
||||
local ret, err = fs.glob("/tmp/*", function(file)
|
||||
print(file.path)
|
||||
print(file.realpath)
|
||||
end)
|
||||
```
|
||||
|
||||
## `markdown`
|
||||
|
||||
```lua
|
||||
local markdown = require "markdown"
|
||||
```
|
||||
|
||||
Markdown -> HTML for string and file
|
||||
|
||||
### API
|
||||
|
||||
#### `markdown.dostring(text)`
|
||||
|
||||
Returns HTML string generated from the markdown text.
|
||||
|
||||
#### `markdown.dofile(file)`
|
||||
|
||||
Returns HTML string generated from the markdown text file. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
## `question`
|
||||
|
||||
```lua
|
||||
local question = require "question"
|
||||
```
|
||||
|
||||
Prompt library
|
||||
|
||||
### API
|
||||
|
||||
* `question.ask(text)`
|
||||
* `question.secret(text)`
|
||||
|
||||
## `ssh`
|
||||
|
||||
```lua
|
||||
local ssh = require "ssh"
|
||||
```
|
||||
|
||||
SSH client library
|
||||
|
||||
https://github.com/kohkimakimoto/gluassh/blob/master/gluassh_test.go
|
||||
|
||||
## `template`
|
||||
|
||||
```lua
|
||||
local template = require "template"
|
||||
```
|
||||
|
||||
Go text templates
|
||||
|
||||
### API
|
||||
|
||||
#### `template.dostring(text, table)`
|
||||
|
||||
Returns string generated by text template with the table values. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
#### `template.dofile(file, table)`
|
||||
|
||||
Returns string generated by file template with the table values. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
## `yaml`
|
||||
|
||||
```lua
|
||||
local yaml = require "yaml"
|
||||
```
|
||||
|
||||
Yaml -> table parser
|
||||
|
||||
### API
|
||||
|
||||
#### `yaml.parse(string)`
|
||||
|
||||
Parses yaml formatted string and returns a table. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
## `flag`
|
||||
|
||||
```lua
|
||||
local flag = require "flag"
|
||||
```
|
||||
|
||||
Command line flag parsing.
|
||||
|
||||
See the tests here: https://github.com/otm/gluaflag
|
||||
|
||||
```lua
|
||||
local flag = require "flag"
|
||||
|
||||
fs = flag.new()
|
||||
|
||||
fs:string("name", "foo", "String help string")
|
||||
fs:intArg("title", 1, "Title")
|
||||
fs:numberArg("title", 1, "Title")
|
||||
|
||||
flags = fs:parse(arg) -- arg is the remaining command line arguments
|
||||
assert(flags.title == 2, "expected title to be 2")
|
||||
assert(flags.title == 2.32, "expected title to be 2.32")
|
||||
```
|
||||
|
||||
## `sh`
|
||||
|
||||
```lua
|
||||
local sh = require "sh"
|
||||
```
|
||||
|
||||
gluash is a interface to call any program as it were a function. Programs are executed asynchronously to enable streaming of data in pipes.
|
||||
|
||||
In all discussions bellow the imported module will be referred to as `sh`.
|
||||
|
||||
Commands are called just like functions, executed on the sh module.
|
||||
|
||||
```lua
|
||||
sh.ls("/")
|
||||
```
|
||||
|
||||
For commands that have exotic names, names that are reserved words, or to execute absolute or relative paths call the sh module directly.
|
||||
|
||||
```lua
|
||||
sh("/bin/ls", "/")
|
||||
```
|
||||
|
||||
#### Multiple Arguments
|
||||
Commands with multiple arguments have to be invoked with a separate string for each argument.
|
||||
|
||||
```lua
|
||||
-- this works
|
||||
sh.ls("-la", "/")
|
||||
|
||||
-- this does not work
|
||||
sh.ls("-la /")
|
||||
```
|
||||
|
||||
#### Piping
|
||||
Piping in sh is done almost like piping in the shell. Just call next command as a method on the previous command.
|
||||
|
||||
```lua
|
||||
sh.du("-sb"):sort("-rn"):print()
|
||||
```
|
||||
|
||||
If the command has a exotic name, or a reserved word, call the command through `cmd(path, ...args)`. The first argument in `cmd` is the path.
|
||||
|
||||
```lua
|
||||
sh.du("-sb"):cmd("sort", "-rn"):print()
|
||||
```
|
||||
|
||||
### Waiting for Processes
|
||||
All commands are executed by default in the background, so one have to explicitly wait for a process to finish. There are several ways to wait for the command to finish.
|
||||
|
||||
* `print()` - write stdout and stderr to stdout.
|
||||
* `ok()` - aborts execution if the command's exit code is not zero
|
||||
* `success()` - returns true of the commands exit code is zero
|
||||
* `exitcode()` - returns the exit code of the command
|
||||
|
||||
### Abort by Default
|
||||
It is possible to set the module to abort on errors without checking. It can be practical in some occasions, however performance will be degraded. When global exit code checks are done the commands are run in series, even in pipes, and output is saved in memory buffers.
|
||||
|
||||
To enable global exit code settings call the sh module with an table with the key `abort` set to true.
|
||||
|
||||
```lua
|
||||
sh{abort=true}
|
||||
```
|
||||
|
||||
To read current settings in the module call the module with an empty table.
|
||||
```lua
|
||||
configuration = sh{}
|
||||
print("abort:", configuration.abort)
|
||||
```
|
||||
|
||||
### Analyzing Output
|
||||
There are several options to analyze the output of a command.
|
||||
|
||||
#### lines()
|
||||
An iterator is accessible by calling the method `lines()` on the command.
|
||||
|
||||
```lua
|
||||
for line in sh.cat("/etc/hosts"):lines() do
|
||||
print(line)
|
||||
end
|
||||
```
|
||||
|
||||
#### stdout([filename]), stderr([filename]), combinedOutput([filename])
|
||||
`stdout()`, `stderr()`, and `combinedOutput()` all returns the output of the command as a string. An optional `filename` can be given to the method, in that case the output is also written to the file. The file will be truncated.
|
||||
|
||||
```lua
|
||||
-- print output of command
|
||||
output = sh.echo("hello world"):combinedOutput("/tmp/output")
|
||||
print(output)
|
||||
```
|
||||
|
||||
In the example above will print `hello world` and it will write it to `/tmp/output`
|
||||
|
||||
### Glob Expansion
|
||||
There is no glob expansion done on arguments, however there is a glob functionality in sh.
|
||||
|
||||
```lua
|
||||
sh.ls(sh.glob("*.go"))
|
||||
```
|
||||
|
||||
## `re`
|
||||
|
||||
```lua
|
||||
local re = require "re"
|
||||
```
|
||||
|
||||
Regular Expressions
|
||||
|
||||
### API
|
||||
|
||||
re.find , re.gsub, re.match, re.gmatch are available. These functions have the same API as Lua pattern match.
|
||||
gluare uses the Go regexp package, so you can use regular expressions that are supported in the Go regexp package.
|
||||
|
||||
In addition, the following functions are defined:
|
||||
```
|
||||
gluare.quote(s string) -> string
|
||||
Arguments:
|
||||
|
||||
s string: a string value to escape meta characters
|
||||
|
||||
Returns:
|
||||
|
||||
string: escaped string
|
||||
gluare.quote returns a string that quotes all regular expression metacharacters inside the given text.
|
||||
```
|
||||
|
||||
## `simplebox`
|
||||
|
||||
```lua
|
||||
local simplebox = require "simplebox"
|
||||
```
|
||||
|
||||
Simple encryption
|
||||
|
||||
### API
|
||||
|
||||
#### Create a new instance of simplebox with a newly generated key
|
||||
|
||||
```lua
|
||||
local simplebox = require "simplebox"
|
||||
local key = simplebox.genkey()
|
||||
print("key is: " .. key)
|
||||
local sb = simplebox.new()
|
||||
|
||||
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
FROM busybox
|
||||
|
||||
ADD glue /glue
|
||||
CMD /glue
|
|
@ -0,0 +1,181 @@
|
|||
memo = ""
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ThomasRooney/gexpect"
|
||||
packages = ["."]
|
||||
revision = "5482f03509440585d13d8f648989e05903001842"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ailncode/gluaxmlpath"
|
||||
packages = ["."]
|
||||
revision = "6ce478ecb4a60c4fc8929838e0b21b7fb7ca7440"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/brandur/simplebox"
|
||||
packages = ["."]
|
||||
revision = "84e9865bb03ad38c464043bf5382ce8c68ca5f0c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluahttp"
|
||||
packages = ["."]
|
||||
revision = "b4bfe0c50fea948dcbf3966e120996d6607bbd89"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluaurl"
|
||||
packages = ["."]
|
||||
revision = "31cbb9bef199454415879f2e6d609d1136d60cad"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/howeyc/gopass"
|
||||
packages = ["."]
|
||||
revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
packages = ["."]
|
||||
revision = "d8ec1a69a250a17bb0e419c386eac1f3711dc142"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaenv"
|
||||
packages = ["."]
|
||||
revision = "2888db6bbe38923d59c42e443895875cc8ce0820"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluafs"
|
||||
packages = ["."]
|
||||
revision = "01391ed2d7ab89dc80157605b073403f960aa223"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaquestion"
|
||||
packages = ["."]
|
||||
revision = "311437c29ba54d027ad2af383661725ae2bfdcdc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluassh"
|
||||
packages = ["."]
|
||||
revision = "2a7bd48d7568de8230c87ac1ef4a4c481e45814d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluatemplate"
|
||||
packages = ["."]
|
||||
revision = "d9e2c9d6b00f069a9da377a9ac529c827c1c7d71"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluayaml"
|
||||
packages = ["."]
|
||||
revision = "6fe413d49d73d785510ecf1529991ab0573e96c7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/fs"
|
||||
packages = ["."]
|
||||
revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/pty"
|
||||
packages = ["."]
|
||||
revision = "ce7fa45920dc37a92de8377972e52bc55ffa8d57"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
revision = "cc8532a8e9a55ea36402aa21efdf403a60d34096"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluaflag"
|
||||
packages = ["."]
|
||||
revision = "078088de689148194436293886e8e39809167332"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluash"
|
||||
packages = ["."]
|
||||
revision = "e145c563986f0b91f740a758a84bca46c163aec7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pkg/sftp"
|
||||
packages = ["."]
|
||||
revision = "e84cc8c755ca39b7b64f510fe1fffc1b51f210a5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yookoala/realpath"
|
||||
packages = ["."]
|
||||
revision = "c416d99ab5ed256fa30c1f3bab73152deb59bb69"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluamapper"
|
||||
packages = ["."]
|
||||
revision = "d836955830e75240d46ce9f0e6d148d94f2e1d3a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluare"
|
||||
packages = ["."]
|
||||
revision = "8e2742cd1bf2b904720ac66eca3c2091b2ea0720"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gopher-lua"
|
||||
packages = [".","parse"]
|
||||
revision = "33ebc07735566cd0c3c4b69e2839d522cc389852"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["nacl/secretbox","ssh/terminal","ssh","ssh/agent"]
|
||||
revision = "dd85ac7e6a88fc6ca420478e934de5f1a42dd3c6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["html"]
|
||||
revision = "66aacef3dd8a676686c7ae3716979581e8b03c47"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "9ccfe848b9db8435a24c424abbc07a921adf1df5"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/xmlpath.v2"
|
||||
packages = ["."]
|
||||
revision = "860cbeca3ebcc600db0b213c0e83ad6ce91f5739"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-json"
|
||||
packages = ["."]
|
||||
revision = "c128cc74278be889c4381681712931976fe0d88b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-luar"
|
||||
packages = ["."]
|
||||
revision = "80196fe2abc5682963fc7a5261f5a5d77509938b"
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/ThomasRooney/gexpect"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/ailncode/gluaxmlpath"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/brandur/simplebox"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluahttp"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/cjoudrey/gluaurl"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaenv"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluafs"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluaquestion"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluassh"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluatemplate"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/kohkimakimoto/gluayaml"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluaflag"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/otm/gluash"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gluare"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "github.com/yuin/gopher-lua"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-json"
|
||||
|
||||
[[dependencies]]
|
||||
branch = "master"
|
||||
name = "layeh.com/gopher-luar"
|
|
@ -0,0 +1,18 @@
|
|||
glue
|
||||
====
|
||||
|
||||
Basically gopher-lua's cmd/glua with the following modules imported:
|
||||
- https://godoc.org/layeh.com/gopher-json
|
||||
- https://github.com/ailncode/gluaxmlpath
|
||||
- https://github.com/cjoudrey/gluahttp
|
||||
- https://github.com/cjoudrey/gluaurl
|
||||
- https://github.com/kohkimakimoto/gluaenv
|
||||
- https://github.com/kohkimakimoto/gluafs
|
||||
- https://github.com/kohkimakimoto/gluamarkdown
|
||||
- https://github.com/kohkimakimoto/gluaquestion
|
||||
- https://github.com/kohkimakimoto/gluassh
|
||||
- https://github.com/kohkimakimoto/gluatemplate
|
||||
- https://github.com/kohkimakimoto/gluayaml
|
||||
- https://github.com/otm/gluaflag
|
||||
- https://github.com/otm/gluash
|
||||
- https://github.com/yuin/gluare
|
|
@ -0,0 +1,6 @@
|
|||
from "alpine:edge"
|
||||
|
||||
copy "glue", "/glue"
|
||||
cmd "/glue"
|
||||
flatten
|
||||
tag "xena/glue"
|
|
@ -0,0 +1,20 @@
|
|||
-- expects glue, $ go get -u github.com/Xe/tools/glue
|
||||
local sh = require "sh"
|
||||
sh { abort = true }
|
||||
|
||||
if os.getenv("CGO_ENABLED") ~= "0" then
|
||||
error("CGO_ENABLED must be set to 1")
|
||||
end
|
||||
|
||||
print "building glue..."
|
||||
sh.go("build"):print()
|
||||
sh.upx("--ultra-brute", "glue"):print()
|
||||
sh.box("box.rb"):print()
|
||||
|
||||
print "releasing to docker hub"
|
||||
sh.docker("push", "xena/glue"):print()
|
||||
|
||||
print "moving glue binary to $GOPATH/bin"
|
||||
sh.mv("glue", (os.getenv("GOPATH") .. "/bin/glue"))
|
||||
|
||||
print "build/release complete"
|
Binary file not shown.
|
@ -0,0 +1,213 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/Xe/x/tools/glue/libs/gluaexpect"
|
||||
"github.com/Xe/x/tools/glue/libs/gluasimplebox"
|
||||
"github.com/ailncode/gluaxmlpath"
|
||||
"github.com/cjoudrey/gluahttp"
|
||||
"github.com/cjoudrey/gluaurl"
|
||||
"github.com/kohkimakimoto/gluaenv"
|
||||
"github.com/kohkimakimoto/gluafs"
|
||||
"github.com/kohkimakimoto/gluaquestion"
|
||||
"github.com/kohkimakimoto/gluassh"
|
||||
"github.com/kohkimakimoto/gluatemplate"
|
||||
"github.com/kohkimakimoto/gluayaml"
|
||||
"github.com/otm/gluaflag"
|
||||
"github.com/otm/gluash"
|
||||
"github.com/yuin/gluare"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/yuin/gopher-lua/parse"
|
||||
json "layeh.com/gopher-json"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(mainAux())
|
||||
}
|
||||
|
||||
func mainAux() int {
|
||||
var opt_e, opt_l, opt_p string
|
||||
var opt_i, opt_v, opt_dt, opt_dc bool
|
||||
var opt_m int
|
||||
flag.StringVar(&opt_e, "e", "", "")
|
||||
flag.StringVar(&opt_l, "l", "", "")
|
||||
flag.StringVar(&opt_p, "p", "", "")
|
||||
flag.IntVar(&opt_m, "mx", 0, "")
|
||||
flag.BoolVar(&opt_i, "i", false, "")
|
||||
flag.BoolVar(&opt_v, "v", false, "")
|
||||
flag.BoolVar(&opt_dt, "dt", false, "")
|
||||
flag.BoolVar(&opt_dc, "dc", false, "")
|
||||
flag.Usage = func() {
|
||||
fmt.Println(`Usage: glue [options] [script [args]].
|
||||
Available options are:
|
||||
-e stat execute string 'stat'
|
||||
-l name require library 'name'
|
||||
-mx MB memory limit(default: unlimited)
|
||||
-dt dump AST trees
|
||||
-dc dump VM codes
|
||||
-i enter interactive mode after executing 'script'
|
||||
-p file write cpu profiles to the file
|
||||
-v show version information
|
||||
`)
|
||||
}
|
||||
flag.Parse()
|
||||
if len(opt_p) != 0 {
|
||||
f, err := os.Create(opt_p)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
if len(opt_e) == 0 && !opt_i && !opt_v && flag.NArg() == 0 {
|
||||
opt_i = true
|
||||
}
|
||||
|
||||
status := 0
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
if opt_m > 0 {
|
||||
L.SetMx(opt_m)
|
||||
}
|
||||
|
||||
preload(L)
|
||||
|
||||
if opt_v || opt_i {
|
||||
fmt.Println(lua.PackageCopyRight)
|
||||
}
|
||||
|
||||
if len(opt_l) > 0 {
|
||||
if err := L.DoFile(opt_l); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if nargs := flag.NArg(); nargs > 0 {
|
||||
script := flag.Arg(0)
|
||||
argtb := L.NewTable()
|
||||
for i := 1; i < nargs; i++ {
|
||||
L.RawSet(argtb, lua.LNumber(i), lua.LString(flag.Arg(i)))
|
||||
}
|
||||
L.SetGlobal("arg", argtb)
|
||||
if opt_dt || opt_dc {
|
||||
file, err := os.Open(script)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return 1
|
||||
}
|
||||
chunk, err2 := parse.Parse(file, script)
|
||||
if err2 != nil {
|
||||
fmt.Println(err2.Error())
|
||||
return 1
|
||||
}
|
||||
if opt_dt {
|
||||
fmt.Println(parse.Dump(chunk))
|
||||
}
|
||||
if opt_dc {
|
||||
proto, err3 := lua.Compile(chunk, script)
|
||||
if err3 != nil {
|
||||
fmt.Println(err3.Error())
|
||||
return 1
|
||||
}
|
||||
fmt.Println(proto.String())
|
||||
}
|
||||
}
|
||||
|
||||
if err := L.DoFile(script); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
status = 1
|
||||
}
|
||||
}
|
||||
|
||||
if len(opt_e) > 0 {
|
||||
if err := L.DoString(opt_e); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
status = 1
|
||||
}
|
||||
}
|
||||
|
||||
if opt_i {
|
||||
doREPL(L)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func preload(L *lua.LState) {
|
||||
L.PreloadModule("re", gluare.Loader)
|
||||
L.PreloadModule("sh", gluash.Loader)
|
||||
L.PreloadModule("fs", gluafs.Loader)
|
||||
L.PreloadModule("env", gluaenv.Loader)
|
||||
L.PreloadModule("yaml", gluayaml.Loader)
|
||||
L.PreloadModule("question", gluaquestion.Loader)
|
||||
L.PreloadModule("ssh", gluassh.Loader)
|
||||
L.PreloadModule("http", gluahttp.NewHttpModule(&http.Client{}).Loader)
|
||||
L.PreloadModule("flag", gluaflag.Loader)
|
||||
L.PreloadModule("template", gluatemplate.Loader)
|
||||
L.PreloadModule("url", gluaurl.Loader)
|
||||
gluaexpect.Preload(L)
|
||||
gluasimplebox.Preload(L)
|
||||
gluaxmlpath.Preload(L)
|
||||
json.Preload(L)
|
||||
}
|
||||
|
||||
// do read/eval/print/loop
|
||||
func doREPL(L *lua.LState) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
if str, err := loadline(reader, L); err == nil {
|
||||
if err := L.DoString(str); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
} else { // error on loadline
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func incomplete(err error) bool {
|
||||
if lerr, ok := err.(*lua.ApiError); ok {
|
||||
if perr, ok := lerr.Cause.(*parse.Error); ok {
|
||||
return perr.Pos.Line == parse.EOF
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadline(reader *bufio.Reader, L *lua.LState) (string, error) {
|
||||
fmt.Print("> ")
|
||||
if line, err := reader.ReadString('\n'); err == nil {
|
||||
if _, err := L.LoadString("return " + line); err == nil { // try add return <...> then compile
|
||||
return line, nil
|
||||
} else {
|
||||
return multiline(line, reader, L)
|
||||
}
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
func multiline(ml string, reader *bufio.Reader, L *lua.LState) (string, error) {
|
||||
for {
|
||||
if _, err := L.LoadString(ml); err == nil { // try compile
|
||||
return ml, nil
|
||||
} else if !incomplete(err) { // syntax error , but not EOF
|
||||
return ml, nil
|
||||
} else {
|
||||
fmt.Print(">> ")
|
||||
if line, err := reader.ReadString('\n'); err == nil {
|
||||
ml = ml + "\n" + line
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package gluaexpect
|
||||
|
||||
import (
|
||||
"github.com/ThomasRooney/gexpect"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
func Preload(L *lua.LState) {
|
||||
L.PreloadModule("expect", Loader)
|
||||
}
|
||||
|
||||
// Loader is the module loader function.
|
||||
func Loader(L *lua.LState) int {
|
||||
mod := L.SetFuncs(L.NewTable(), api)
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
var api = map[string]lua.LGFunction{
|
||||
"spawn": spawn,
|
||||
}
|
||||
|
||||
func spawn(L *lua.LState) int {
|
||||
cmd := L.CheckString(1)
|
||||
child, err := gexpect.Spawn(cmd)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(luar.New(L, child))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package gluasimplebox
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/brandur/simplebox"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
func Preload(L *lua.LState) {
|
||||
L.PreloadModule("simplebox", Loader)
|
||||
}
|
||||
|
||||
// Loader is the module loader function.
|
||||
func Loader(L *lua.LState) int {
|
||||
mod := L.SetFuncs(L.NewTable(), api)
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
var api = map[string]lua.LGFunction{
|
||||
"new": newSecretBox,
|
||||
"genkey": genKey,
|
||||
}
|
||||
|
||||
func newSecretBox(L *lua.LState) int {
|
||||
key := L.CheckString(1)
|
||||
|
||||
k, err := parseKey(key)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
sb := simplebox.NewFromSecretKey(k)
|
||||
|
||||
L.Push(luar.New(L, &box{sb: sb}))
|
||||
return 1
|
||||
}
|
||||
|
||||
func genKey(L *lua.LState) int {
|
||||
key, err := generateKey()
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(base64.URLEncoding.EncodeToString(key[:])))
|
||||
return 1
|
||||
}
|
||||
|
||||
func generateKey() (*[32]byte, error) {
|
||||
var k [32]byte
|
||||
_, err := rand.Read(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
func parseKey(s string) (*[32]byte, error) {
|
||||
k := &[32]byte{}
|
||||
raw, err := base64.URLEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n := copy(k[:], raw); n < len(k) {
|
||||
return nil, errors.New("not valid")
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
type box struct {
|
||||
sb *simplebox.SimpleBox
|
||||
}
|
||||
|
||||
func (b *box) Encrypt(data string) string {
|
||||
result := b.sb.Encrypt([]byte(data))
|
||||
return hex.EncodeToString(result)
|
||||
}
|
||||
|
||||
func (b *box) Decrypt(data string) (string, error) {
|
||||
d, err := hex.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plain, err := b.sb.Decrypt([]byte(d))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plain), nil
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
# gluaxmlpath
|
||||
|
||||
gluaxmlpath provides an easy way to use [xmlpath](https://github.com/go-xmlpath/xmlpath) from within [GopherLua](https://github.com/yuin/gopher-lua).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/ailncode/gluaxmlpath
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ailncode/gluaxmlpath"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
gluaxmlpath.Preload(L)
|
||||
|
||||
if err := L.DoString(`
|
||||
xml ="<bookist><book>x1</book><book>x2</book><book>x3</book></booklist>"
|
||||
local xmlpath = require("xmlpath")
|
||||
node,err = xmlpath.loadxml(xml)
|
||||
path,err = xmlpath.compile("//book")
|
||||
it = path:iter(node)
|
||||
for k,v in pairs(it) do
|
||||
print(k,v:string())
|
||||
end
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,37 @@
|
|||
package gluaxmlpath
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/yuin/gopher-lua"
|
||||
xmlpath "gopkg.in/xmlpath.v2"
|
||||
)
|
||||
|
||||
var api = map[string]lua.LGFunction{
|
||||
"loadxml": loadXml,
|
||||
"compile": compile,
|
||||
}
|
||||
|
||||
func loadXml(L *lua.LState) int {
|
||||
xmlStr := L.CheckString(1)
|
||||
r := bytes.NewReader([]byte(xmlStr))
|
||||
node, err := xmlpath.ParseHTML(r)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(newNode(L, node))
|
||||
return 1
|
||||
}
|
||||
|
||||
func compile(L *lua.LState) int {
|
||||
xpathStr := L.CheckString(1)
|
||||
path, err := xmlpath.Compile(xpathStr)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(newPath(L, path))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package gluaxmlpath
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// Preload adds xmlpath to the given Lua state's package.preload table. After it
|
||||
// has been preloaded, it can be loaded using require:
|
||||
//
|
||||
// local xmlpath = require("xmlpath")
|
||||
func Preload(L *lua.LState) {
|
||||
L.PreloadModule("xmlpath", Loader)
|
||||
}
|
||||
|
||||
// Loader is the module loader function.
|
||||
func Loader(L *lua.LState) int {
|
||||
mod := L.SetFuncs(L.NewTable(), api)
|
||||
registerType(L, mod)
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package gluaxmlpath
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
xmlpath "gopkg.in/xmlpath.v2"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
base *xmlpath.Node
|
||||
}
|
||||
type Path struct {
|
||||
base *xmlpath.Path
|
||||
}
|
||||
|
||||
type Iter struct {
|
||||
base *xmlpath.Iter
|
||||
}
|
||||
|
||||
const luaNodeTypeName = "xmlpath.node"
|
||||
const luaPathTypeName = "xmlpath.path"
|
||||
const luaIterTypeName = "xmlpath.iter"
|
||||
|
||||
func registerType(L *lua.LState, module *lua.LTable) {
|
||||
//reg node
|
||||
nodemt := L.NewTypeMetatable(luaNodeTypeName)
|
||||
L.SetField(module, "node", nodemt)
|
||||
L.SetField(nodemt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"string": nodeString,
|
||||
}))
|
||||
//reg path
|
||||
pathmt := L.NewTypeMetatable(luaPathTypeName)
|
||||
L.SetField(module, "path", pathmt)
|
||||
L.SetField(pathmt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"iter": iter,
|
||||
}))
|
||||
//reg iter
|
||||
itermt := L.NewTypeMetatable(luaIterTypeName)
|
||||
L.SetField(module, "iter", itermt)
|
||||
L.SetField(itermt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
//"next": next,
|
||||
"node": node,
|
||||
}))
|
||||
}
|
||||
func newNode(L *lua.LState, n *xmlpath.Node) *lua.LUserData {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = &Node{
|
||||
n,
|
||||
}
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaNodeTypeName))
|
||||
return ud
|
||||
}
|
||||
func checkNode(L *lua.LState) *Node {
|
||||
ud := L.CheckUserData(1)
|
||||
if v, ok := ud.Value.(*Node); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(1, "node expected")
|
||||
return nil
|
||||
}
|
||||
func newPath(L *lua.LState, p *xmlpath.Path) *lua.LUserData {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = &Path{
|
||||
p,
|
||||
}
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaPathTypeName))
|
||||
return ud
|
||||
}
|
||||
func checkPath(L *lua.LState) *Path {
|
||||
ud := L.CheckUserData(1)
|
||||
if v, ok := ud.Value.(*Path); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(1, "path expected")
|
||||
return nil
|
||||
}
|
||||
func newIter(L *lua.LState, i *xmlpath.Iter) *lua.LUserData {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = &Iter{
|
||||
i,
|
||||
}
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaIterTypeName))
|
||||
return ud
|
||||
}
|
||||
func checkIter(L *lua.LState) *Iter {
|
||||
ud := L.CheckUserData(1)
|
||||
if v, ok := ud.Value.(*Iter); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(1, "iter expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
//iter := path.iter(node)
|
||||
func iter(L *lua.LState) int {
|
||||
path := checkPath(L)
|
||||
if L.GetTop() == 2 {
|
||||
ut := L.CheckUserData(2)
|
||||
if node, ok := ut.Value.(*Node); ok {
|
||||
it := path.base.Iter(node.base)
|
||||
ltab := L.NewTable()
|
||||
i := 1
|
||||
for it.Next() {
|
||||
L.RawSetInt(ltab, i, newNode(L, it.Node()))
|
||||
i++
|
||||
}
|
||||
L.Push(ltab)
|
||||
//L.Push(newIter(L, it))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
L.ArgError(1, "node expected")
|
||||
return 0
|
||||
}
|
||||
|
||||
//support lua standard iterator
|
||||
//hasNext := iter.next()
|
||||
// func next(L *lua.LState) int {
|
||||
// iter := checkIter(L)
|
||||
// L.Push(lua.LBool(iter.base.Next()))
|
||||
// return 1
|
||||
// }
|
||||
|
||||
//node := iter.node()
|
||||
func node(L *lua.LState) int {
|
||||
iter := checkIter(L)
|
||||
L.Push(newNode(L, iter.base.Node()))
|
||||
return 1
|
||||
}
|
||||
|
||||
//string := node.string()
|
||||
func nodeString(L *lua.LState) int {
|
||||
node := checkNode(L)
|
||||
L.Push(lua.LString(node.base.String()))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
|
@ -0,0 +1,15 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
|
||||
install:
|
||||
- go get github.com/yuin/gopher-lua
|
||||
|
||||
script:
|
||||
- go test -v
|
||||
|
||||
notifications:
|
||||
email: false
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Christian Joudrey
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
# gluahttp
|
||||
|
||||
[![](https://travis-ci.org/cjoudrey/gluahttp.svg)](https://travis-ci.org/cjoudrey/gluahttp)
|
||||
|
||||
gluahttp provides an easy way to make HTTP requests from within [GopherLua](https://github.com/yuin/gopher-lua).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/cjoudrey/gluahttp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "github.com/cjoudrey/gluahttp"
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("http", NewHttpModule(&http.Client{}).Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
|
||||
local http = require("http")
|
||||
|
||||
response, error_message = http.request("GET", "http://example.com", {
|
||||
query="page=1"
|
||||
headers={
|
||||
Accept="*/*"
|
||||
}
|
||||
})
|
||||
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- [`http.delete(url [, options])`](#httpdeleteurl--options)
|
||||
- [`http.get(url [, options])`](#httpgeturl--options)
|
||||
- [`http.head(url [, options])`](#httpheadurl--options)
|
||||
- [`http.patch(url [, options])`](#httppatchurl--options)
|
||||
- [`http.post(url [, options])`](#httpposturl--options)
|
||||
- [`http.put(url [, options])`](#httpputurl--options)
|
||||
- [`http.request(method, url [, options])`](#httprequestmethod-url--options)
|
||||
- [`http.request_batch(requests)`](#httprequest_batchrequests)
|
||||
- [`http.response`](#httpresponse)
|
||||
|
||||
### http.delete(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.get(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.head(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.patch(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.post(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.put(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.request(method, url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| method | String | The HTTP request method |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| body | String | Request body. |
|
||||
| form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.request_batch(requests)
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------- | ----- | ----------- |
|
||||
| requests | Table | A table of requests to send. Each request item is by itself a table containing [http.request](#httprequestmethod-url--options) parameters for the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[[http.response](#httpresponse)] or ([[http.response](#httpresponse)], [error message])
|
||||
|
||||
### http.response
|
||||
|
||||
The `http.response` table contains information about a completed HTTP request.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------ | ----------- |
|
||||
| body | String | The HTTP response body |
|
||||
| body_size | Number | The size of the HTTP reponse body in bytes |
|
||||
| headers | Table | The HTTP response headers |
|
||||
| cookies | Table | The cookies sent by the server in the HTTP response |
|
||||
| status_code | Number | The HTTP response status code |
|
||||
| url | String | The final URL the request ended pointing to after redirects |
|
|
@ -0,0 +1,212 @@
|
|||
package gluahttp
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "net/http"
|
||||
import "fmt"
|
||||
import "errors"
|
||||
import "io/ioutil"
|
||||
import "strings"
|
||||
|
||||
type httpModule struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type empty struct{}
|
||||
|
||||
func NewHttpModule(client *http.Client) *httpModule {
|
||||
return &httpModule{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpModule) Loader(L *lua.LState) int {
|
||||
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"get": h.get,
|
||||
"delete": h.delete,
|
||||
"head": h.head,
|
||||
"patch": h.patch,
|
||||
"post": h.post,
|
||||
"put": h.put,
|
||||
"request": h.request,
|
||||
"request_batch": h.requestBatch,
|
||||
})
|
||||
registerHttpResponseType(mod, L)
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
func (h *httpModule) get(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "get", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) delete(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "delete", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) head(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "head", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) patch(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "patch", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) post(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "post", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) put(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "put", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) request(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, L.ToString(1), L.ToString(2), L.ToTable(3))
|
||||
}
|
||||
|
||||
func (h *httpModule) requestBatch(L *lua.LState) int {
|
||||
requests := L.ToTable(1)
|
||||
amountRequests := requests.Len()
|
||||
|
||||
errs := make([]error, amountRequests)
|
||||
responses := make([]*lua.LUserData, amountRequests)
|
||||
sem := make(chan empty, amountRequests)
|
||||
|
||||
i := 0
|
||||
|
||||
requests.ForEach(func(_ lua.LValue, value lua.LValue) {
|
||||
requestTable := toTable(value)
|
||||
|
||||
if requestTable != nil {
|
||||
method := requestTable.RawGet(lua.LNumber(1)).String()
|
||||
url := requestTable.RawGet(lua.LNumber(2)).String()
|
||||
options := toTable(requestTable.RawGet(lua.LNumber(3)))
|
||||
|
||||
go func(i int, L *lua.LState, method string, url string, options *lua.LTable) {
|
||||
response, err := h.doRequest(L, method, url, options)
|
||||
|
||||
if err == nil {
|
||||
errs[i] = nil
|
||||
responses[i] = response
|
||||
} else {
|
||||
errs[i] = err
|
||||
responses[i] = nil
|
||||
}
|
||||
|
||||
sem <- empty{}
|
||||
}(i, L, method, url, options)
|
||||
} else {
|
||||
errs[i] = errors.New("Request must be a table")
|
||||
responses[i] = nil
|
||||
sem <- empty{}
|
||||
}
|
||||
|
||||
i = i + 1
|
||||
})
|
||||
|
||||
for i = 0; i < amountRequests; i++ {
|
||||
<-sem
|
||||
}
|
||||
|
||||
hasErrors := false
|
||||
errorsTable := L.NewTable()
|
||||
responsesTable := L.NewTable()
|
||||
for i = 0; i < amountRequests; i++ {
|
||||
if errs[i] == nil {
|
||||
responsesTable.Append(responses[i])
|
||||
errorsTable.Append(lua.LNil)
|
||||
} else {
|
||||
responsesTable.Append(lua.LNil)
|
||||
errorsTable.Append(lua.LString(fmt.Sprintf("%s", errs[i])))
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasErrors {
|
||||
L.Push(responsesTable)
|
||||
L.Push(errorsTable)
|
||||
return 2
|
||||
} else {
|
||||
L.Push(responsesTable)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpModule) doRequest(L *lua.LState, method string, url string, options *lua.LTable) (*lua.LUserData, error) {
|
||||
req, err := http.NewRequest(strings.ToUpper(method), url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options != nil {
|
||||
if reqCookies, ok := options.RawGet(lua.LString("cookies")).(*lua.LTable); ok {
|
||||
reqCookies.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
req.AddCookie(&http.Cookie{Name: key.String(), Value: value.String()})
|
||||
})
|
||||
}
|
||||
|
||||
switch reqQuery := options.RawGet(lua.LString("query")).(type) {
|
||||
case lua.LString:
|
||||
req.URL.RawQuery = reqQuery.String()
|
||||
}
|
||||
|
||||
body := options.RawGet(lua.LString("body"))
|
||||
if _, ok := body.(lua.LString); !ok {
|
||||
// "form" is deprecated.
|
||||
body = options.RawGet(lua.LString("form"))
|
||||
// Only set the Content-Type to application/x-www-form-urlencoded
|
||||
// when someone uses "form", not for "body".
|
||||
if _, ok := body.(lua.LString); ok {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
}
|
||||
|
||||
switch reqBody := body.(type) {
|
||||
case lua.LString:
|
||||
body := reqBody.String()
|
||||
req.ContentLength = int64(len(body))
|
||||
req.Body = ioutil.NopCloser(strings.NewReader(body))
|
||||
}
|
||||
|
||||
// Set these last. That way the code above doesn't overwrite them.
|
||||
if reqHeaders, ok := options.RawGet(lua.LString("headers")).(*lua.LTable); ok {
|
||||
reqHeaders.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
req.Header.Set(key.String(), value.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newHttpResponse(res, &body, len(body), L), nil
|
||||
}
|
||||
|
||||
func (h *httpModule) doRequestAndPush(L *lua.LState, method string, url string, options *lua.LTable) int {
|
||||
response, err := h.doRequest(L, method, url, options)
|
||||
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("%s", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(response)
|
||||
return 1
|
||||
}
|
||||
|
||||
func toTable(v lua.LValue) *lua.LTable {
|
||||
if lv, ok := v.(*lua.LTable); ok {
|
||||
return lv
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,433 @@
|
|||
package gluahttp
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "testing"
|
||||
import "io/ioutil"
|
||||
import "net/http"
|
||||
import "net"
|
||||
import "fmt"
|
||||
import "net/http/cookiejar"
|
||||
import "strings"
|
||||
|
||||
func TestRequestNoMethod(t *testing.T) {
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request()
|
||||
|
||||
assert_equal(nil, response)
|
||||
assert_contains('unsupported protocol scheme ""', error)
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestNoUrl(t *testing.T) {
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get")
|
||||
|
||||
assert_equal(nil, response)
|
||||
assert_contains('unsupported protocol scheme ""', error)
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBatch(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
responses, errors = http.request_batch({
|
||||
{"get", "http://`+listener.Addr().String()+`", {query="page=1"}},
|
||||
{"post", "http://`+listener.Addr().String()+`/set_cookie"},
|
||||
{"post", ""},
|
||||
1
|
||||
})
|
||||
|
||||
assert_equal(nil, errors[1])
|
||||
assert_equal(nil, errors[2])
|
||||
assert_contains('unsupported protocol scheme ""', errors[3])
|
||||
assert_equal('Request must be a table', errors[4])
|
||||
|
||||
assert_equal('Requested GET / with query "page=1"', responses[1]["body"])
|
||||
assert_equal('Cookie set!', responses[2]["body"])
|
||||
assert_equal('12345', responses[2]["cookies"]["session_id"])
|
||||
assert_equal(nil, responses[3])
|
||||
assert_equal(nil, responses[4])
|
||||
|
||||
responses, errors = http.request_batch({
|
||||
{"get", "http://`+listener.Addr().String()+`/get_cookie"}
|
||||
})
|
||||
|
||||
assert_equal(nil, errors)
|
||||
assert_equal("session_id=12345", responses[1]["body"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestGet(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get", "http://`+listener.Addr().String()+`")
|
||||
|
||||
assert_equal('Requested GET / with query ""', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('29', response['headers']['Content-Length'])
|
||||
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestGetWithRedirect(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get", "http://`+listener.Addr().String()+`/redirect")
|
||||
|
||||
assert_equal('Requested GET / with query ""', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('http://`+listener.Addr().String()+`/', response['url'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestPostForm(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("post", "http://`+listener.Addr().String()+`", {
|
||||
form="username=bob&password=secret"
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested POST / with query ""' ..
|
||||
'Content-Type: application/x-www-form-urlencoded' ..
|
||||
'Content-Length: 28' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestHeaders(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("post", "http://`+listener.Addr().String()+`", {
|
||||
headers={
|
||||
["Content-Type"]="application/json"
|
||||
}
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested POST / with query ""' ..
|
||||
'Content-Type: application/json' ..
|
||||
'Content-Length: 0' ..
|
||||
'Body: ', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestQuery(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get", "http://`+listener.Addr().String()+`", {
|
||||
query="page=2"
|
||||
})
|
||||
|
||||
assert_equal('Requested GET / with query "page=2"', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`", {
|
||||
query="page=1"
|
||||
})
|
||||
|
||||
assert_equal('Requested GET / with query "page=1"', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('35', response['headers']['Content-Length'])
|
||||
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.delete("http://`+listener.Addr().String()+`", {
|
||||
query="page=1"
|
||||
})
|
||||
|
||||
assert_equal('Requested DELETE / with query "page=1"', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('38', response['headers']['Content-Length'])
|
||||
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHead(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.head("http://`+listener.Addr().String()+`/head", {
|
||||
query="page=1"
|
||||
})
|
||||
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal("/head?page=1", response['headers']['X-Request-Uri'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPost(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.post("http://`+listener.Addr().String()+`", {
|
||||
body="username=bob&password=secret"
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested POST / with query ""' ..
|
||||
'Content-Type: ' ..
|
||||
'Content-Length: 28' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.patch("http://`+listener.Addr().String()+`", {
|
||||
body='{"username":"bob"}',
|
||||
headers={
|
||||
["Content-Type"]="application/json"
|
||||
}
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested PATCH / with query ""' ..
|
||||
'Content-Type: application/json' ..
|
||||
'Content-Length: 18' ..
|
||||
'Body: {"username":"bob"}', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.put("http://`+listener.Addr().String()+`", {
|
||||
body="username=bob&password=secret",
|
||||
headers={
|
||||
["Content-Type"]="application/x-www-form-urlencoded"
|
||||
}
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested PUT / with query ""' ..
|
||||
'Content-Type: application/x-www-form-urlencoded' ..
|
||||
'Content-Length: 28' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseCookies(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/set_cookie")
|
||||
|
||||
assert_equal('Cookie set!', response["body"])
|
||||
assert_equal('12345', response["cookies"]["session_id"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestCookies(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/get_cookie", {
|
||||
cookies={
|
||||
["session_id"]="test"
|
||||
}
|
||||
})
|
||||
|
||||
assert_equal('session_id=test', response["body"])
|
||||
assert_equal(15, response["body_size"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseBodySize(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/")
|
||||
|
||||
assert_equal(29, response["body_size"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseBody(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/")
|
||||
|
||||
assert_equal("Requested XXX / with query \"\"", string.gsub(response.body, "GET", "XXX"))
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseUrl(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/redirect")
|
||||
assert_equal("http://`+listener.Addr().String()+`/", response["url"])
|
||||
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/get_cookie")
|
||||
assert_equal("http://`+listener.Addr().String()+`/get_cookie", response["url"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func evalLua(t *testing.T, script string) error {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
|
||||
L.PreloadModule("http", NewHttpModule(&http.Client{
|
||||
Jar: cookieJar,
|
||||
},
|
||||
).Loader)
|
||||
|
||||
L.SetGlobal("assert_equal", L.NewFunction(func(L *lua.LState) int {
|
||||
expected := L.Get(1)
|
||||
actual := L.Get(2)
|
||||
|
||||
if expected.Type() != actual.Type() || expected.String() != actual.String() {
|
||||
t.Errorf("Expected %s %q, got %s %q", expected.Type(), expected, actual.Type(), actual)
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetGlobal("assert_contains", L.NewFunction(func(L *lua.LState) int {
|
||||
contains := L.Get(1)
|
||||
actual := L.Get(2)
|
||||
|
||||
if !strings.Contains(actual.String(), contains.String()) {
|
||||
t.Errorf("Expected %s %q contains %s %q", actual.Type(), actual, contains.Type(), contains)
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
return L.DoString(script)
|
||||
}
|
||||
|
||||
func setupServer(listener net.Listener) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Requested %s / with query %q", req.Method, req.URL.RawQuery)
|
||||
|
||||
if req.Method == "POST" || req.Method == "PATCH" || req.Method == "PUT" {
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
fmt.Fprintf(w, "Content-Type: %s", req.Header.Get("Content-Type"))
|
||||
fmt.Fprintf(w, "Content-Length: %s", req.Header.Get("Content-Length"))
|
||||
fmt.Fprintf(w, "Body: %s", body)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/head", func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "HEAD" {
|
||||
w.Header().Set("X-Request-Uri", req.URL.String())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/set_cookie", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "12345"})
|
||||
fmt.Fprint(w, "Cookie set!")
|
||||
})
|
||||
mux.HandleFunc("/get_cookie", func(w http.ResponseWriter, req *http.Request) {
|
||||
session_id, _ := req.Cookie("session_id")
|
||||
fmt.Fprint(w, session_id)
|
||||
})
|
||||
mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, "/", http.StatusFound)
|
||||
})
|
||||
s := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
go s.Serve(listener)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package gluahttp
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "net/http"
|
||||
|
||||
const luaHttpResponseTypeName = "http.response"
|
||||
|
||||
type luaHttpResponse struct {
|
||||
res *http.Response
|
||||
body lua.LString
|
||||
bodySize int
|
||||
}
|
||||
|
||||
func registerHttpResponseType(module *lua.LTable, L *lua.LState) {
|
||||
mt := L.NewTypeMetatable(luaHttpResponseTypeName)
|
||||
L.SetField(mt, "__index", L.NewFunction(httpResponseIndex))
|
||||
|
||||
L.SetField(module, "response", mt)
|
||||
}
|
||||
|
||||
func newHttpResponse(res *http.Response, body *[]byte, bodySize int, L *lua.LState) *lua.LUserData {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = &luaHttpResponse{
|
||||
res: res,
|
||||
body: lua.LString(*body),
|
||||
bodySize: bodySize,
|
||||
}
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaHttpResponseTypeName))
|
||||
return ud
|
||||
}
|
||||
|
||||
func checkHttpResponse(L *lua.LState) *luaHttpResponse {
|
||||
ud := L.CheckUserData(1)
|
||||
if v, ok := ud.Value.(*luaHttpResponse); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(1, "http.response expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func httpResponseIndex(L *lua.LState) int {
|
||||
res := checkHttpResponse(L)
|
||||
|
||||
switch L.CheckString(2) {
|
||||
case "headers":
|
||||
return httpResponseHeaders(res, L)
|
||||
case "cookies":
|
||||
return httpResponseCookies(res, L)
|
||||
case "status_code":
|
||||
return httpResponseStatusCode(res, L)
|
||||
case "url":
|
||||
return httpResponseUrl(res, L)
|
||||
case "body":
|
||||
return httpResponseBody(res, L)
|
||||
case "body_size":
|
||||
return httpResponseBodySize(res, L)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func httpResponseHeaders(res *luaHttpResponse, L *lua.LState) int {
|
||||
headers := L.NewTable()
|
||||
for key, _ := range res.res.Header {
|
||||
headers.RawSetString(key, lua.LString(res.res.Header.Get(key)))
|
||||
}
|
||||
L.Push(headers)
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseCookies(res *luaHttpResponse, L *lua.LState) int {
|
||||
cookies := L.NewTable()
|
||||
for _, cookie := range res.res.Cookies() {
|
||||
cookies.RawSetString(cookie.Name, lua.LString(cookie.Value))
|
||||
}
|
||||
L.Push(cookies)
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseStatusCode(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(lua.LNumber(res.res.StatusCode))
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseUrl(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(lua.LString(res.res.Request.URL.String()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseBody(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(res.body)
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseBodySize(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(lua.LNumber(res.bodySize))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
|
@ -0,0 +1,14 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
|
||||
install:
|
||||
- go get github.com/yuin/gopher-lua
|
||||
|
||||
script:
|
||||
- go test -v
|
||||
|
||||
notifications:
|
||||
email: false
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Christian Joudrey
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# gluaurl
|
||||
|
||||
[![](https://travis-ci.org/cjoudrey/gluaurl.svg)](https://travis-ci.org/cjoudrey/gluaurl)
|
||||
|
||||
gluahttp provides an easy way to parse and build URLs from within [GopherLua](https://github.com/yuin/gopher-lua).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/cjoudrey/gluaurl
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "github.com/cjoudrey/gluaurl"
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("url", gluaurl.Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
|
||||
local url = require("url")
|
||||
|
||||
parsed_url = url.parse("http://example.com/")
|
||||
|
||||
print(parsed_url.host)
|
||||
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- [`url.parse(url)`](#urlparseurl)
|
||||
- [`url.build(options)`](#urlbuildoptions)
|
||||
- [`url.build_query_string(query_params)`](#urlbuild_query_stringquery_params)
|
||||
- [`url.resolve(from, to)`](#urlresolvefrom-to)
|
||||
|
||||
### url.parse(url)
|
||||
|
||||
Parse URL into a table of key/value components.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL to parsed |
|
||||
|
||||
**Returns**
|
||||
|
||||
Table with parsed URL or (nil, error message)
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------- | ------ | ----------- |
|
||||
| scheme | String | Scheme of the URL |
|
||||
| username | String | Username |
|
||||
| password | String | Password |
|
||||
| host | String | Host and port of the URL |
|
||||
| path | String | Path |
|
||||
| query | String | Query string |
|
||||
| fragment | String | Fragment |
|
||||
|
||||
### url.build(options)
|
||||
|
||||
Assemble a URL string from a table of URL components.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ----- | ----------- |
|
||||
| options | Table | Table with URL components, see [`url.parse`](#urlparseurl) for list of valid components |
|
||||
|
||||
**Returns**
|
||||
|
||||
String
|
||||
|
||||
### url.build_query_string(query_params)
|
||||
|
||||
Assemble table of query string parameters into a string.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------ | ----- | ----------- |
|
||||
| query_params | Table | Table with query parameters |
|
||||
|
||||
**Returns**
|
||||
|
||||
String
|
||||
|
||||
### url.resolve(from, to)
|
||||
|
||||
Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ------ | ----------- |
|
||||
| from | String | base URL |
|
||||
| to | String | href URL |
|
||||
|
||||
**Returns**
|
||||
|
||||
String or (nil, error message)
|
|
@ -0,0 +1,178 @@
|
|||
package gluaurl
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "net/url"
|
||||
import "strings"
|
||||
import "fmt"
|
||||
import "sort"
|
||||
import "regexp"
|
||||
|
||||
var rBracket = regexp.MustCompile("\\[\\]$")
|
||||
|
||||
func Loader(L *lua.LState) int {
|
||||
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"parse": parse,
|
||||
"build": build,
|
||||
"build_query_string": buildQueryString,
|
||||
"resolve": resolve,
|
||||
})
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
func parse(L *lua.LState) int {
|
||||
parsed := L.NewTable()
|
||||
|
||||
url, err := url.Parse(L.CheckString(1))
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("%s", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
parsed.RawSetString("scheme", lua.LString(url.Scheme))
|
||||
|
||||
if url.User != nil {
|
||||
parsed.RawSetString("username", lua.LString(url.User.Username()))
|
||||
|
||||
if password, hasPassword := url.User.Password(); hasPassword {
|
||||
parsed.RawSetString("password", lua.LString(password))
|
||||
} else {
|
||||
parsed.RawSetString("password", lua.LNil)
|
||||
}
|
||||
|
||||
} else {
|
||||
parsed.RawSetString("username", lua.LNil)
|
||||
parsed.RawSetString("password", lua.LNil)
|
||||
}
|
||||
|
||||
parsed.RawSetString("host", lua.LString(url.Host))
|
||||
parsed.RawSetString("path", lua.LString(url.Path))
|
||||
parsed.RawSetString("query", lua.LString(url.RawQuery))
|
||||
parsed.RawSetString("fragment", lua.LString(url.Fragment))
|
||||
|
||||
L.Push(parsed)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func build(L *lua.LState) int {
|
||||
options := L.CheckTable(1)
|
||||
|
||||
buildUrl := url.URL{}
|
||||
|
||||
if scheme := options.RawGetString("scheme"); scheme != lua.LNil {
|
||||
buildUrl.Scheme = scheme.String()
|
||||
}
|
||||
|
||||
if username := options.RawGetString("username"); username != lua.LNil {
|
||||
if password := options.RawGetString("password"); password != lua.LNil {
|
||||
buildUrl.User = url.UserPassword(username.String(), password.String())
|
||||
} else {
|
||||
buildUrl.User = url.User(username.String())
|
||||
}
|
||||
}
|
||||
|
||||
if host := options.RawGetString("host"); host != lua.LNil {
|
||||
buildUrl.Host = host.String()
|
||||
}
|
||||
|
||||
if path := options.RawGetString("path"); path != lua.LNil {
|
||||
buildUrl.Path = path.String()
|
||||
}
|
||||
|
||||
if query := options.RawGetString("query"); query != lua.LNil {
|
||||
buildUrl.RawQuery = query.String()
|
||||
}
|
||||
|
||||
if fragment := options.RawGetString("fragment"); fragment != lua.LNil {
|
||||
buildUrl.Fragment = fragment.String()
|
||||
}
|
||||
|
||||
L.Push(lua.LString(buildUrl.String()))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func buildQueryString(L *lua.LState) int {
|
||||
options := L.CheckTable(1)
|
||||
|
||||
ret := make([]string, 0)
|
||||
|
||||
options.ForEach(func(key, value lua.LValue) {
|
||||
toQueryString(key.String(), value, &ret)
|
||||
})
|
||||
|
||||
sort.Strings(ret)
|
||||
|
||||
L.Push(lua.LString(strings.Join(ret, "&")))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func toQueryString(prefix string, lv lua.LValue, ret *[]string) {
|
||||
switch v := lv.(type) {
|
||||
case lua.LBool:
|
||||
*ret = append(*ret, url.QueryEscape(prefix)+"="+v.String())
|
||||
break
|
||||
|
||||
case lua.LNumber:
|
||||
*ret = append(*ret, url.QueryEscape(prefix)+"="+v.String())
|
||||
break
|
||||
|
||||
case lua.LString:
|
||||
*ret = append(*ret, url.QueryEscape(prefix)+"="+url.QueryEscape(v.String()))
|
||||
break
|
||||
|
||||
case *lua.LTable:
|
||||
maxn := v.MaxN()
|
||||
if maxn == 0 {
|
||||
ret2 := make([]string, 0)
|
||||
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
toQueryString(prefix+"["+key.String()+"]", value, &ret2)
|
||||
})
|
||||
sort.Strings(ret2)
|
||||
*ret = append(*ret, strings.Join(ret2, "&"))
|
||||
} else {
|
||||
ret2 := make([]string, 0)
|
||||
for i := 1; i <= maxn; i++ {
|
||||
vi := v.RawGetInt(i)
|
||||
|
||||
if rBracket.MatchString(prefix) {
|
||||
ret2 = append(ret2, url.QueryEscape(prefix)+"="+vi.String())
|
||||
} else {
|
||||
if vi.Type() == lua.LTTable {
|
||||
toQueryString(fmt.Sprintf("%s[%d]", prefix, i-1), vi, &ret2)
|
||||
} else {
|
||||
toQueryString(prefix+"[]", vi, &ret2)
|
||||
}
|
||||
}
|
||||
}
|
||||
*ret = append(*ret, strings.Join(ret2, "&"))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func resolve(L *lua.LState) int {
|
||||
from := L.CheckString(1)
|
||||
to := L.CheckString(2)
|
||||
|
||||
fromUrl, err := url.Parse(from)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("%s", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
toUrl, err := url.Parse(to)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("%s", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
resolvedUrl := fromUrl.ResolveReference(toUrl).String()
|
||||
L.Push(lua.LString(resolvedUrl))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
package gluaurl
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "testing"
|
||||
import "os"
|
||||
import "bytes"
|
||||
import "io"
|
||||
import "strings"
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
output, err := evalScript(`
|
||||
local url = require("url")
|
||||
|
||||
parsed = url.parse("http://bob:secret@example.com:8080/products?page=2#something")
|
||||
|
||||
print(parsed.scheme)
|
||||
print(parsed.username)
|
||||
print(parsed.password)
|
||||
print(parsed.host)
|
||||
print(parsed.path)
|
||||
print(parsed.query)
|
||||
print(parsed.fragment)
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
} else {
|
||||
if expected := `http
|
||||
bob
|
||||
secret
|
||||
example.com:8080
|
||||
/products
|
||||
page=2
|
||||
something
|
||||
`; expected != output {
|
||||
t.Errorf("Expected output does not match actual output\nExpected: %s\nActual: %s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOnlyHost(t *testing.T) {
|
||||
output, err := evalScript(`
|
||||
local url = require("url")
|
||||
|
||||
parsed = url.parse("https://example.com")
|
||||
|
||||
print(parsed.scheme)
|
||||
print(parsed.username)
|
||||
print(parsed.password)
|
||||
print(parsed.host)
|
||||
print(parsed.path)
|
||||
print(parsed.query)
|
||||
print(parsed.fragment)
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
} else {
|
||||
if expected := `https
|
||||
nil
|
||||
nil
|
||||
example.com
|
||||
|
||||
|
||||
|
||||
`; expected != output {
|
||||
t.Errorf("Expected output does not match actual output\nExpected: %s\nActual: %s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
output, err := evalScript(`
|
||||
local url = require("url")
|
||||
|
||||
built = url.build({
|
||||
scheme="https",
|
||||
username="bob",
|
||||
password="secret",
|
||||
host="example.com:8080",
|
||||
path="/products",
|
||||
query="page=2",
|
||||
fragment="something"
|
||||
})
|
||||
|
||||
print(built)
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
} else {
|
||||
if expected := `https://bob:secret@example.com:8080/products?page=2#something
|
||||
`; expected != output {
|
||||
t.Errorf("Expected output does not match actual output\nExpected: %s\nActual: %s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEmpty(t *testing.T) {
|
||||
output, err := evalScript(`
|
||||
local url = require("url")
|
||||
|
||||
built = url.build({})
|
||||
|
||||
print(built)
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
} else {
|
||||
if expected := `
|
||||
`; expected != output {
|
||||
t.Errorf("Expected output does not match actual output\nExpected: %s\nActual: %s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQueryString(t *testing.T) {
|
||||
output, err := evalScript(`
|
||||
local url = require("url")
|
||||
|
||||
function assert_query_string(options, expected, message)
|
||||
actual = url.build_query_string(options)
|
||||
|
||||
if expected ~= actual then
|
||||
print("Failed to build '" .. message .. "'")
|
||||
print("Expected:")
|
||||
print(expected)
|
||||
print("Actual:")
|
||||
print(actual)
|
||||
end
|
||||
end
|
||||
|
||||
assert_query_string(
|
||||
{foo="bar", baz=42, quux="All your base are belong to us"},
|
||||
"baz=42&foo=bar&quux=All+your+base+are+belong+to+us",
|
||||
"simple"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{someName={1, 2, 3}, regularThing="blah"},
|
||||
"regularThing=blah&someName%5B%5D=1&someName%5B%5D=2&someName%5B%5D=3",
|
||||
"with array"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{foo={"a", "b", "c"}},
|
||||
"foo%5B%5D=a&foo%5B%5D=b&foo%5B%5D=c",
|
||||
"with array of strings"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{foo={"baz", 42, "All your base are belong to us"}},
|
||||
"foo%5B%5D=baz&foo%5B%5D=42&foo%5B%5D=All+your+base+are+belong+to+us",
|
||||
"more array"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{foo={bar="baz", beep=42, quux="All your base are belong to us"}},
|
||||
"foo%5Bbar%5D=baz&foo%5Bbeep%5D=42&foo%5Bquux%5D=All+your+base+are+belong+to+us",
|
||||
"even more arrays"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{a={1,2}, b={c=3, d={4,5}, e={ x={6}, y=7, z={8,9} }, f=true, g=false, h=""}, i={10,11}, j=true, k=false, l={"",0}, m="cowboy hat?" },
|
||||
"a%5B%5D=1&a%5B%5D=2&b%5Bc%5D=3&b%5Bd%5D%5B%5D=4&b%5Bd%5D%5B%5D=5&b%5Be%5D%5Bx%5D%5B%5D=6&b%5Be%5D%5By%5D=7&b%5Be%5D%5Bz%5D%5B%5D=8&b%5Be%5D%5Bz%5D%5B%5D=9&b%5Bf%5D=true&b%5Bg%5D=false&b%5Bh%5D=&i%5B%5D=10&i%5B%5D=11&j=true&k=false&l%5B%5D=&l%5B%5D=0&m=cowboy+hat%3F",
|
||||
"huge structure"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{ a={0, { 1, 2 }, { 3, { 4, 5 }, { 6 } }, { b= { 7, { 8, 9 }, { { c=10, d=11 } }, { { 12 } }, { { { 13 } } }, { e= { f= { g={ 14, { 15 } } } } }, 16 } }, 17 } },
|
||||
"a%5B%5D=0&a%5B1%5D%5B%5D=1&a%5B1%5D%5B%5D=2&a%5B2%5D%5B%5D=3&a%5B2%5D%5B1%5D%5B%5D=4&a%5B2%5D%5B1%5D%5B%5D=5&a%5B2%5D%5B2%5D%5B%5D=6&a%5B3%5D%5Bb%5D%5B%5D=7&a%5B3%5D%5Bb%5D%5B1%5D%5B%5D=8&a%5B3%5D%5Bb%5D%5B1%5D%5B%5D=9&a%5B3%5D%5Bb%5D%5B2%5D%5B0%5D%5Bc%5D=10&a%5B3%5D%5Bb%5D%5B2%5D%5B0%5D%5Bd%5D=11&a%5B3%5D%5Bb%5D%5B3%5D%5B0%5D%5B%5D=12&a%5B3%5D%5Bb%5D%5B4%5D%5B0%5D%5B0%5D%5B%5D=13&a%5B3%5D%5Bb%5D%5B5%5D%5Be%5D%5Bf%5D%5Bg%5D%5B%5D=14&a%5B3%5D%5Bb%5D%5B5%5D%5Be%5D%5Bf%5D%5Bg%5D%5B1%5D%5B%5D=15&a%5B3%5D%5Bb%5D%5B%5D=16&a%5B%5D=17",
|
||||
"nested arrays"
|
||||
)
|
||||
|
||||
assert_query_string(
|
||||
{ a= {1,2,3}, ["b[]"]= {4,5,6}, ["c[d]"]= {7,8,9}, e= { f= {10}, g= {11,12}, h= 13 } },
|
||||
"a%5B%5D=1&a%5B%5D=2&a%5B%5D=3&b%5B%5D=4&b%5B%5D=5&b%5B%5D=6&c%5Bd%5D%5B%5D=7&c%5Bd%5D%5B%5D=8&c%5Bd%5D%5B%5D=9&e%5Bf%5D%5B%5D=10&e%5Bg%5D%5B%5D=11&e%5Bg%5D%5B%5D=12&e%5Bh%5D=13",
|
||||
"make sure params are not double-encoded"
|
||||
)
|
||||
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
} else {
|
||||
if expected := ``; expected != output {
|
||||
t.Error(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
output, err := evalScript(`
|
||||
local url = require("url")
|
||||
|
||||
print(url.resolve('/one/two/three', 'four'))
|
||||
print(url.resolve('http://example.com/', '/one'))
|
||||
print(url.resolve('http://example.com/one', '/two'))
|
||||
print(url.resolve('https://example.com/one', '//example2.com'))
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
} else {
|
||||
if expected := `/one/two/four
|
||||
http://example.com/one
|
||||
http://example.com/two
|
||||
https://example2.com
|
||||
`; expected != output {
|
||||
t.Errorf("Expected output does not match actual output\nExpected: %s\nActual: %s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func evalScript(script string) (string, error) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("url", Loader)
|
||||
|
||||
var err error
|
||||
|
||||
out := captureStdout(func() {
|
||||
err = L.DoString(script)
|
||||
})
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
func captureStdout(inner func()) string {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
inner()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
out := strings.Replace(<-outC, "\r", "", -1)
|
||||
|
||||
return out
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
language: go
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- tip
|
|
@ -0,0 +1,15 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2012 Chris Howey
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,384 @@
|
|||
Unless otherwise noted, all files in this distribution are released
|
||||
under the Common Development and Distribution License (CDDL).
|
||||
Exceptions are noted within the associated source files.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
|
||||
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0
|
||||
|
||||
1. Definitions.
|
||||
|
||||
1.1. "Contributor" means each individual or entity that creates
|
||||
or contributes to the creation of Modifications.
|
||||
|
||||
1.2. "Contributor Version" means the combination of the Original
|
||||
Software, prior Modifications used by a Contributor (if any),
|
||||
and the Modifications made by that particular Contributor.
|
||||
|
||||
1.3. "Covered Software" means (a) the Original Software, or (b)
|
||||
Modifications, or (c) the combination of files containing
|
||||
Original Software with files containing Modifications, in
|
||||
each case including portions thereof.
|
||||
|
||||
1.4. "Executable" means the Covered Software in any form other
|
||||
than Source Code.
|
||||
|
||||
1.5. "Initial Developer" means the individual or entity that first
|
||||
makes Original Software available under this License.
|
||||
|
||||
1.6. "Larger Work" means a work which combines Covered Software or
|
||||
portions thereof with code not governed by the terms of this
|
||||
License.
|
||||
|
||||
1.7. "License" means this document.
|
||||
|
||||
1.8. "Licensable" means having the right to grant, to the maximum
|
||||
extent possible, whether at the time of the initial grant or
|
||||
subsequently acquired, any and all of the rights conveyed
|
||||
herein.
|
||||
|
||||
1.9. "Modifications" means the Source Code and Executable form of
|
||||
any of the following:
|
||||
|
||||
A. Any file that results from an addition to, deletion from or
|
||||
modification of the contents of a file containing Original
|
||||
Software or previous Modifications;
|
||||
|
||||
B. Any new file that contains any part of the Original
|
||||
Software or previous Modifications; or
|
||||
|
||||
C. Any new file that is contributed or otherwise made
|
||||
available under the terms of this License.
|
||||
|
||||
1.10. "Original Software" means the Source Code and Executable
|
||||
form of computer software code that is originally released
|
||||
under this License.
|
||||
|
||||
1.11. "Patent Claims" means any patent claim(s), now owned or
|
||||
hereafter acquired, including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by
|
||||
grantor.
|
||||
|
||||
1.12. "Source Code" means (a) the common form of computer software
|
||||
code in which modifications are made and (b) associated
|
||||
documentation included in or with such code.
|
||||
|
||||
1.13. "You" (or "Your") means an individual or a legal entity
|
||||
exercising rights under, and complying with all of the terms
|
||||
of, this License. For legal entities, "You" includes any
|
||||
entity which controls, is controlled by, or is under common
|
||||
control with You. For purposes of this definition,
|
||||
"control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by
|
||||
contract or otherwise, or (b) ownership of more than fifty
|
||||
percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants.
|
||||
|
||||
2.1. The Initial Developer Grant.
|
||||
|
||||
Conditioned upon Your compliance with Section 3.1 below and
|
||||
subject to third party intellectual property claims, the Initial
|
||||
Developer hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or
|
||||
trademark) Licensable by Initial Developer, to use,
|
||||
reproduce, modify, display, perform, sublicense and
|
||||
distribute the Original Software (or portions thereof),
|
||||
with or without Modifications, and/or as part of a Larger
|
||||
Work; and
|
||||
|
||||
(b) under Patent Claims infringed by the making, using or
|
||||
selling of Original Software, to make, have made, use,
|
||||
practice, sell, and offer for sale, and/or otherwise
|
||||
dispose of the Original Software (or portions thereof).
|
||||
|
||||
(c) The licenses granted in Sections 2.1(a) and (b) are
|
||||
effective on the date Initial Developer first distributes
|
||||
or otherwise makes the Original Software available to a
|
||||
third party under the terms of this License.
|
||||
|
||||
(d) Notwithstanding Section 2.1(b) above, no patent license is
|
||||
granted: (1) for code that You delete from the Original
|
||||
Software, or (2) for infringements caused by: (i) the
|
||||
modification of the Original Software, or (ii) the
|
||||
combination of the Original Software with other software
|
||||
or devices.
|
||||
|
||||
2.2. Contributor Grant.
|
||||
|
||||
Conditioned upon Your compliance with Section 3.1 below and
|
||||
subject to third party intellectual property claims, each
|
||||
Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or
|
||||
trademark) Licensable by Contributor to use, reproduce,
|
||||
modify, display, perform, sublicense and distribute the
|
||||
Modifications created by such Contributor (or portions
|
||||
thereof), either on an unmodified basis, with other
|
||||
Modifications, as Covered Software and/or as part of a
|
||||
Larger Work; and
|
||||
|
||||
(b) under Patent Claims infringed by the making, using, or
|
||||
selling of Modifications made by that Contributor either
|
||||
alone and/or in combination with its Contributor Version
|
||||
(or portions of such combination), to make, use, sell,
|
||||
offer for sale, have made, and/or otherwise dispose of:
|
||||
(1) Modifications made by that Contributor (or portions
|
||||
thereof); and (2) the combination of Modifications made by
|
||||
that Contributor with its Contributor Version (or portions
|
||||
of such combination).
|
||||
|
||||
(c) The licenses granted in Sections 2.2(a) and 2.2(b) are
|
||||
effective on the date Contributor first distributes or
|
||||
otherwise makes the Modifications available to a third
|
||||
party.
|
||||
|
||||
(d) Notwithstanding Section 2.2(b) above, no patent license is
|
||||
granted: (1) for any code that Contributor has deleted
|
||||
from the Contributor Version; (2) for infringements caused
|
||||
by: (i) third party modifications of Contributor Version,
|
||||
or (ii) the combination of Modifications made by that
|
||||
Contributor with other software (except as part of the
|
||||
Contributor Version) or other devices; or (3) under Patent
|
||||
Claims infringed by Covered Software in the absence of
|
||||
Modifications made by that Contributor.
|
||||
|
||||
3. Distribution Obligations.
|
||||
|
||||
3.1. Availability of Source Code.
|
||||
|
||||
Any Covered Software that You distribute or otherwise make
|
||||
available in Executable form must also be made available in Source
|
||||
Code form and that Source Code form must be distributed only under
|
||||
the terms of this License. You must include a copy of this
|
||||
License with every copy of the Source Code form of the Covered
|
||||
Software You distribute or otherwise make available. You must
|
||||
inform recipients of any such Covered Software in Executable form
|
||||
as to how they can obtain such Covered Software in Source Code
|
||||
form in a reasonable manner on or through a medium customarily
|
||||
used for software exchange.
|
||||
|
||||
3.2. Modifications.
|
||||
|
||||
The Modifications that You create or to which You contribute are
|
||||
governed by the terms of this License. You represent that You
|
||||
believe Your Modifications are Your original creation(s) and/or
|
||||
You have sufficient rights to grant the rights conveyed by this
|
||||
License.
|
||||
|
||||
3.3. Required Notices.
|
||||
|
||||
You must include a notice in each of Your Modifications that
|
||||
identifies You as the Contributor of the Modification. You may
|
||||
not remove or alter any copyright, patent or trademark notices
|
||||
contained within the Covered Software, or any notices of licensing
|
||||
or any descriptive text giving attribution to any Contributor or
|
||||
the Initial Developer.
|
||||
|
||||
3.4. Application of Additional Terms.
|
||||
|
||||
You may not offer or impose any terms on any Covered Software in
|
||||
Source Code form that alters or restricts the applicable version
|
||||
of this License or the recipients' rights hereunder. You may
|
||||
choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of
|
||||
Covered Software. However, you may do so only on Your own behalf,
|
||||
and not on behalf of the Initial Developer or any Contributor.
|
||||
You must make it absolutely clear that any such warranty, support,
|
||||
indemnity or liability obligation is offered by You alone, and You
|
||||
hereby agree to indemnify the Initial Developer and every
|
||||
Contributor for any liability incurred by the Initial Developer or
|
||||
such Contributor as a result of warranty, support, indemnity or
|
||||
liability terms You offer.
|
||||
|
||||
3.5. Distribution of Executable Versions.
|
||||
|
||||
You may distribute the Executable form of the Covered Software
|
||||
under the terms of this License or under the terms of a license of
|
||||
Your choice, which may contain terms different from this License,
|
||||
provided that You are in compliance with the terms of this License
|
||||
and that the license for the Executable form does not attempt to
|
||||
limit or alter the recipient's rights in the Source Code form from
|
||||
the rights set forth in this License. If You distribute the
|
||||
Covered Software in Executable form under a different license, You
|
||||
must make it absolutely clear that any terms which differ from
|
||||
this License are offered by You alone, not by the Initial
|
||||
Developer or Contributor. You hereby agree to indemnify the
|
||||
Initial Developer and every Contributor for any liability incurred
|
||||
by the Initial Developer or such Contributor as a result of any
|
||||
such terms You offer.
|
||||
|
||||
3.6. Larger Works.
|
||||
|
||||
You may create a Larger Work by combining Covered Software with
|
||||
other code not governed by the terms of this License and
|
||||
distribute the Larger Work as a single product. In such a case,
|
||||
You must make sure the requirements of this License are fulfilled
|
||||
for the Covered Software.
|
||||
|
||||
4. Versions of the License.
|
||||
|
||||
4.1. New Versions.
|
||||
|
||||
Sun Microsystems, Inc. is the initial license steward and may
|
||||
publish revised and/or new versions of this License from time to
|
||||
time. Each version will be given a distinguishing version number.
|
||||
Except as provided in Section 4.3, no one other than the license
|
||||
steward has the right to modify this License.
|
||||
|
||||
4.2. Effect of New Versions.
|
||||
|
||||
You may always continue to use, distribute or otherwise make the
|
||||
Covered Software available under the terms of the version of the
|
||||
License under which You originally received the Covered Software.
|
||||
If the Initial Developer includes a notice in the Original
|
||||
Software prohibiting it from being distributed or otherwise made
|
||||
available under any subsequent version of the License, You must
|
||||
distribute and make the Covered Software available under the terms
|
||||
of the version of the License under which You originally received
|
||||
the Covered Software. Otherwise, You may also choose to use,
|
||||
distribute or otherwise make the Covered Software available under
|
||||
the terms of any subsequent version of the License published by
|
||||
the license steward.
|
||||
|
||||
4.3. Modified Versions.
|
||||
|
||||
When You are an Initial Developer and You want to create a new
|
||||
license for Your Original Software, You may create and use a
|
||||
modified version of this License if You: (a) rename the license
|
||||
and remove any references to the name of the license steward
|
||||
(except to note that the license differs from this License); and
|
||||
(b) otherwise make it clear that the license contains terms which
|
||||
differ from this License.
|
||||
|
||||
5. DISCLAIMER OF WARRANTY.
|
||||
|
||||
COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS"
|
||||
BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
|
||||
INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED
|
||||
SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR
|
||||
PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||
PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY
|
||||
COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE
|
||||
INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY
|
||||
NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF
|
||||
WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
|
||||
ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS
|
||||
DISCLAIMER.
|
||||
|
||||
6. TERMINATION.
|
||||
|
||||
6.1. This License and the rights granted hereunder will terminate
|
||||
automatically if You fail to comply with terms herein and fail to
|
||||
cure such breach within 30 days of becoming aware of the breach.
|
||||
Provisions which, by their nature, must remain in effect beyond
|
||||
the termination of this License shall survive.
|
||||
|
||||
6.2. If You assert a patent infringement claim (excluding
|
||||
declaratory judgment actions) against Initial Developer or a
|
||||
Contributor (the Initial Developer or Contributor against whom You
|
||||
assert such claim is referred to as "Participant") alleging that
|
||||
the Participant Software (meaning the Contributor Version where
|
||||
the Participant is a Contributor or the Original Software where
|
||||
the Participant is the Initial Developer) directly or indirectly
|
||||
infringes any patent, then any and all rights granted directly or
|
||||
indirectly to You by such Participant, the Initial Developer (if
|
||||
the Initial Developer is not the Participant) and all Contributors
|
||||
under Sections 2.1 and/or 2.2 of this License shall, upon 60 days
|
||||
notice from Participant terminate prospectively and automatically
|
||||
at the expiration of such 60 day notice period, unless if within
|
||||
such 60 day period You withdraw Your claim with respect to the
|
||||
Participant Software against such Participant either unilaterally
|
||||
or pursuant to a written agreement with Participant.
|
||||
|
||||
6.3. In the event of termination under Sections 6.1 or 6.2 above,
|
||||
all end user licenses that have been validly granted by You or any
|
||||
distributor hereunder prior to termination (excluding licenses
|
||||
granted to You by any distributor) shall survive termination.
|
||||
|
||||
7. LIMITATION OF LIABILITY.
|
||||
|
||||
UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
|
||||
(INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE
|
||||
INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF
|
||||
COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE
|
||||
LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR
|
||||
CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT
|
||||
LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK
|
||||
STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
|
||||
COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
|
||||
INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
|
||||
LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL
|
||||
INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT
|
||||
APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO
|
||||
NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT
|
||||
APPLY TO YOU.
|
||||
|
||||
8. U.S. GOVERNMENT END USERS.
|
||||
|
||||
The Covered Software is a "commercial item," as that term is
|
||||
defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial
|
||||
computer software" (as that term is defined at 48
|
||||
C.F.R. 252.227-7014(a)(1)) and "commercial computer software
|
||||
documentation" as such terms are used in 48 C.F.R. 12.212
|
||||
(Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48
|
||||
C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all
|
||||
U.S. Government End Users acquire Covered Software with only those
|
||||
rights set forth herein. This U.S. Government Rights clause is in
|
||||
lieu of, and supersedes, any other FAR, DFAR, or other clause or
|
||||
provision that addresses Government rights in computer software
|
||||
under this License.
|
||||
|
||||
9. MISCELLANEOUS.
|
||||
|
||||
This License represents the complete agreement concerning subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. This License shall be governed
|
||||
by the law of the jurisdiction specified in a notice contained
|
||||
within the Original Software (except to the extent applicable law,
|
||||
if any, provides otherwise), excluding such jurisdiction's
|
||||
conflict-of-law provisions. Any litigation relating to this
|
||||
License shall be subject to the jurisdiction of the courts located
|
||||
in the jurisdiction and venue specified in a notice contained
|
||||
within the Original Software, with the losing party responsible
|
||||
for costs, including, without limitation, court costs and
|
||||
reasonable attorneys' fees and expenses. The application of the
|
||||
United Nations Convention on Contracts for the International Sale
|
||||
of Goods is expressly excluded. Any law or regulation which
|
||||
provides that the language of a contract shall be construed
|
||||
against the drafter shall not apply to this License. You agree
|
||||
that You alone are responsible for compliance with the United
|
||||
States export administration regulations (and the export control
|
||||
laws and regulation of any other countries) when You use,
|
||||
distribute or otherwise make available any Covered Software.
|
||||
|
||||
10. RESPONSIBILITY FOR CLAIMS.
|
||||
|
||||
As between Initial Developer and the Contributors, each party is
|
||||
responsible for claims and damages arising, directly or
|
||||
indirectly, out of its utilization of rights under this License
|
||||
and You agree to work with Initial Developer and Contributors to
|
||||
distribute such responsibility on an equitable basis. Nothing
|
||||
herein is intended or shall be deemed to constitute any admission
|
||||
of liability.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND
|
||||
DISTRIBUTION LICENSE (CDDL)
|
||||
|
||||
For Covered Software in this distribution, this License shall
|
||||
be governed by the laws of the State of California (excluding
|
||||
conflict-of-law provisions).
|
||||
|
||||
Any litigation relating to this License shall be subject to the
|
||||
jurisdiction of the Federal Courts of the Northern District of
|
||||
California and the state courts of the State of California, with
|
||||
venue lying in Santa Clara County, California.
|
|
@ -0,0 +1,27 @@
|
|||
# getpasswd in Go [![GoDoc](https://godoc.org/github.com/howeyc/gopass?status.svg)](https://godoc.org/github.com/howeyc/gopass) [![Build Status](https://secure.travis-ci.org/howeyc/gopass.png?branch=master)](http://travis-ci.org/howeyc/gopass)
|
||||
|
||||
Retrieve password from user terminal or piped input without echo.
|
||||
|
||||
Verified on BSD, Linux, and Windows.
|
||||
|
||||
Example:
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import "github.com/howeyc/gopass"
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Password: ")
|
||||
|
||||
// Silent. For printing *'s use gopass.GetPasswdMasked()
|
||||
pass, err := gopass.GetPasswd()
|
||||
if err != nil {
|
||||
// Handle gopass.ErrInterrupted or getch() read error
|
||||
}
|
||||
|
||||
// Do something with pass
|
||||
}
|
||||
```
|
||||
|
||||
Caution: Multi-byte characters not supported!
|
|
@ -0,0 +1,110 @@
|
|||
package gopass
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type FdReader interface {
|
||||
io.Reader
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
var defaultGetCh = func(r io.Reader) (byte, error) {
|
||||
buf := make([]byte, 1)
|
||||
if n, err := r.Read(buf); n == 0 || err != nil {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
return buf[0], nil
|
||||
}
|
||||
|
||||
var (
|
||||
maxLength = 512
|
||||
ErrInterrupted = errors.New("interrupted")
|
||||
ErrMaxLengthExceeded = fmt.Errorf("maximum byte limit (%v) exceeded", maxLength)
|
||||
|
||||
// Provide variable so that tests can provide a mock implementation.
|
||||
getch = defaultGetCh
|
||||
)
|
||||
|
||||
// getPasswd returns the input read from terminal.
|
||||
// If prompt is not empty, it will be output as a prompt to the user
|
||||
// If masked is true, typing will be matched by asterisks on the screen.
|
||||
// Otherwise, typing will echo nothing.
|
||||
func getPasswd(prompt string, masked bool, r FdReader, w io.Writer) ([]byte, error) {
|
||||
var err error
|
||||
var pass, bs, mask []byte
|
||||
if masked {
|
||||
bs = []byte("\b \b")
|
||||
mask = []byte("*")
|
||||
}
|
||||
|
||||
if isTerminal(r.Fd()) {
|
||||
if oldState, err := makeRaw(r.Fd()); err != nil {
|
||||
return pass, err
|
||||
} else {
|
||||
defer func() {
|
||||
restore(r.Fd(), oldState)
|
||||
fmt.Fprintln(w)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if prompt != "" {
|
||||
fmt.Fprint(w, prompt)
|
||||
}
|
||||
|
||||
// Track total bytes read, not just bytes in the password. This ensures any
|
||||
// errors that might flood the console with nil or -1 bytes infinitely are
|
||||
// capped.
|
||||
var counter int
|
||||
for counter = 0; counter <= maxLength; counter++ {
|
||||
if v, e := getch(r); e != nil {
|
||||
err = e
|
||||
break
|
||||
} else if v == 127 || v == 8 {
|
||||
if l := len(pass); l > 0 {
|
||||
pass = pass[:l-1]
|
||||
fmt.Fprint(w, string(bs))
|
||||
}
|
||||
} else if v == 13 || v == 10 {
|
||||
break
|
||||
} else if v == 3 {
|
||||
err = ErrInterrupted
|
||||
break
|
||||
} else if v != 0 {
|
||||
pass = append(pass, v)
|
||||
fmt.Fprint(w, string(mask))
|
||||
}
|
||||
}
|
||||
|
||||
if counter > maxLength {
|
||||
err = ErrMaxLengthExceeded
|
||||
}
|
||||
|
||||
return pass, err
|
||||
}
|
||||
|
||||
// GetPasswd returns the password read from the terminal without echoing input.
|
||||
// The returned byte array does not include end-of-line characters.
|
||||
func GetPasswd() ([]byte, error) {
|
||||
return getPasswd("", false, os.Stdin, os.Stdout)
|
||||
}
|
||||
|
||||
// GetPasswdMasked returns the password read from the terminal, echoing asterisks.
|
||||
// The returned byte array does not include end-of-line characters.
|
||||
func GetPasswdMasked() ([]byte, error) {
|
||||
return getPasswd("", true, os.Stdin, os.Stdout)
|
||||
}
|
||||
|
||||
// GetPasswdPrompt prompts the user and returns the password read from the terminal.
|
||||
// If mask is true, then asterisks are echoed.
|
||||
// The returned byte array does not include end-of-line characters.
|
||||
func GetPasswdPrompt(prompt string, mask bool, r FdReader, w io.Writer) ([]byte, error) {
|
||||
return getPasswd(prompt, mask, r, w)
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
package gopass
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestGetPasswd tests the password creation and output based on a byte buffer
|
||||
// as input to mock the underlying getch() methods.
|
||||
func TestGetPasswd(t *testing.T) {
|
||||
type testData struct {
|
||||
input []byte
|
||||
|
||||
// Due to how backspaces are written, it is easier to manually write
|
||||
// each expected output for the masked cases.
|
||||
masked string
|
||||
password string
|
||||
byesLeft int
|
||||
reason string
|
||||
}
|
||||
|
||||
ds := []testData{
|
||||
testData{[]byte("abc\n"), "***", "abc", 0, "Password parsing should stop at \\n"},
|
||||
testData{[]byte("abc\r"), "***", "abc", 0, "Password parsing should stop at \\r"},
|
||||
testData{[]byte("a\nbc\n"), "*", "a", 3, "Password parsing should stop at \\n"},
|
||||
testData{[]byte("*!]|\n"), "****", "*!]|", 0, "Special characters shouldn't affect the password."},
|
||||
|
||||
testData{[]byte("abc\r\n"), "***", "abc", 1,
|
||||
"Password parsing should stop at \\r; Windows LINE_MODE should be unset so \\r is not converted to \\r\\n."},
|
||||
|
||||
testData{[]byte{'a', 'b', 'c', 8, '\n'}, "***\b \b", "ab", 0, "Backspace byte should remove the last read byte."},
|
||||
testData{[]byte{'a', 'b', 127, 'c', '\n'}, "**\b \b*", "ac", 0, "Delete byte should remove the last read byte."},
|
||||
testData{[]byte{'a', 'b', 127, 'c', 8, 127, '\n'}, "**\b \b*\b \b\b \b", "", 0, "Successive deletes continue to delete."},
|
||||
testData{[]byte{8, 8, 8, '\n'}, "", "", 0, "Deletes before characters are noops."},
|
||||
testData{[]byte{8, 8, 8, 'a', 'b', 'c', '\n'}, "***", "abc", 0, "Deletes before characters are noops."},
|
||||
|
||||
testData{[]byte{'a', 'b', 0, 'c', '\n'}, "***", "abc", 0,
|
||||
"Nil byte should be ignored due; may get unintended nil bytes from syscalls on Windows."},
|
||||
}
|
||||
|
||||
// Redirecting output for tests as they print to os.Stdout but we want to
|
||||
// capture and test the output.
|
||||
for _, masked := range []bool{true, false} {
|
||||
for _, d := range ds {
|
||||
pipeBytesToStdin(d.input)
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
result, err := getPasswd("", masked, os.Stdin, w)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting password: %s", err.Error())
|
||||
}
|
||||
leftOnBuffer := flushStdin()
|
||||
|
||||
// Test output (masked and unmasked). Delete/backspace actually
|
||||
// deletes, overwrites and deletes again. As a result, we need to
|
||||
// remove those from the pipe afterwards to mimic the console's
|
||||
// interpretation of those bytes.
|
||||
w.Close()
|
||||
output, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
var expectedOutput []byte
|
||||
if masked {
|
||||
expectedOutput = []byte(d.masked)
|
||||
} else {
|
||||
expectedOutput = []byte("")
|
||||
}
|
||||
if bytes.Compare(expectedOutput, output) != 0 {
|
||||
t.Errorf("Expected output to equal %v (%q) but got %v (%q) instead when masked=%v. %s", expectedOutput, string(expectedOutput), output, string(output), masked, d.reason)
|
||||
}
|
||||
|
||||
if string(result) != d.password {
|
||||
t.Errorf("Expected %q but got %q instead when masked=%v. %s", d.password, result, masked, d.reason)
|
||||
}
|
||||
|
||||
if leftOnBuffer != d.byesLeft {
|
||||
t.Errorf("Expected %v bytes left on buffer but instead got %v when masked=%v. %s", d.byesLeft, leftOnBuffer, masked, d.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPipe ensures we get our expected pipe behavior.
|
||||
func TestPipe(t *testing.T) {
|
||||
type testData struct {
|
||||
input string
|
||||
password string
|
||||
expError error
|
||||
}
|
||||
ds := []testData{
|
||||
testData{"abc", "abc", io.EOF},
|
||||
testData{"abc\n", "abc", nil},
|
||||
testData{"abc\r", "abc", nil},
|
||||
testData{"abc\r\n", "abc", nil},
|
||||
}
|
||||
|
||||
for _, d := range ds {
|
||||
_, err := pipeToStdin(d.input)
|
||||
if err != nil {
|
||||
t.Log("Error writing input to stdin:", err)
|
||||
t.FailNow()
|
||||
}
|
||||
pass, err := GetPasswd()
|
||||
if string(pass) != d.password {
|
||||
t.Errorf("Expected %q but got %q instead.", d.password, string(pass))
|
||||
}
|
||||
if err != d.expError {
|
||||
t.Errorf("Expected %v but got %q instead.", d.expError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushStdin reads from stdin for .5 seconds to ensure no bytes are left on
|
||||
// the buffer. Returns the number of bytes read.
|
||||
func flushStdin() int {
|
||||
ch := make(chan byte)
|
||||
go func(ch chan byte) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
b, err := reader.ReadByte()
|
||||
if err != nil { // Maybe log non io.EOF errors, if you want
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
ch <- b
|
||||
}
|
||||
close(ch)
|
||||
}(ch)
|
||||
|
||||
numBytes := 0
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-ch:
|
||||
if !ok {
|
||||
return numBytes
|
||||
}
|
||||
numBytes++
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
return numBytes
|
||||
}
|
||||
}
|
||||
return numBytes
|
||||
}
|
||||
|
||||
// pipeToStdin pipes the given string onto os.Stdin by replacing it with an
|
||||
// os.Pipe. The write end of the pipe is closed so that EOF is read after the
|
||||
// final byte.
|
||||
func pipeToStdin(s string) (int, error) {
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting os pipes:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdin = pipeReader
|
||||
w, err := pipeWriter.WriteString(s)
|
||||
pipeWriter.Close()
|
||||
return w, err
|
||||
}
|
||||
|
||||
func pipeBytesToStdin(b []byte) (int, error) {
|
||||
return pipeToStdin(string(b))
|
||||
}
|
||||
|
||||
// TestGetPasswd_Err tests errors are properly handled from getch()
|
||||
func TestGetPasswd_Err(t *testing.T) {
|
||||
var inBuffer *bytes.Buffer
|
||||
getch = func(io.Reader) (byte, error) {
|
||||
b, err := inBuffer.ReadByte()
|
||||
if err != nil {
|
||||
return 13, err
|
||||
}
|
||||
if b == 'z' {
|
||||
return 'z', fmt.Errorf("Forced error; byte returned should not be considered accurate.")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
defer func() { getch = defaultGetCh }()
|
||||
|
||||
for input, expectedPassword := range map[string]string{"abc": "abc", "abzc": "ab"} {
|
||||
inBuffer = bytes.NewBufferString(input)
|
||||
p, err := GetPasswdMasked()
|
||||
if string(p) != expectedPassword {
|
||||
t.Errorf("Expected %q but got %q instead.", expectedPassword, p)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("Expected error to be returned.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxPasswordLength(t *testing.T) {
|
||||
type testData struct {
|
||||
input []byte
|
||||
expectedErr error
|
||||
|
||||
// Helper field to output in case of failure; rather than hundreds of
|
||||
// bytes.
|
||||
inputDesc string
|
||||
}
|
||||
|
||||
ds := []testData{
|
||||
testData{append(bytes.Repeat([]byte{'a'}, maxLength), '\n'), nil, fmt.Sprintf("%v 'a' bytes followed by a newline", maxLength)},
|
||||
testData{append(bytes.Repeat([]byte{'a'}, maxLength+1), '\n'), ErrMaxLengthExceeded, fmt.Sprintf("%v 'a' bytes followed by a newline", maxLength+1)},
|
||||
testData{append(bytes.Repeat([]byte{0x00}, maxLength+1), '\n'), ErrMaxLengthExceeded, fmt.Sprintf("%v 0x00 bytes followed by a newline", maxLength+1)},
|
||||
}
|
||||
|
||||
for _, d := range ds {
|
||||
pipeBytesToStdin(d.input)
|
||||
_, err := GetPasswd()
|
||||
if err != d.expectedErr {
|
||||
t.Errorf("Expected error to be %v; isntead got %v from %v", d.expectedErr, err, d.inputDesc)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// +build !solaris
|
||||
|
||||
package gopass
|
||||
|
||||
import "golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
type terminalState struct {
|
||||
state *terminal.State
|
||||
}
|
||||
|
||||
func isTerminal(fd uintptr) bool {
|
||||
return terminal.IsTerminal(int(fd))
|
||||
}
|
||||
|
||||
func makeRaw(fd uintptr) (*terminalState, error) {
|
||||
state, err := terminal.MakeRaw(int(fd))
|
||||
|
||||
return &terminalState{
|
||||
state: state,
|
||||
}, err
|
||||
}
|
||||
|
||||
func restore(fd uintptr, oldState *terminalState) error {
|
||||
return terminal.Restore(int(fd), oldState.state)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* CDDL HEADER START
|
||||
*
|
||||
* The contents of this file are subject to the terms of the
|
||||
* Common Development and Distribution License, Version 1.0 only
|
||||
* (the "License"). You may not use this file except in compliance
|
||||
* with the License.
|
||||
*
|
||||
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
|
||||
* or http://www.opensolaris.org/os/licensing.
|
||||
* See the License for the specific language governing permissions
|
||||
* and limitations under the License.
|
||||
*
|
||||
* When distributing Covered Code, include this CDDL HEADER in each
|
||||
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
|
||||
* If applicable, add the following below this CDDL HEADER, with the
|
||||
* fields enclosed by brackets "[]" replaced with your own identifying
|
||||
* information: Portions Copyright [yyyy] [name of copyright owner]
|
||||
*
|
||||
* CDDL HEADER END
|
||||
*/
|
||||
// Below is derived from Solaris source, so CDDL license is included.
|
||||
|
||||
package gopass
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type terminalState struct {
|
||||
state *unix.Termios
|
||||
}
|
||||
|
||||
// isTerminal returns true if there is a terminal attached to the given
|
||||
// file descriptor.
|
||||
// Source: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c
|
||||
func isTerminal(fd uintptr) bool {
|
||||
var termio unix.Termio
|
||||
err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// makeRaw puts the terminal connected to the given file descriptor into raw
|
||||
// mode and returns the previous state of the terminal so that it can be
|
||||
// restored.
|
||||
// Source: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c
|
||||
func makeRaw(fd uintptr) (*terminalState, error) {
|
||||
oldTermiosPtr, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldTermios := *oldTermiosPtr
|
||||
|
||||
newTermios := oldTermios
|
||||
newTermios.Lflag &^= syscall.ECHO | syscall.ECHOE | syscall.ECHOK | syscall.ECHONL
|
||||
if err := unix.IoctlSetTermios(int(fd), unix.TCSETS, &newTermios); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &terminalState{
|
||||
state: oldTermiosPtr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func restore(fd uintptr, oldState *terminalState) error {
|
||||
return unix.IoctlSetTermios(int(fd), unix.TCSETS, oldState.state)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (C) 2014 Kevin Ballard
|
||||
|
||||
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.
|
|
@ -0,0 +1,36 @@
|
|||
PACKAGE
|
||||
|
||||
package shellquote
|
||||
import "github.com/kballard/go-shellquote"
|
||||
|
||||
Shellquote provides utilities for joining/splitting strings using sh's
|
||||
word-splitting rules.
|
||||
|
||||
VARIABLES
|
||||
|
||||
var (
|
||||
UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
|
||||
UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
|
||||
UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
|
||||
)
|
||||
|
||||
|
||||
FUNCTIONS
|
||||
|
||||
func Join(args ...string) string
|
||||
Join quotes each argument and joins them with a space. If passed to
|
||||
/bin/sh, the resulting string will be split back into the original
|
||||
arguments.
|
||||
|
||||
func Split(input string) (words []string, err error)
|
||||
Split splits a string according to /bin/sh's word-splitting rules. It
|
||||
supports backslash-escapes, single-quotes, and double-quotes. Notably it
|
||||
does not support the $'' style of quoting. It also doesn't attempt to
|
||||
perform any other sort of expansion, including brace expansion, shell
|
||||
expansion, or pathname expansion.
|
||||
|
||||
If the given input has an unterminated quoted string or ends in a
|
||||
backslash-escape, one of UnterminatedSingleQuoteError,
|
||||
UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package shellquote
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
// this is called bothtest because it tests Split and Join together
|
||||
|
||||
func TestJoinSplit(t *testing.T) {
|
||||
f := func(strs []string) bool {
|
||||
// Join, then split, the input
|
||||
combined := Join(strs...)
|
||||
split, err := Split(combined)
|
||||
if err != nil {
|
||||
t.Logf("Error splitting %#v: %v", combined, err)
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(strs, split) {
|
||||
t.Logf("Input %q did not match output %q", strs, split)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if err := quick.Check(f, nil); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Shellquote provides utilities for joining/splitting strings using sh's
|
||||
// word-splitting rules.
|
||||
package shellquote
|
|
@ -0,0 +1,102 @@
|
|||
package shellquote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Join quotes each argument and joins them with a space.
|
||||
// If passed to /bin/sh, the resulting string will be split back into the
|
||||
// original arguments.
|
||||
func Join(args ...string) string {
|
||||
var buf bytes.Buffer
|
||||
for i, arg := range args {
|
||||
if i != 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
quote(arg, &buf)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
const (
|
||||
specialChars = "\\'\"`${[|&;<>()*?!"
|
||||
extraSpecialChars = " \t\n"
|
||||
prefixChars = "~"
|
||||
)
|
||||
|
||||
func quote(word string, buf *bytes.Buffer) {
|
||||
// We want to try to produce a "nice" output. As such, we will
|
||||
// backslash-escape most characters, but if we encounter a space, or if we
|
||||
// encounter an extra-special char (which doesn't work with
|
||||
// backslash-escaping) we switch over to quoting the whole word. We do this
|
||||
// with a space because it's typically easier for people to read multi-word
|
||||
// arguments when quoted with a space rather than with ugly backslashes
|
||||
// everywhere.
|
||||
origLen := buf.Len()
|
||||
|
||||
if len(word) == 0 {
|
||||
// oops, no content
|
||||
buf.WriteString("''")
|
||||
return
|
||||
}
|
||||
|
||||
cur, prev := word, word
|
||||
atStart := true
|
||||
for len(cur) > 0 {
|
||||
c, l := utf8.DecodeRuneInString(cur)
|
||||
cur = cur[l:]
|
||||
if strings.ContainsRune(specialChars, c) || (atStart && strings.ContainsRune(prefixChars, c)) {
|
||||
// copy the non-special chars up to this point
|
||||
if len(cur) < len(prev) {
|
||||
buf.WriteString(prev[0 : len(prev)-len(cur)-l])
|
||||
}
|
||||
buf.WriteByte('\\')
|
||||
buf.WriteRune(c)
|
||||
prev = cur
|
||||
} else if strings.ContainsRune(extraSpecialChars, c) {
|
||||
// start over in quote mode
|
||||
buf.Truncate(origLen)
|
||||
goto quote
|
||||
}
|
||||
atStart = false
|
||||
}
|
||||
if len(prev) > 0 {
|
||||
buf.WriteString(prev)
|
||||
}
|
||||
return
|
||||
|
||||
quote:
|
||||
// quote mode
|
||||
// Use single-quotes, but if we find a single-quote in the word, we need
|
||||
// to terminate the string, emit an escaped quote, and start the string up
|
||||
// again
|
||||
inQuote := false
|
||||
for len(word) > 0 {
|
||||
i := strings.IndexRune(word, '\'')
|
||||
if i == -1 {
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
if !inQuote {
|
||||
buf.WriteByte('\'')
|
||||
inQuote = true
|
||||
}
|
||||
buf.WriteString(word[0:i])
|
||||
}
|
||||
word = word[i+1:]
|
||||
if inQuote {
|
||||
buf.WriteByte('\'')
|
||||
inQuote = false
|
||||
}
|
||||
buf.WriteString("\\'")
|
||||
}
|
||||
if len(word) > 0 {
|
||||
if !inQuote {
|
||||
buf.WriteByte('\'')
|
||||
}
|
||||
buf.WriteString(word)
|
||||
buf.WriteByte('\'')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package shellquote
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSimpleJoin(t *testing.T) {
|
||||
for _, elem := range simpleJoinTest {
|
||||
output := Join(elem.input...)
|
||||
if output != elem.output {
|
||||
t.Errorf("Input %q, got %q, expected %q", elem.input, output, elem.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var simpleJoinTest = []struct {
|
||||
input []string
|
||||
output string
|
||||
}{
|
||||
{[]string{"test"}, "test"},
|
||||
{[]string{"hello goodbye"}, "'hello goodbye'"},
|
||||
{[]string{"hello", "goodbye"}, "hello goodbye"},
|
||||
{[]string{"don't you know the dewey decimal system?"}, "'don'\\''t you know the dewey decimal system?'"},
|
||||
{[]string{"don't", "you", "know", "the", "dewey", "decimal", "system?"}, "don\\'t you know the dewey decimal system\\?"},
|
||||
{[]string{"~user", "u~ser", " ~user", "!~user"}, "\\~user u~ser ' ~user' \\!~user"},
|
||||
{[]string{"foo*", "M{ovies,usic}", "ab[cd]", "%3"}, "foo\\* M\\{ovies,usic} ab\\[cd] %3"},
|
||||
{[]string{"one", "", "three"}, "one '' three"},
|
||||
{[]string{"some(parentheses)"}, "some\\(parentheses\\)"},
|
||||
{[]string{"$some_ot~her_)spe!cial_*_characters"}, "\\$some_ot~her_\\)spe\\!cial_\\*_characters"},
|
||||
{[]string{"' "}, "\\'' '"},
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package shellquote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
|
||||
UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
|
||||
UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
|
||||
)
|
||||
|
||||
var (
|
||||
splitChars = " \n\t"
|
||||
singleChar = '\''
|
||||
doubleChar = '"'
|
||||
escapeChar = '\\'
|
||||
doubleEscapeChars = "$`\"\n\\"
|
||||
)
|
||||
|
||||
// Split splits a string according to /bin/sh's word-splitting rules. It
|
||||
// supports backslash-escapes, single-quotes, and double-quotes. Notably it does
|
||||
// not support the $'' style of quoting. It also doesn't attempt to perform any
|
||||
// other sort of expansion, including brace expansion, shell expansion, or
|
||||
// pathname expansion.
|
||||
//
|
||||
// If the given input has an unterminated quoted string or ends in a
|
||||
// backslash-escape, one of UnterminatedSingleQuoteError,
|
||||
// UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
|
||||
func Split(input string) (words []string, err error) {
|
||||
var buf bytes.Buffer
|
||||
words = make([]string, 0)
|
||||
|
||||
for len(input) > 0 {
|
||||
// skip any splitChars at the start
|
||||
c, l := utf8.DecodeRuneInString(input)
|
||||
if strings.ContainsRune(splitChars, c) {
|
||||
input = input[l:]
|
||||
continue
|
||||
}
|
||||
|
||||
var word string
|
||||
word, input, err = splitWord(input, &buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
|
||||
buf.Reset()
|
||||
|
||||
raw:
|
||||
{
|
||||
cur := input
|
||||
for len(cur) > 0 {
|
||||
c, l := utf8.DecodeRuneInString(cur)
|
||||
cur = cur[l:]
|
||||
if c == singleChar {
|
||||
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||
input = cur
|
||||
goto single
|
||||
} else if c == doubleChar {
|
||||
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||
input = cur
|
||||
goto double
|
||||
} else if c == escapeChar {
|
||||
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||
input = cur
|
||||
goto escape
|
||||
} else if strings.ContainsRune(splitChars, c) {
|
||||
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||
return buf.String(), cur, nil
|
||||
}
|
||||
}
|
||||
if len(input) > 0 {
|
||||
buf.WriteString(input)
|
||||
input = ""
|
||||
}
|
||||
goto done
|
||||
}
|
||||
|
||||
escape:
|
||||
{
|
||||
if len(input) == 0 {
|
||||
return "", "", UnterminatedEscapeError
|
||||
}
|
||||
c, l := utf8.DecodeRuneInString(input)
|
||||
if c == '\n' {
|
||||
// a backslash-escaped newline is elided from the output entirely
|
||||
} else {
|
||||
buf.WriteString(input[:l])
|
||||
}
|
||||
input = input[l:]
|
||||
}
|
||||
goto raw
|
||||
|
||||
single:
|
||||
{
|
||||
i := strings.IndexRune(input, singleChar)
|
||||
if i == -1 {
|
||||
return "", "", UnterminatedSingleQuoteError
|
||||
}
|
||||
buf.WriteString(input[0:i])
|
||||
input = input[i+1:]
|
||||
goto raw
|
||||
}
|
||||
|
||||
double:
|
||||
{
|
||||
cur := input
|
||||
for len(cur) > 0 {
|
||||
c, l := utf8.DecodeRuneInString(cur)
|
||||
cur = cur[l:]
|
||||
if c == doubleChar {
|
||||
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||
input = cur
|
||||
goto raw
|
||||
} else if c == escapeChar {
|
||||
// bash only supports certain escapes in double-quoted strings
|
||||
c2, l2 := utf8.DecodeRuneInString(cur)
|
||||
cur = cur[l2:]
|
||||
if strings.ContainsRune(doubleEscapeChars, c2) {
|
||||
buf.WriteString(input[0 : len(input)-len(cur)-l-l2])
|
||||
if c2 == '\n' {
|
||||
// newline is special, skip the backslash entirely
|
||||
} else {
|
||||
buf.WriteRune(c2)
|
||||
}
|
||||
input = cur
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", "", UnterminatedDoubleQuoteError
|
||||
}
|
||||
|
||||
done:
|
||||
return buf.String(), input, nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package shellquote
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSimpleSplit(t *testing.T) {
|
||||
for _, elem := range simpleSplitTest {
|
||||
output, err := Split(elem.input)
|
||||
if err != nil {
|
||||
t.Errorf("Input %q, got error %#v", elem.input, err)
|
||||
} else if !reflect.DeepEqual(output, elem.output) {
|
||||
t.Errorf("Input %q, got %q, expected %q", elem.input, output, elem.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorSplit(t *testing.T) {
|
||||
for _, elem := range errorSplitTest {
|
||||
_, err := Split(elem.input)
|
||||
if err != elem.error {
|
||||
t.Errorf("Input %q, got error %#v, expected error %#v", elem.input, err, elem.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var simpleSplitTest = []struct {
|
||||
input string
|
||||
output []string
|
||||
}{
|
||||
{"hello", []string{"hello"}},
|
||||
{"hello goodbye", []string{"hello", "goodbye"}},
|
||||
{"hello goodbye", []string{"hello", "goodbye"}},
|
||||
{"glob* test?", []string{"glob*", "test?"}},
|
||||
{"don\\'t you know the dewey decimal system\\?", []string{"don't", "you", "know", "the", "dewey", "decimal", "system?"}},
|
||||
{"'don'\\''t you know the dewey decimal system?'", []string{"don't you know the dewey decimal system?"}},
|
||||
{"one '' two", []string{"one", "", "two"}},
|
||||
{"text with\\\na backslash-escaped newline", []string{"text", "witha", "backslash-escaped", "newline"}},
|
||||
{"text \"with\na\" quoted newline", []string{"text", "with\na", "quoted", "newline"}},
|
||||
{"\"quoted\\d\\\\\\\" text with\\\na backslash-escaped newline\"", []string{"quoted\\d\\\" text witha backslash-escaped newline"}},
|
||||
{"foo\"bar\"baz", []string{"foobarbaz"}},
|
||||
}
|
||||
|
||||
var errorSplitTest = []struct {
|
||||
input string
|
||||
error error
|
||||
}{
|
||||
{"don't worry", UnterminatedSingleQuoteError},
|
||||
{"'test'\\''ing", UnterminatedSingleQuoteError},
|
||||
{"\"foo'bar", UnterminatedDoubleQuoteError},
|
||||
{"foo\\", UnterminatedEscapeError},
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
.tags*
|
||||
._*
|
||||
.vagrant
|
||||
*.iml
|
||||
|
||||
!.gitkeep
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Kohki Makimoto
|
||||
|
||||
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.
|
|
@ -0,0 +1,75 @@
|
|||
# gluaenv
|
||||
|
||||
Utility package for manipulating environment variables for [gopher-lua](https://github.com/yuin/gopher-lua)
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/kohkimakimoto/gluaenv
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `env.set(key, value)`
|
||||
|
||||
Same `os.setenv`
|
||||
|
||||
### `env.get(key)`
|
||||
|
||||
Same `os.getenv`
|
||||
|
||||
### `env.loadfile(file)`
|
||||
|
||||
Loads environment variables from a file. The file is as the following:
|
||||
|
||||
```
|
||||
AAA=BBB
|
||||
CCC=DDD
|
||||
```
|
||||
|
||||
If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/kohkimakimoto/gluaenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("env", gluaenv.Loader)
|
||||
if err := L.DoString(`
|
||||
local env = require("env")
|
||||
|
||||
-- set a environment variable
|
||||
env.set("HOGE_KEY", "HOGE_VALUE")
|
||||
|
||||
-- get a environment variable
|
||||
local v = env.get("HOGE_KEY")
|
||||
|
||||
-- load envrironment variables from a file.
|
||||
env.loadfile("path/to/.env")
|
||||
|
||||
-- file example
|
||||
-- AAA=BBB
|
||||
-- CCC=DDD
|
||||
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Kohki Makimoto <kohki.makimoto@gmail.com>
|
||||
|
||||
## License
|
||||
|
||||
MIT license.
|
|
@ -0,0 +1,193 @@
|
|||
package gluaenv
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"os"
|
||||
"bufio"
|
||||
"strings"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func Loader(L *lua.LState) int {
|
||||
tb := L.NewTable()
|
||||
L.SetFuncs(tb, map[string]lua.LGFunction{
|
||||
"set": envSet,
|
||||
"get": envGet,
|
||||
"loadfile": envLoadFile,
|
||||
})
|
||||
L.Push(tb)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func envSet(L *lua.LState) int {
|
||||
// same github.com/yuin/gopher-lua/oslib.go
|
||||
err := os.Setenv(L.CheckString(1), L.CheckString(2))
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
} else {
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func envGet(L *lua.LState) int {
|
||||
// same github.com/yuin/gopher-lua/oslib.go
|
||||
v := os.Getenv(L.CheckString(1))
|
||||
if len(v) == 0 {
|
||||
L.Push(lua.LNil)
|
||||
} else {
|
||||
L.Push(lua.LString(v))
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func envLoadFile(L *lua.LState) int {
|
||||
if err := loadFile(L.CheckString(1)); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
} else {
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// loadFile' s code is highly inspired by https://github.com/joho/godotenv/blob/master/godotenv.go
|
||||
//Copyright (c) 2013 John Barton
|
||||
//
|
||||
//MIT License
|
||||
//
|
||||
//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.
|
||||
|
||||
func loadFile(filename string) error {
|
||||
envMap, err := readFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, value := range envMap {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFile(filename string) (envMap map[string]string, err error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
envMap = make(map[string]string)
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
|
||||
for _, fullLine := range lines {
|
||||
if !isIgnoredLine(fullLine) {
|
||||
key, value, err := parseLine(fullLine)
|
||||
|
||||
if err == nil {
|
||||
envMap[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseLine(line string) (key string, value string, err error) {
|
||||
if len(line) == 0 {
|
||||
err = errors.New("zero length string")
|
||||
return
|
||||
}
|
||||
|
||||
// ditch the comments (but keep quoted hashes)
|
||||
if strings.Contains(line, "#") {
|
||||
segmentsBetweenHashes := strings.Split(line, "#")
|
||||
quotesAreOpen := false
|
||||
var segmentsToKeep []string
|
||||
for _, segment := range segmentsBetweenHashes {
|
||||
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
|
||||
if quotesAreOpen {
|
||||
quotesAreOpen = false
|
||||
segmentsToKeep = append(segmentsToKeep, segment)
|
||||
} else {
|
||||
quotesAreOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(segmentsToKeep) == 0 || quotesAreOpen {
|
||||
segmentsToKeep = append(segmentsToKeep, segment)
|
||||
}
|
||||
}
|
||||
|
||||
line = strings.Join(segmentsToKeep, "#")
|
||||
}
|
||||
|
||||
// now split key from value
|
||||
splitString := strings.SplitN(line, "=", 2)
|
||||
|
||||
if len(splitString) != 2 {
|
||||
// try yaml mode!
|
||||
splitString = strings.SplitN(line, ":", 2)
|
||||
}
|
||||
|
||||
if len(splitString) != 2 {
|
||||
err = errors.New("Can't separate key from value")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the key
|
||||
key = splitString[0]
|
||||
if strings.HasPrefix(key, "export") {
|
||||
key = strings.TrimPrefix(key, "export")
|
||||
}
|
||||
key = strings.Trim(key, " ")
|
||||
|
||||
// Parse the value
|
||||
value = splitString[1]
|
||||
// trim
|
||||
value = strings.Trim(value, " ")
|
||||
|
||||
// check if we've got quoted values
|
||||
if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 {
|
||||
// pull the quotes off the edges
|
||||
value = strings.Trim(value, "\"'")
|
||||
|
||||
// expand quotes
|
||||
value = strings.Replace(value, "\\\"", "\"", -1)
|
||||
// expand newlines
|
||||
value = strings.Replace(value, "\\n", "\n", -1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func isIgnoredLine(line string) bool {
|
||||
trimmedLine := strings.Trim(line, " \n\t")
|
||||
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package gluaenv
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"testing"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func TestSetAndGet(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("env", Loader)
|
||||
if err := L.DoString(`
|
||||
local env = require("env")
|
||||
|
||||
env.set("HOGE_KEY", "HOGE_VALUE")
|
||||
|
||||
local v = env.get("HOGE_KEY")
|
||||
|
||||
assert(v == "HOGE_VALUE")
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
var sampleFile = `
|
||||
AAA=hogehoge
|
||||
BBB=bbbbbbbb
|
||||
|
||||
# CCC=eeeeee
|
||||
|
||||
DDD=ddddddd
|
||||
`
|
||||
|
||||
func TestLoadfile(t *testing.T) {
|
||||
tmpFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
if err = ioutil.WriteFile(tmpFile.Name(), []byte(sampleFile), 0644); err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFile.Name())
|
||||
}()
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.PreloadModule("env", Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
local env = require("env")
|
||||
env.loadfile("` + tmpFile.Name() + `")
|
||||
|
||||
assert(env.get("AAA") == "hogehoge")
|
||||
assert(env.get("BBB") == "bbbbbbbb")
|
||||
assert(env.get("CCC") == nil)
|
||||
assert(env.get("DDD") == "ddddddd")
|
||||
|
||||
r1, r2 = env.loadfile("` + tmpFile.Name() + `.notfound_file")
|
||||
assert(r1 == nil)
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
.tags*
|
||||
._*
|
||||
*.iml
|
|
@ -0,0 +1,108 @@
|
|||
# gluafs
|
||||
|
||||
filesystem utility for [gopher-lua](https://github.com/yuin/gopher-lua). This project is inspired by [layeh/gopher-lfs](https://github.com/layeh/gopher-lfs).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/kohkimakimoto/gluafs
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `fs.exists(file)`
|
||||
|
||||
Returns true if the file exists.
|
||||
|
||||
### `fs.read(file)`
|
||||
|
||||
Reads file content and return it. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.write(file, content, [mode])`
|
||||
|
||||
Writes content to the file. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.mkdir(path, [mode, recursive])`
|
||||
|
||||
Create directory. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.remove(path, [recursive])`
|
||||
|
||||
Remove path. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.symlink(target, link)`
|
||||
|
||||
Create symbolic link. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.dirname(path)`
|
||||
|
||||
Returns all but the last element of path.
|
||||
|
||||
### `fs.basename(path)`
|
||||
|
||||
Returns the last element of path.
|
||||
|
||||
### `fs.realpath(path)`
|
||||
|
||||
Returns the real path of a given path in the os. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.getcwd()`
|
||||
|
||||
Returns the current working directory. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.chdir(path)`
|
||||
|
||||
Changes the current working directory. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.file()`
|
||||
|
||||
Returns the script file path. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.dir()`
|
||||
|
||||
Returns the directory path that is parent of the script file. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `fs.glob(pattern, function)`
|
||||
|
||||
Run the callback function with the files matching pattern. See below example:
|
||||
|
||||
```lua
|
||||
local fs = require("fs")
|
||||
local ret, err = fs.glob("/tmp/*", function(file)
|
||||
print(file.path)
|
||||
print(file.realpath)
|
||||
end)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/kohkimakimoto/gluafs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("fs", gluafs.Loader)
|
||||
if err := L.DoString(`
|
||||
local fs = require("fs")
|
||||
local ret = fs.exists("path/to/file")
|
||||
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Kohki Makimoto <kohki.makimoto@gmail.com>
|
||||
|
||||
## License
|
||||
|
||||
MIT license.
|
|
@ -0,0 +1,327 @@
|
|||
package gluafs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/yookoala/realpath"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Loader(L *lua.LState) int {
|
||||
tb := L.NewTable()
|
||||
L.SetFuncs(tb, map[string]lua.LGFunction{
|
||||
"exists": exists,
|
||||
"read": read,
|
||||
"write": write,
|
||||
"mkdir": mkdir,
|
||||
"remove": remove,
|
||||
"symlink": symlink,
|
||||
"dirname": dirname,
|
||||
"basename": basename,
|
||||
"realpath": fnRealpath,
|
||||
"getcwd": getcwd,
|
||||
"chdir": chdir,
|
||||
"file": file,
|
||||
"dir": dir,
|
||||
"glob": glob,
|
||||
})
|
||||
L.Push(tb)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func exists(L *lua.LState) int {
|
||||
var ret bool
|
||||
|
||||
path := L.CheckString(1)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
ret = false
|
||||
} else {
|
||||
ret = true
|
||||
}
|
||||
|
||||
L.Push(lua.LBool(ret))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func read(L *lua.LState) int {
|
||||
path := L.CheckString(1)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(string(content)))
|
||||
return 1
|
||||
}
|
||||
|
||||
func write(L *lua.LState) int {
|
||||
p := L.CheckString(1)
|
||||
content := []byte(L.CheckString(2))
|
||||
var mode os.FileMode = 0666
|
||||
|
||||
top := L.GetTop()
|
||||
if top == 3 {
|
||||
m, err := oct2decimal(L.CheckInt(3))
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
mode = os.FileMode(m)
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(p, content, mode)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
|
||||
func mkdir(L *lua.LState) int {
|
||||
dir := L.CheckString(1)
|
||||
var mode os.FileMode = 0777
|
||||
|
||||
top := L.GetTop()
|
||||
if top >= 2 {
|
||||
m, err := oct2decimal(L.CheckInt(2))
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
mode = os.FileMode(m)
|
||||
}
|
||||
|
||||
recursive := false
|
||||
if top >= 3 {
|
||||
recursive = L.ToBool(3)
|
||||
}
|
||||
|
||||
var err error
|
||||
if recursive {
|
||||
err = os.MkdirAll(dir, mode)
|
||||
} else {
|
||||
err = os.Mkdir(dir, mode)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LTrue)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func remove(L *lua.LState) int {
|
||||
p := L.CheckString(1)
|
||||
|
||||
recursive := false
|
||||
if L.GetTop() >= 2 {
|
||||
recursive = L.ToBool(2)
|
||||
}
|
||||
|
||||
var err error
|
||||
if recursive {
|
||||
err = os.Remove(p)
|
||||
} else {
|
||||
err = os.RemoveAll(p)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LTrue)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func symlink(L *lua.LState) int {
|
||||
target := L.CheckString(1)
|
||||
link := L.CheckString(2)
|
||||
|
||||
err := os.Symlink(target, link)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
|
||||
func dirname(L *lua.LState) int {
|
||||
filep := L.CheckString(1)
|
||||
dirna := filepath.Dir(filep)
|
||||
L.Push(lua.LString(dirna))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func basename(L *lua.LState) int {
|
||||
filep := L.CheckString(1)
|
||||
dirna := filepath.Base(filep)
|
||||
L.Push(lua.LString(dirna))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func fnRealpath(L *lua.LState) int {
|
||||
filep := L.CheckString(1)
|
||||
|
||||
real, err := realpath.Realpath(filep)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LString(real))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func getcwd(L *lua.LState) int {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LString(dir))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func chdir(L *lua.LState) int {
|
||||
dir := L.CheckString(1)
|
||||
|
||||
err := os.Chdir(dir)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
|
||||
func file(L *lua.LState) int {
|
||||
// same: debug.getinfo(2,'S').source
|
||||
var dbg *lua.Debug
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
dbg, ok = L.GetStack(1)
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprint(dbg)))
|
||||
return 2
|
||||
}
|
||||
_, err = L.GetInfo("S", dbg, lua.LNil)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(dbg.Source))
|
||||
return 1
|
||||
}
|
||||
|
||||
func dir(L *lua.LState) int {
|
||||
// same: debug.getinfo(2,'S').source
|
||||
var dbg *lua.Debug
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
dbg, ok = L.GetStack(1)
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprint(dbg)))
|
||||
return 2
|
||||
}
|
||||
_, err = L.GetInfo("S", dbg, lua.LNil)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
dirname := filepath.Dir(dbg.Source)
|
||||
L.Push(lua.LString(dirname))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func glob(L *lua.LState) int {
|
||||
ptn := L.CheckString(1)
|
||||
fn := L.CheckFunction(2)
|
||||
|
||||
files, err := filepath.Glob(ptn)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
tb := L.NewTable()
|
||||
tb.RawSetString("path", lua.LString(f))
|
||||
abspath, err := filepath.Abs(f)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
tb.RawSetString("realpath", lua.LString(abspath))
|
||||
|
||||
err = L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, tb)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
|
||||
func isDir(path string) (ret bool) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fi.IsDir()
|
||||
}
|
||||
|
||||
func oct2decimal(oct int) (uint64, error) {
|
||||
return strconv.ParseUint(fmt.Sprintf("%d", oct), 8, 32)
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package gluafs
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("fs", Loader)
|
||||
if err := L.DoString(`
|
||||
local fs = require("fs")
|
||||
assert(fs.exists(".") == true)
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
var tmpFileContent = "aaaaaaaabbbbbbbb"
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
tmpFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
if err = ioutil.WriteFile(tmpFile.Name(), []byte(tmpFileContent), 0644); err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFile.Name())
|
||||
}()
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("fs", Loader)
|
||||
if err := L.DoString(`
|
||||
local fs = require("fs")
|
||||
local content = fs.read("` + tmpFile.Name() + `")
|
||||
|
||||
assert(content == "aaaaaaaabbbbbbbb")
|
||||
|
||||
local content2, err = fs.read("` + tmpFile.Name() + `.hoge")
|
||||
assert(content2 == nil)
|
||||
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
}()
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("fs", Loader)
|
||||
if err := L.DoString(`
|
||||
local fs = require("fs")
|
||||
local ret, err = fs.write("` + tmpDir + `/hoge", "aaaaaaaabbbbbbbb", 755)
|
||||
local content = fs.read("` + tmpDir + `/hoge")
|
||||
|
||||
assert(content == "aaaaaaaabbbbbbbb")
|
||||
|
||||
ret, err = fs.write("` + tmpDir + `/hoge/aaaa", "aaaaaaaabbbbbbbb")
|
||||
assert(ret == nil)
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkdir(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
}()
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("fs", Loader)
|
||||
if err := L.DoString(`
|
||||
local fs = require("fs")
|
||||
local ret, err = fs.mkdir("` + tmpDir + `/hoge")
|
||||
assert(ret == true)
|
||||
|
||||
local ret, err = fs.mkdir("` + tmpDir + `/hoge/aaa/bbb", 0777, true)
|
||||
assert(ret == true)
|
||||
|
||||
local ret, err = fs.mkdir("` + tmpDir + `/hoge/bbb/eeee", 0777)
|
||||
assert(ret == nil)
|
||||
-- print(err)
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlob(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Errorf("should not raise error: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
}()
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("fs", Loader)
|
||||
if err := L.DoString(`
|
||||
local fs = require("fs")
|
||||
fs.mkdir("` + tmpDir + `/dir01")
|
||||
fs.mkdir("` + tmpDir + `/dir02")
|
||||
fs.mkdir("` + tmpDir + `/dir03")
|
||||
fs.write("` + tmpDir + `/file01", "aaaaaaa")
|
||||
fs.write("` + tmpDir + `/file02", "bbbbbbb")
|
||||
|
||||
local ret, err = fs.glob("` + tmpDir + `/*", function(file)
|
||||
print(file.path)
|
||||
print(file.realpath)
|
||||
end)
|
||||
|
||||
assert(ret == true)
|
||||
assert(err == nil)
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
.tags*
|
||||
._*
|
||||
*.iml
|
||||
.vagrant
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Kohki Makimoto
|
||||
|
||||
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.
|
|
@ -0,0 +1,44 @@
|
|||
# gluaquestion
|
||||
|
||||
A library of [gopher-lua](https://github.com/yuin/gopher-lua) to prompt the user for input.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/kohkimakimoto/gluaquestion
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
* `question.ask(text)`
|
||||
* `question.secret(text)`
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/kohkimakimoto/gluaquestion"
|
||||
)
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("question", gluaquestion.Loader)
|
||||
if err := L.DoString(`
|
||||
local question = require("question")
|
||||
|
||||
local name = question.ask("What's your name: ")
|
||||
print("hello " .. name)
|
||||
|
||||
-- What's your name: kohki
|
||||
-- hello kohki
|
||||
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,45 @@
|
|||
package gluaquestion
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/howeyc/gopass"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// Loader is glua module loader.
|
||||
func Loader(L *lua.LState) int {
|
||||
tb := L.NewTable()
|
||||
L.SetFuncs(tb, map[string]lua.LGFunction{
|
||||
"ask": ask,
|
||||
"secret": secret,
|
||||
})
|
||||
L.Push(tb)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func ask(L *lua.LState) int {
|
||||
msg := L.CheckString(1)
|
||||
|
||||
fmt.Printf(msg)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
str, _ := reader.ReadString('\n')
|
||||
str = strings.TrimRight(str, "\r\n")
|
||||
L.Push(lua.LString(str))
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func secret(L *lua.LState) int {
|
||||
msg := L.CheckString(1)
|
||||
|
||||
fmt.Printf(msg)
|
||||
pass, _ := gopass.GetPasswd()
|
||||
L.Push(lua.LString(pass))
|
||||
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
.tags*
|
||||
._*
|
||||
*.iml
|
||||
.vagrant
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Kohki Makimoto
|
||||
|
||||
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.
|
|
@ -0,0 +1,10 @@
|
|||
.PHONY: build dep install test
|
||||
|
||||
test_setup:
|
||||
cd _tests && vagrant up && cd -
|
||||
|
||||
test:
|
||||
go test ./... -cover
|
||||
|
||||
testverbose:
|
||||
go test ./... -v -cover
|
|
@ -0,0 +1,4 @@
|
|||
# gluassh
|
||||
|
||||
SSH library for [gopher-lua](https://github.com/yuin/gopher-lua) to run commands on remote severs.
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
package gluassh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
//
|
||||
// Refers to
|
||||
//
|
||||
// https://github.com/markpeek/packer/blob/53446bf713ed2866b451aa1d00813a90308ee9e9/communicator/ssh/communicator.go
|
||||
// https://github.com/rapidloop/rtop
|
||||
// https://github.com/rapidloop/rtop/blob/ba5b35e964135d50e0babedf0bd69b2fcb5dbcb4/src/sshhelper.go#L185
|
||||
// https://gist.github.com/zxvdr/8229523
|
||||
//
|
||||
|
||||
// SSH communicator
|
||||
type Communicator struct {
|
||||
Config *Config
|
||||
Agent agent.Agent
|
||||
Auths []ssh.AuthMethod
|
||||
ClientConfig *ssh.ClientConfig
|
||||
UpstreamConfig *Config
|
||||
client *ssh.Client
|
||||
clientConns []interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
OriginalConfig *Config
|
||||
}
|
||||
|
||||
func NewComm(config *Config) (*Communicator, error) {
|
||||
// Has a proxy?
|
||||
originalConfig := config
|
||||
|
||||
var upstreamConfig *Config = nil
|
||||
proxy := config.PopProxyConfig()
|
||||
if proxy != nil {
|
||||
upstreamConfig = config
|
||||
config = proxy
|
||||
}
|
||||
|
||||
comm := &Communicator{
|
||||
Config: config,
|
||||
UpstreamConfig: upstreamConfig,
|
||||
clientConns: make([]interface {
|
||||
Close() error
|
||||
}, 0),
|
||||
OriginalConfig: originalConfig,
|
||||
}
|
||||
|
||||
// construct auths
|
||||
auths := []ssh.AuthMethod{}
|
||||
|
||||
if config.UseAgent {
|
||||
// use ssh-agent
|
||||
if sock := os.Getenv("SSH_AUTH_SOCK"); len(sock) > 0 {
|
||||
agconn, err := net.Dial("unix", sock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ag := agent.NewClient(agconn)
|
||||
auth := ssh.PublicKeysCallback(ag.Signers)
|
||||
auths = append(auths, auth)
|
||||
|
||||
comm.Agent = ag
|
||||
|
||||
} else {
|
||||
return nil, errors.New("Could not get a socket from SSH_AUTH_SOCK")
|
||||
}
|
||||
} else {
|
||||
// use key file
|
||||
pemBytes, err := ioutil.ReadFile(config.KeyOrDefault())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return nil, errors.New("no key found in " + config.KeyOrDefault())
|
||||
}
|
||||
|
||||
// handle plain and encrypted keyfile
|
||||
if x509.IsEncryptedPEMBlock(block) {
|
||||
if config.KeyPassphrase == "" {
|
||||
return nil, errors.New("You have to set a key_passphrase for the encryped keyfile '" + config.KeyOrDefault() + "'")
|
||||
}
|
||||
|
||||
block.Bytes, err = x509.DecryptPEMBlock(block, []byte(config.KeyPassphrase))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := parsePemBlock(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer, err := ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auths = append(auths, ssh.PublicKeys(signer))
|
||||
|
||||
} else {
|
||||
signer, err := ssh.ParsePrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auths = append(auths, ssh.PublicKeys(signer))
|
||||
}
|
||||
}
|
||||
|
||||
if config.Password != "" {
|
||||
// Use password
|
||||
auths = append(auths, ssh.Password(config.Password))
|
||||
}
|
||||
|
||||
comm.Auths = auths
|
||||
|
||||
// ssh client config
|
||||
comm.ClientConfig = &ssh.ClientConfig{
|
||||
User: config.UserOrDefault(),
|
||||
Auth: auths,
|
||||
}
|
||||
|
||||
return comm, nil
|
||||
}
|
||||
|
||||
// Exec runs command on remote server.
|
||||
// It is a low-layer method to be used Run, Script and others method.
|
||||
func (c *Communicator) Exec(cmd string, opt *Option, L *lua.LState) (*Result, error) {
|
||||
// get a client
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get a session
|
||||
sess, err := client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
// RequestAgentForwarding
|
||||
if c.Config.ForwardAgent && c.Agent != nil {
|
||||
agent.ForwardToAgent(client, c.Agent)
|
||||
if err = agent.RequestAgentForwarding(sess); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Request a PTY
|
||||
if c.Config.Pty {
|
||||
// refers to https://github.com/markpeek/packer/blob/53446bf713ed2866b451aa1d00813a90308ee9e9/communicator/ssh/communicator.go
|
||||
termModes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0, // do not echo
|
||||
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||
}
|
||||
err = sess.RequestPty("xterm", 80, 40, termModes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Compose sudo command line.
|
||||
if opt.Sudo {
|
||||
if opt.User != "" {
|
||||
cmd = "sudo -Sku " + opt.User + " /bin/sh -l -c '" + cmd + "'"
|
||||
} else {
|
||||
cmd = "sudo -Sk /bin/sh -l -c '" + cmd + "'"
|
||||
}
|
||||
}
|
||||
|
||||
// apply io
|
||||
var outWriter io.Writer
|
||||
var errWriter io.Writer
|
||||
var inReader io.Reader
|
||||
|
||||
// buffer
|
||||
var outBuffer = new(bytes.Buffer)
|
||||
var errBuffer = new(bytes.Buffer)
|
||||
var inBuffer = new(bytes.Buffer)
|
||||
|
||||
|
||||
// append stdio to buffer
|
||||
if opt.UseStdout {
|
||||
if opt.OutputFunc != nil {
|
||||
outWriter = io.MultiWriter(os.Stdout, outBuffer, NewLFuncWriter(1, opt.OutputFunc, L))
|
||||
} else {
|
||||
outWriter = io.MultiWriter(os.Stdout, outBuffer)
|
||||
}
|
||||
} else {
|
||||
outWriter = io.MultiWriter(outBuffer)
|
||||
}
|
||||
|
||||
if opt.UseStderr {
|
||||
if opt.OutputFunc != nil {
|
||||
errWriter = io.MultiWriter(os.Stderr, errBuffer, NewLFuncWriter(2, opt.OutputFunc, L))
|
||||
} else {
|
||||
errWriter = io.MultiWriter(os.Stderr, errBuffer)
|
||||
}
|
||||
} else {
|
||||
errWriter = io.MultiWriter(errBuffer)
|
||||
}
|
||||
|
||||
inReader = io.MultiReader(inBuffer, os.Stdin)
|
||||
|
||||
sess.Stdin = inReader
|
||||
sess.Stdout = outWriter
|
||||
sess.Stderr = errWriter
|
||||
|
||||
// write sudo password
|
||||
if opt.Password != "" {
|
||||
// If it try to use sudo password, write password to input buffer
|
||||
fmt.Fprintln(inBuffer, opt.Password)
|
||||
}
|
||||
|
||||
err = sess.Run(cmd)
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*ssh.ExitError); ok {
|
||||
return NewResult(outBuffer, errBuffer, exitErr.ExitStatus()), err
|
||||
} else {
|
||||
return NewResult(outBuffer, errBuffer, 1), err
|
||||
}
|
||||
}
|
||||
|
||||
return NewResult(outBuffer, errBuffer, 0), nil
|
||||
}
|
||||
|
||||
func (c *Communicator) Run(cmd string, opt *Option, L *lua.LState) (*Result, error) {
|
||||
return c.Exec(cmd, opt, L)
|
||||
}
|
||||
|
||||
func (c *Communicator) Get(remote string, local string) error {
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sc.Close()
|
||||
|
||||
fi, err := sc.Open(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fi.Close()
|
||||
|
||||
fo, err := os.Create(local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fo.Close()
|
||||
|
||||
_, err = io.Copy(fo, fi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Communicator) Put(local string, remote string) error {
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sc.Close()
|
||||
|
||||
b, err := ioutil.ReadFile(local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fo, err := sc.Create(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fo.Close()
|
||||
|
||||
_, err = fo.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Communicator) Client() (*ssh.Client, error) {
|
||||
if c.client != nil {
|
||||
return c.client, nil
|
||||
}
|
||||
|
||||
// create ssh client.
|
||||
client, err := ssh.Dial("tcp", c.Config.HostOrDefault()+":"+c.Config.PortOrDefault(), c.ClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.clientConns = append(c.clientConns, client)
|
||||
|
||||
// If it has a upstream server?
|
||||
for c.UpstreamConfig != nil {
|
||||
// It is next server config to connect.
|
||||
var config *Config = nil
|
||||
|
||||
// Does the upstream server need proxy to connet?
|
||||
proxy := c.UpstreamConfig.PopProxyConfig()
|
||||
if proxy != nil {
|
||||
config = proxy
|
||||
} else {
|
||||
config = c.UpstreamConfig
|
||||
c.UpstreamConfig = nil
|
||||
}
|
||||
|
||||
// dial to ssh proxy
|
||||
connection, err := client.Dial("tcp", config.HostOrDefault()+":"+config.PortOrDefault())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.clientConns = append(c.clientConns, connection)
|
||||
|
||||
conn, chans, reqs, err := ssh.NewClientConn(connection, config.HostOrDefault()+":"+config.PortOrDefault(), c.ClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client = ssh.NewClient(conn, chans, reqs)
|
||||
c.clientConns = append(c.clientConns, client)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.client = client
|
||||
return c.client, nil
|
||||
}
|
||||
|
||||
func (c *Communicator) Close() error {
|
||||
for _, conn := range c.clientConns {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey.
|
||||
func parsePemBlock(block *pem.Block) (interface{}, error) {
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(block.Bytes)
|
||||
case "DSA PRIVATE KEY":
|
||||
return ssh.ParseDSAPrivateKey(block.Bytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("rtop: unsupported key type %q", block.Type)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package gluassh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// SSH connection config.
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
User string `json:"user"`
|
||||
Key string `json:"key"`
|
||||
KeyPassphrase string `json:"key_passphrase"`
|
||||
UseAgent bool `json:"use_agent"`
|
||||
ForwardAgent bool `json:"forward_agent"`
|
||||
Pty bool `json:"pty"`
|
||||
Password string `json:"password"`
|
||||
Proxy *Config `json:"proxy"`
|
||||
}
|
||||
|
||||
// NewConfig creates new config instance.
|
||||
func NewConfig() *Config {
|
||||
c := &Config{}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Pop the last proxy server
|
||||
func (c *Config) PopProxyConfig() *Config {
|
||||
proxy := c.Proxy
|
||||
if proxy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if proxy.Proxy != nil {
|
||||
return proxy.PopProxyConfig()
|
||||
}
|
||||
|
||||
c.Proxy = nil
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
func (c *Config) UpdateWithJSON(data []byte) error {
|
||||
err := json.Unmarshal(data, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) ConfigsChain() []*Config {
|
||||
var configs []*Config
|
||||
|
||||
configs = append(configs, c)
|
||||
|
||||
if c.Proxy != nil {
|
||||
proxyConfigs := c.Proxy.ConfigsChain()
|
||||
configs = append(proxyConfigs, configs...)
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
func (c *Config) HostOrDefault() string {
|
||||
if c.Host == "" {
|
||||
return "localhost"
|
||||
} else {
|
||||
return c.Host
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) PortOrDefault() string {
|
||||
if c.Port == "" {
|
||||
return "22"
|
||||
} else {
|
||||
return c.Port
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) KeyOrDefault() string {
|
||||
if c.Key == "" {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
return filepath.Join(home, ".ssh/id_rsa")
|
||||
}
|
||||
|
||||
return c.Key
|
||||
}
|
||||
|
||||
func (c *Config) UserOrDefault() string {
|
||||
if c.User == "" {
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
return u.Username
|
||||
}
|
||||
}
|
||||
|
||||
return c.User
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
package gluassh
|
||||
|
||||
import (
|
||||
"github.com/yuin/gluamapper"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const LConnectionClass = "SSHConnectionClass*"
|
||||
|
||||
type Connection struct {
|
||||
|
||||
}
|
||||
|
||||
// Loader is glua module loader.
|
||||
func Loader(L *lua.LState) int {
|
||||
// register ssh connection class
|
||||
registerConnectionClass(L)
|
||||
|
||||
// load module
|
||||
tb := L.NewTable()
|
||||
L.SetFuncs(tb, map[string]lua.LGFunction{
|
||||
"run": run,
|
||||
"get": get,
|
||||
"put": put,
|
||||
})
|
||||
|
||||
// set up meta table
|
||||
mt := L.NewTable()
|
||||
L.SetField(mt, "__call",L.NewFunction(call))
|
||||
L.SetMetatable(tb, mt)
|
||||
|
||||
L.Push(tb)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func registerConnectionClass(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable(LConnectionClass)
|
||||
// methods
|
||||
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
// "run": connRun,
|
||||
// "get": connGet,
|
||||
// "put": connPut,
|
||||
}))
|
||||
}
|
||||
|
||||
func call(L *lua.LState) int {
|
||||
// TODO: implements
|
||||
if L.GetTop() != 2 {
|
||||
L.RaiseError("calling ssh requires 1 argment that is a server config.")
|
||||
}
|
||||
|
||||
// tb := L.Get(1)
|
||||
return 0
|
||||
}
|
||||
|
||||
func newConnection(L *lua.LState) int {
|
||||
connection := &Connection{}
|
||||
ud := L.NewUserData()
|
||||
ud.Value = connection
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(LConnectionClass))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}
|
||||
|
||||
// This is a Lua module function.
|
||||
// run execute command on a remote host via SSH.
|
||||
func run(L *lua.LState) int {
|
||||
// communicator
|
||||
comm, err := commFromLTable(L.CheckTable(1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer comm.Close()
|
||||
|
||||
// option and command
|
||||
option := NewOption()
|
||||
cmd := ""
|
||||
|
||||
if L.GetTop() == 4 {
|
||||
if err := gluamapper.Map(L.CheckTable(2), option); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cmds, err := toStrings(L.Get(3))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cmd = concatCommandLines(cmds)
|
||||
|
||||
fn := L.CheckFunction(4)
|
||||
option.OutputFunc = fn
|
||||
|
||||
} else if L.GetTop() == 3 {
|
||||
if err := gluamapper.Map(L.CheckTable(2), option); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cmds, err := toStrings(L.Get(3))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cmd = concatCommandLines(cmds)
|
||||
} else if L.GetTop() == 2 {
|
||||
cmds, err := toStrings(L.Get(2))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cmd = concatCommandLines(cmds)
|
||||
} else {
|
||||
L.RaiseError("run method requires 2 arugments at least.")
|
||||
}
|
||||
|
||||
// run
|
||||
ret, err := comm.Run(cmd, option, L)
|
||||
if err != nil {
|
||||
L.RaiseError("ssh error: %s", err)
|
||||
}
|
||||
|
||||
tb := updateLTableByRunResult(L.NewTable(), ret)
|
||||
L.Push(tb)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// This is a Lua module function.
|
||||
func get(L *lua.LState) int {
|
||||
// communicator
|
||||
comm, err := commFromLTable(L.CheckTable(1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer comm.Close()
|
||||
|
||||
remote := L.ToString(2)
|
||||
local := L.ToString(3)
|
||||
|
||||
err = comm.Get(remote, local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// This is a Lua module function.
|
||||
func put(L *lua.LState) int {
|
||||
// communicator
|
||||
comm, err := commFromLTable(L.CheckTable(1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer comm.Close()
|
||||
|
||||
local := L.ToString(2)
|
||||
remote := L.ToString(3)
|
||||
|
||||
err = comm.Put(local, remote)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func commFromLTable(table *lua.LTable) (*Communicator, error) {
|
||||
// config
|
||||
cfg := NewConfig()
|
||||
// update config by passed table.
|
||||
if err := gluamapper.Map(table, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// communicator
|
||||
comm, err := NewComm(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comm, nil
|
||||
}
|
||||
|
||||
func toStrings(v lua.LValue) ([]string, error) {
|
||||
var ret []string
|
||||
|
||||
if lv, ok := v.(*lua.LTable); ok {
|
||||
lv.ForEach(func(tk lua.LValue, tv lua.LValue) {
|
||||
if ls, ok := tv.(lua.LString); ok {
|
||||
ret = append(ret, string(ls))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if ls, ok := v.(lua.LString); ok {
|
||||
ret = append(ret, string(ls))
|
||||
} else {
|
||||
return ret, errors.New("Could not transfer lua value to string slice")
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func concatCommandLines(cmdlines []string) string {
|
||||
cmdline := ""
|
||||
|
||||
for i, v := range cmdlines {
|
||||
if i == 0 {
|
||||
cmdline = v
|
||||
} else {
|
||||
cmdline = cmdline + " && " + v
|
||||
}
|
||||
}
|
||||
|
||||
return cmdline
|
||||
}
|
||||
|
||||
func updateLTableByRunResult(tb *lua.LTable, ret *Result) *lua.LTable {
|
||||
if ret != nil {
|
||||
tb.RawSetString("out", lua.LString(ret.Out.String()))
|
||||
tb.RawSetString("err", lua.LString(ret.Err.String()))
|
||||
tb.RawSetString("status", lua.LNumber(ret.Status))
|
||||
tb.RawSetString("successful", lua.LBool(ret.Successful()))
|
||||
tb.RawSetString("failed", lua.LBool(ret.Failed()))
|
||||
} else {
|
||||
tb.RawSetString("out", lua.LNil)
|
||||
tb.RawSetString("err", lua.LNil)
|
||||
tb.RawSetString("status", lua.LNil)
|
||||
tb.RawSetString("successful", lua.LBool(false))
|
||||
tb.RawSetString("failed", lua.LBool(true))
|
||||
}
|
||||
|
||||
return tb
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package gluassh
|
||||
|
||||
import(
|
||||
"testing"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var keynopass string
|
||||
var keypass string
|
||||
|
||||
func TestRunWithKeynopass(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("ssh", Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
local ssh = require "ssh"
|
||||
|
||||
conn_user_keynopass = {
|
||||
host = "192.168.56.81",
|
||||
port = "22",
|
||||
user = "user_keynopass",
|
||||
key = "` + keynopass + `",
|
||||
}
|
||||
|
||||
local ret = ssh.run(conn_user_keynopass, "hostname")
|
||||
|
||||
assert(ret.out == "gluassh-test-server\n")
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithKeynopassSudo(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("ssh", Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
local ssh = require "ssh"
|
||||
|
||||
conn_user_keynopass = {
|
||||
host = "192.168.56.81",
|
||||
port = "22",
|
||||
user = "user_keynopass",
|
||||
key = "` + keynopass + `",
|
||||
}
|
||||
|
||||
ret = ssh.run(conn_user_keynopass, {sudo = true}, "whoami")
|
||||
assert(ret.out == "root\n")
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithKeypass(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("ssh", Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
local ssh = require "ssh"
|
||||
|
||||
conn_user_keypass = {
|
||||
host = "192.168.56.81",
|
||||
port = "22",
|
||||
user = "user_keypass",
|
||||
key = "` + keypass + `",
|
||||
key_passphrase = "hogehoge",
|
||||
}
|
||||
|
||||
local ret = ssh.run(conn_user_keypass, "hostname")
|
||||
|
||||
assert(ret.out == "gluassh-test-server\n")
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithKeynopassFunc(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("ssh", Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
local ssh = require "ssh"
|
||||
|
||||
conn_user_keynopass = {
|
||||
host = "192.168.56.81",
|
||||
port = "22",
|
||||
user = "user_keynopass",
|
||||
key = "` + keynopass + `",
|
||||
}
|
||||
|
||||
local ret = ssh.run(conn_user_keynopass, {}, "hostname", function(stdout, stderr)
|
||||
|
||||
print(stdout)
|
||||
|
||||
end)
|
||||
|
||||
`); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
keynopass = filepath.Join(wd, "_tests", "keys", "id_rsa.nopass")
|
||||
keypass = filepath.Join(wd, "_tests", "keys", "id_rsa.pass")
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package gluassh
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// SSH run option
|
||||
type Option struct {
|
||||
// Use sudo
|
||||
Sudo bool
|
||||
// sudo user
|
||||
User string
|
||||
// sudo password
|
||||
Password string
|
||||
// Use stdout
|
||||
UseStdout bool
|
||||
// Use stderr
|
||||
UseStderr bool
|
||||
// function receives output.
|
||||
OutputFunc *lua.LFunction
|
||||
}
|
||||
|
||||
func NewOption() *Option {
|
||||
opt := &Option{
|
||||
Sudo: false,
|
||||
UseStdout: true,
|
||||
UseStderr: true,
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
type LFuncWriter struct {
|
||||
outType int
|
||||
fn *lua.LFunction
|
||||
L *lua.LState
|
||||
}
|
||||
|
||||
func NewLFuncWriter(outType int, fn *lua.LFunction, L *lua.LState) *LFuncWriter {
|
||||
return &LFuncWriter{
|
||||
L: L,
|
||||
outType: outType,
|
||||
fn: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LFuncWriter) Write(data []byte) (int, error) {
|
||||
if w.outType == 1 {
|
||||
err := w.L.CallByParam(lua.P{
|
||||
Fn: w.fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, lua.LString(string(data)))
|
||||
if err != nil {
|
||||
return len(data), err
|
||||
}
|
||||
|
||||
} else {
|
||||
err := w.L.CallByParam(lua.P{
|
||||
Fn: w.fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, lua.LString(string(data)))
|
||||
if err != nil {
|
||||
return len(data), err
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package gluassh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Out *bytes.Buffer
|
||||
Err *bytes.Buffer
|
||||
Status int
|
||||
}
|
||||
|
||||
func NewResult(outbuf *bytes.Buffer, errbuf *bytes.Buffer, status int) *Result {
|
||||
return &Result{
|
||||
Out: outbuf,
|
||||
Err: errbuf,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Result) Successful() bool {
|
||||
if r.Status == 0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Result) Failed() bool {
|
||||
return !r.Successful()
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
/.tags*
|
||||
._*
|
||||
*.iml
|
||||
/_vendor
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Kohki Makimoto
|
||||
|
||||
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.
|
|
@ -0,0 +1,67 @@
|
|||
# gluatemplate
|
||||
|
||||
Text template for [gopher-lua](https://github.com/yuin/gopher-lua)
|
||||
|
||||
This product is based on [Go Text Template](https://golang.org/pkg/text/template/).
|
||||
If you are not familiar with the syntax, please read the documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/kohkimakimoto/gluatemplate
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `template.dostring(text, table)`
|
||||
|
||||
Returns string generated by text template with the table values. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
### `template.dofile(file, table)`
|
||||
|
||||
Returns string generated by file template with the table values. If this function fails, it returns `nil`, plus a string describing the error.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/kohkimakimoto/gluatemplate"
|
||||
)
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("template", gluatemplate.Loader)
|
||||
if err := L.DoString(`
|
||||
local template = require("template")
|
||||
|
||||
local text = template.dostring([[
|
||||
This is a text template library.
|
||||
Created by {{.first_name}} {{.last_name}}
|
||||
]], {
|
||||
first_name = "kohki",
|
||||
last_name = "makimoto",
|
||||
})
|
||||
|
||||
print(text)
|
||||
--
|
||||
-- This is a text template library.
|
||||
-- Created by kohki makimoto
|
||||
--
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Kohki Makimoto <kohki.makimoto@gmail.com>
|
||||
|
||||
## License
|
||||
|
||||
MIT license.
|
|
@ -0,0 +1,106 @@
|
|||
package gluatemplate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func Loader(L *lua.LState) int {
|
||||
tb := L.NewTable()
|
||||
L.SetFuncs(tb, map[string]lua.LGFunction{
|
||||
"dostring": doString,
|
||||
"dofile": doFile,
|
||||
})
|
||||
L.Push(tb)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// render
|
||||
func doString(L *lua.LState) int {
|
||||
tmplcontent := L.CheckString(1)
|
||||
var dict interface{}
|
||||
|
||||
tmpl, err := template.New("T").Parse(tmplcontent)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
if L.GetTop() >= 2 {
|
||||
dict = toGoValue(L.CheckTable(2))
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := tmpl.Execute(&b, dict); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
s := b.String()
|
||||
|
||||
L.Push(lua.LString(s))
|
||||
return 1
|
||||
}
|
||||
|
||||
func doFile(L *lua.LState) int {
|
||||
tmplfile := L.CheckString(1)
|
||||
var dict interface{}
|
||||
|
||||
tmpl, err := template.ParseFiles(tmplfile)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
if L.GetTop() >= 2 {
|
||||
dict = toGoValue(L.CheckTable(2))
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := tmpl.Execute(&b, dict); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
s := b.String()
|
||||
|
||||
L.Push(lua.LString(s))
|
||||
return 1
|
||||
}
|
||||
|
||||
// This code refers to https://github.com/yuin/gluamapper/blob/master/gluamapper.go
|
||||
func toGoValue(lv lua.LValue) interface{} {
|
||||
switch v := lv.(type) {
|
||||
case *lua.LNilType:
|
||||
return nil
|
||||
case lua.LBool:
|
||||
return bool(v)
|
||||
case lua.LString:
|
||||
return string(v)
|
||||
case lua.LNumber:
|
||||
return float64(v)
|
||||
case *lua.LTable:
|
||||
maxn := v.MaxN()
|
||||
if maxn == 0 { // table
|
||||
ret := make(map[interface{}]interface{})
|
||||
v.ForEach(func(key, value lua.LValue) {
|
||||
keystr := fmt.Sprint(toGoValue(key))
|
||||
ret[keystr] = toGoValue(value)
|
||||
})
|
||||
return ret
|
||||
} else { // array
|
||||
ret := make([]interface{}, 0, maxn)
|
||||
for i := 1; i <= maxn; i++ {
|
||||
ret = append(ret, toGoValue(v.RawGetInt(i)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue