├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _disabled_appveyor.yml ├── ast ├── doc_test.go ├── node.go ├── node_args.go ├── node_fmt.go ├── node_fmt_test.go ├── nodetype_string.go ├── tree.go └── tree_test.go ├── cmd ├── nash │ ├── cli.go │ ├── completer.go │ ├── env.go │ ├── env_test.go │ ├── example.sh │ ├── install.go │ ├── install_test.go │ ├── main.go │ └── rpc.go └── nashfmt │ └── main.go ├── docs ├── interactive.md ├── reference.md └── stdlib │ └── fmt.md ├── errors └── error.go ├── examples ├── append.sh ├── args.sh ├── init └── len.sh ├── examples_test.go ├── fuzz.go ├── go.mod ├── go.sum ├── hack ├── check.sh ├── install │ └── unix.sh └── releaser.sh ├── internal ├── sh │ ├── builtin.go │ ├── builtin │ │ ├── append.go │ │ ├── append_test.go │ │ ├── chdir.go │ │ ├── doc.go │ │ ├── exec_test.go │ │ ├── exit.go │ │ ├── exit_test.go │ │ ├── format.go │ │ ├── format_test.go │ │ ├── glob.go │ │ ├── glob_test.go │ │ ├── len.go │ │ ├── len_test.go │ │ ├── loader.go │ │ ├── print.go │ │ ├── print_test.go │ │ ├── split.go │ │ ├── split_test.go │ │ └── testdata │ │ │ ├── exit.sh │ │ │ ├── split.sh │ │ │ └── splitfunc.sh │ ├── cmd.go │ ├── cmd_test.go │ ├── fncall.go │ ├── fndef.go │ ├── functions_test.go │ ├── internal │ │ └── fixture │ │ │ └── fixture.go │ ├── ioutils_test.go │ ├── log.go │ ├── rfork.go │ ├── rfork_linux.go │ ├── rfork_linux_test.go │ ├── rfork_plan9.go │ ├── shell.go │ ├── shell_import_test.go │ ├── shell_linux_test.go │ ├── shell_regression_test.go │ ├── shell_test.go │ ├── shell_var_test.go │ ├── util.go │ └── util_test.go └── testing │ └── fixture │ └── io.go ├── nash.go ├── nash_test.go ├── parser ├── parse.go ├── parse_fmt_test.go ├── parse_regression_test.go └── parse_test.go ├── proposal ├── 1-scope-management.md └── 2-concurrency.md ├── readline ├── LICENSE ├── ansi_windows.go ├── complete.go ├── complete_helper.go ├── complete_segment.go ├── complete_segment_test.go ├── doc │ └── shortcut.md ├── history.go ├── operation.go ├── password.go ├── rawreader_windows.go ├── readline.go ├── readline_test.go ├── remote.go ├── runebuf.go ├── runes.go ├── runes │ ├── runes.go │ └── runes_test.go ├── runes_test.go ├── std.go ├── std_windows.go ├── term.go ├── term_bsd.go ├── term_linux.go ├── term_windows.go ├── terminal.go ├── utils.go ├── utils_test.go ├── utils_unix.go ├── utils_windows.go ├── vim.go └── windows_api.go ├── scanner ├── examples_test.go ├── lex.go ├── lex_regression_test.go └── lex_test.go ├── sh ├── obj.go ├── objtype_string.go └── shell.go ├── spec.ebnf ├── spec_test.go ├── stdbin ├── mkdir │ ├── main.go │ └── mkdir_test.go ├── pwd │ └── main.go ├── strings │ ├── main.go │ ├── strings.go │ └── strings_test.go └── write │ ├── common_test.sh │ ├── fd.go │ ├── fd_windows.go │ ├── main.go │ ├── write.go │ ├── write_linux_test.sh │ └── write_test.sh ├── stdlib ├── io.sh ├── io_example.sh ├── io_test.sh ├── map.sh └── map_test.sh ├── testfiles ├── ex1.sh ├── fibonacci.sh └── sieve.sh ├── tests ├── cfg.go ├── doc.go ├── internal │ ├── assert │ │ ├── doc.go │ │ ├── equal.go │ │ └── error.go │ ├── sh │ │ └── shell.go │ └── tester │ │ └── tester.go ├── listindex_test.go └── stringindex_test.go ├── token └── token.go └── vendor ├── github.com └── chzyer │ ├── logex │ ├── .travis.yml │ ├── Makefile │ ├── README.md │ ├── err.go │ └── logex.go │ └── test │ ├── LICENSE │ ├── README.md │ ├── disk.go │ └── test.go ├── golang.org └── x │ └── exp │ ├── AUTHORS │ ├── CONTRIBUTORS │ ├── LICENSE │ ├── PATENTS │ └── ebnf │ ├── ebnf.go │ └── parser.go └── modules.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ./cmd/cnt/cnt 2 | /cmd/nash/nash 3 | /coverage.txt 4 | cmd/nashfmt/nashfmt 5 | dist 6 | *.exe 7 | stdbin/mkdir/mkdir 8 | stdbin/pwd/pwd 9 | stdbin/strings/strings 10 | stdbin/write/write 11 | coverage.html 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | 4 | language: go 5 | sudo: false 6 | 7 | go: 8 | - "tip" 9 | - "1.14" 10 | - "1.13" 11 | - "1.12" 12 | 13 | install: 14 | - go get -v golang.org/x/exp/ebnf 15 | - make build 16 | 17 | script: 18 | - go get github.com/axw/gocov/gocov 19 | - go get github.com/mattn/goveralls 20 | - go get golang.org/x/tools/cmd/cover 21 | - mkdir $HOME/nashroot 22 | - make test 23 | - make build 24 | - ./cmd/nash/nash ./hack/releaser.sh testci 25 | 26 | after_success: 27 | - bash <(curl -s https://codecov.io/bash) 28 | 29 | notifications: 30 | webhooks: 31 | urls: 32 | - https://webhooks.gitter.im/e/52ad02845e880cdca2cf 33 | on_success: change 34 | on_failure: always 35 | on_start: never 36 | email: 37 | - tiago4orion@gmail.com 38 | - tiagokatcipis@gmail.com 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12 2 | 3 | ADD . /go/src/github.com/madlambda/nash 4 | 5 | ENV NASHPATH /nashpath 6 | ENV NASHROOT /nashroot 7 | 8 | RUN cd /go/src/github.com/madlambda/nash && \ 9 | make install 10 | 11 | CMD ["/nashroot/bin/nash"] 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifndef version 2 | version=$(shell git rev-list -1 HEAD) 3 | endif 4 | 5 | buildargs = -ldflags "-X main.VersionString=$(version)" -v 6 | 7 | all: build test install 8 | 9 | build: 10 | cd cmd/nash && go build $(buildargs) 11 | cd cmd/nashfmt && go build $(buildargs) 12 | cd stdbin/mkdir && go build $(buildargs) 13 | cd stdbin/pwd && go build $(buildargs) 14 | cd stdbin/write && go build $(buildargs) 15 | cd stdbin/strings && go build $(buildargs) 16 | 17 | NASHPATH?=$(HOME)/nash 18 | NASHROOT?=$(HOME)/nashroot 19 | 20 | # FIXME: binaries install do not work on windows this way (the .exe) 21 | install: build 22 | @echo 23 | @echo "Installing nash at: "$(NASHROOT) 24 | mkdir -p $(NASHROOT)/bin 25 | rm -f $(NASHROOT)/bin/nash 26 | rm -f $(NASHROOT)/bin/nashfmt 27 | cp -p ./cmd/nash/nash $(NASHROOT)/bin 28 | cp -p ./cmd/nashfmt/nashfmt $(NASHROOT)/bin 29 | rm -rf $(NASHROOT)/stdlib 30 | cp -pr ./stdlib $(NASHROOT)/stdlib 31 | cp -pr ./stdbin/mkdir/mkdir $(NASHROOT)/bin/mkdir 32 | cp -pr ./stdbin/pwd/pwd $(NASHROOT)/bin/pwd 33 | cp -pr ./stdbin/write/write $(NASHROOT)/bin/write 34 | cp -pr ./stdbin/strings/strings $(NASHROOT)/bin/strings 35 | 36 | docsdeps: 37 | go get github.com/madlambda/mdtoc/cmd/mdtoc 38 | 39 | docs: docsdeps 40 | mdtoc -w ./README.md 41 | mdtoc -w ./docs/interactive.md 42 | mdtoc -w ./docs/reference.md 43 | mdtoc -w ./docs/stdlib/fmt.md 44 | 45 | test: build 46 | ./hack/check.sh 47 | 48 | update-vendor: 49 | cd cmd/nash && nash ./vendor.sh 50 | 51 | release: clean 52 | ./hack/releaser.sh $(version) 53 | 54 | coverage-html: test 55 | go tool cover -html=coverage.txt -o coverage.html 56 | @echo "coverage file: coverage.html" 57 | 58 | coverage-show: coverage-html 59 | xdg-open coverage.html 60 | 61 | clean: 62 | rm -f cmd/nash/nash 63 | rm -f cmd/nashfmt/nashfmt 64 | rm -rf dist 65 | -------------------------------------------------------------------------------- /_disabled_appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "1.0.0.{build}" 2 | platform: x64 3 | clone_folder: "c:\\gopath\\src\\github.com\\madlambda\\nash" 4 | environment: 5 | GOPATH: "c:\\gopath" 6 | install: 7 | - "echo %PATH%" 8 | - "echo %GOPATH%" 9 | - "set PATH=%GOPATH%\\bin;c:\\go\\bin;c:\\MinGW\\bin;%PATH%" 10 | - "go version" 11 | - "go env" 12 | - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe 13 | - choco install cygwin 14 | - set PATH=C:\\cygwin64\\bin;%PATH% 15 | 16 | build_script: 17 | - make build 18 | - make test 19 | 20 | notifications: 21 | - provider: GitHubPullRequest 22 | auth_token: 23 | secure: QuTLyXQp/4bQNeeEe5DLt9NIt/TzmZkn87s6wfOWpELX1L5UJyRCKV8AJitZWgwv 24 | template: "{{#passed}}:white_check_mark:{{/passed}}{{#failed}}:x:{{/failed}} [Build {{&projectName}} {{buildVersion}} {{status}}]({{buildUrl}}) (commit {{commitUrl}} by @{{&commitAuthorUsername}})" 25 | -------------------------------------------------------------------------------- /ast/doc_test.go: -------------------------------------------------------------------------------- 1 | package ast_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/madlambda/nash/ast" 7 | "github.com/madlambda/nash/token" 8 | ) 9 | 10 | func Example_AssignmentNode() { 11 | one := ast.NewNameNode(token.NewFileInfo(1, 0), "one", nil) 12 | two := ast.NewNameNode(token.NewFileInfo(1, 4), "two", nil) 13 | value1 := ast.NewStringExpr(token.NewFileInfo(1, 8), "1", true) 14 | value2 := ast.NewStringExpr(token.NewFileInfo(1, 10), "2", true) 15 | assign := ast.NewAssignNode(token.NewFileInfo(1, 0), 16 | []*ast.NameNode{one, two}, 17 | []ast.Expr{value1, value2}, 18 | ) 19 | 20 | fmt.Printf("%s", assign) 21 | 22 | // Output: one, two = "1", "2" 23 | } 24 | 25 | func Example_AssignmentNode_Single() { 26 | operatingSystems := ast.NewNameNode(token.NewFileInfo(1, 0), "operatingSystems", nil) 27 | values := []ast.Expr{ 28 | ast.NewStringExpr(token.NewFileInfo(1, 19), "plan9 from bell labs", true), 29 | ast.NewStringExpr(token.NewFileInfo(2, 19), "unix", true), 30 | ast.NewStringExpr(token.NewFileInfo(3, 19), "linux", true), 31 | ast.NewStringExpr(token.NewFileInfo(4, 19), "oberon", true), 32 | ast.NewStringExpr(token.NewFileInfo(5, 19), "windows", true), 33 | } 34 | 35 | list := ast.NewListExpr(token.NewFileInfo(0, 18), values) 36 | assign := ast.NewSingleAssignNode(token.NewFileInfo(1, 0), 37 | operatingSystems, 38 | list, 39 | ) 40 | 41 | fmt.Printf("%s", assign) 42 | 43 | // Output: operatingSystems = ( 44 | // "plan9 from bell labs" 45 | // "unix" 46 | // "linux" 47 | // "oberon" 48 | // "windows" 49 | // ) 50 | } 51 | -------------------------------------------------------------------------------- /ast/node_fmt_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/madlambda/nash/token" 7 | ) 8 | 9 | func testPrinter(t *testing.T, node Node, expected string) { 10 | if node.String() != expected { 11 | t.Errorf("Values differ: '%s' != '%s'", node, expected) 12 | } 13 | } 14 | 15 | func TestAstPrinterStringExpr(t *testing.T) { 16 | for _, testcase := range []struct { 17 | expected string 18 | node Node 19 | }{ 20 | // quote 21 | { 22 | expected: `"\""`, 23 | node: NewStringExpr(token.NewFileInfo(1, 0), "\"", true), 24 | }, 25 | 26 | // escape 27 | { 28 | expected: `"\\this is a test\n"`, 29 | node: NewStringExpr(token.NewFileInfo(1, 0), "\\this is a test\n", true), 30 | }, 31 | 32 | // tab 33 | { 34 | expected: `"this is a test\t"`, 35 | node: NewStringExpr(token.NewFileInfo(1, 0), "this is a test\t", true), 36 | }, 37 | 38 | // linefeed 39 | { 40 | expected: `"this is a test\n"`, 41 | node: NewStringExpr(token.NewFileInfo(1, 0), "this is a test\n", true), 42 | }, 43 | { 44 | expected: `"\nthis is a test"`, 45 | node: NewStringExpr(token.NewFileInfo(1, 0), "\nthis is a test", true), 46 | }, 47 | { 48 | expected: `"\n\n\n"`, 49 | node: NewStringExpr(token.NewFileInfo(1, 0), "\n\n\n", true), 50 | }, 51 | 52 | // carriege return 53 | { 54 | expected: `"this is a test\r"`, 55 | node: NewStringExpr(token.NewFileInfo(1, 0), "this is a test\r", true), 56 | }, 57 | { 58 | expected: `"\rthis is a test"`, 59 | node: NewStringExpr(token.NewFileInfo(1, 0), "\rthis is a test", true), 60 | }, 61 | { 62 | expected: `"\r\r\r"`, 63 | node: NewStringExpr(token.NewFileInfo(1, 0), "\r\r\r", true), 64 | }, 65 | } { 66 | testPrinter(t, testcase.node, testcase.expected) 67 | } 68 | } 69 | 70 | func TestASTPrinterAssignment(t *testing.T) { 71 | zeroFileInfo := token.NewFileInfo(1, 0) 72 | 73 | for _, testcase := range []struct { 74 | expected string 75 | node Node 76 | }{ 77 | { 78 | expected: `a = "1"`, 79 | node: NewAssignNode(zeroFileInfo, []*NameNode{ 80 | NewNameNode(zeroFileInfo, "a", nil), 81 | }, []Expr{NewStringExpr(zeroFileInfo, "1", true)}), 82 | }, 83 | { 84 | expected: `a = ()`, 85 | node: NewAssignNode(zeroFileInfo, []*NameNode{ 86 | NewNameNode(zeroFileInfo, "a", nil), 87 | }, []Expr{NewListExpr(zeroFileInfo, []Expr{})}), 88 | }, 89 | { 90 | expected: `a, b = (), ()`, 91 | node: NewAssignNode(zeroFileInfo, []*NameNode{ 92 | NewNameNode(zeroFileInfo, "a", nil), 93 | NewNameNode(zeroFileInfo, "b", nil), 94 | }, []Expr{NewListExpr(zeroFileInfo, []Expr{}), 95 | NewListExpr(zeroFileInfo, []Expr{})}), 96 | }, 97 | { 98 | expected: `a, b = "1", "2"`, 99 | node: NewAssignNode(zeroFileInfo, []*NameNode{ 100 | NewNameNode(zeroFileInfo, "a", nil), 101 | NewNameNode(zeroFileInfo, "b", nil), 102 | }, []Expr{NewStringExpr(zeroFileInfo, "1", true), 103 | NewStringExpr(zeroFileInfo, "2", true)}), 104 | }, 105 | } { 106 | testPrinter(t, testcase.node, testcase.expected) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ast/nodetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=NodeType"; DO NOT EDIT 2 | 3 | package ast 4 | 5 | import "fmt" 6 | 7 | const _NodeType_name = "NodeSetenvNodeBlockNodeNameNodeAssignNodeExecAssignNodeImportexecBeginNodeCommandNodePipeNodeRedirectNodeFnInvexecEndexpressionBeginNodeStringExprNodeIntExprNodeVarExprNodeListExprNodeIndexExprNodeConcatExprexpressionEndNodeStringNodeRforkNodeRforkFlagsNodeIfNodeCommentNodeFnArgNodeVarAssignDeclNodeVarExecAssignDeclNodeFnDeclNodeReturnNodeBindFnNodeDumpNodeFor" 8 | 9 | var _NodeType_index = [...]uint16{0, 10, 19, 27, 37, 51, 61, 70, 81, 89, 101, 110, 117, 132, 146, 157, 168, 180, 193, 207, 220, 230, 239, 253, 259, 270, 279, 296, 317, 327, 337, 347, 355, 362} 10 | 11 | func (i NodeType) String() string { 12 | i -= 1 13 | if i < 0 || i >= NodeType(len(_NodeType_index)-1) { 14 | return fmt.Sprintf("NodeType(%d)", i+1) 15 | } 16 | return _NodeType_name[_NodeType_index[i]:_NodeType_index[i+1]] 17 | } 18 | -------------------------------------------------------------------------------- /ast/tree.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type ( 4 | // Tree is the AST 5 | Tree struct { 6 | Name string 7 | Root *BlockNode // top-level root of the tree. 8 | } 9 | ) 10 | 11 | // NewTree creates a new AST tree 12 | func NewTree(name string) *Tree { 13 | return &Tree{ 14 | Name: name, 15 | } 16 | } 17 | 18 | func (t *Tree) IsEqual(other *Tree) bool { 19 | if t == other { 20 | return true 21 | } 22 | 23 | return t.Root.IsEqual(other.Root) 24 | } 25 | 26 | func (tree *Tree) String() string { 27 | if tree.Root == nil { 28 | return "" 29 | } 30 | 31 | if len(tree.Root.Nodes) == 0 { 32 | return "" 33 | } 34 | 35 | return tree.Root.String() 36 | } 37 | -------------------------------------------------------------------------------- /ast/tree_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/madlambda/nash/token" 7 | ) 8 | 9 | // Test API 10 | func TestTreeCreation(t *testing.T) { 11 | tr := NewTree("example") 12 | 13 | if tr.Name != "example" { 14 | t.Errorf("Invalid name") 15 | return 16 | } 17 | } 18 | 19 | func TestTreeRawCreation(t *testing.T) { 20 | tr := NewTree("creating a tree by hand") 21 | 22 | ln := NewBlockNode(token.NewFileInfo(1, 0)) 23 | rfarg := NewStringExpr(token.NewFileInfo(1, 0), "unp", false) 24 | 25 | r := NewRforkNode(token.NewFileInfo(1, 0)) 26 | r.SetFlags(rfarg) 27 | ln.Push(r) 28 | 29 | tr.Root = ln 30 | 31 | if tr.String() != "rfork unp" { 32 | t.Error("Failed to build AST by hand") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/nash/completer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/madlambda/nash" 9 | "github.com/madlambda/nash/readline" 10 | "github.com/madlambda/nash/sh" 11 | ) 12 | 13 | var runes = readline.Runes{} 14 | 15 | type Completer struct { 16 | op *readline.Operation 17 | term *readline.Terminal 18 | sh *nash.Shell 19 | } 20 | 21 | func NewCompleter(op *readline.Operation, term *readline.Terminal, sh *nash.Shell) *Completer { 22 | return &Completer{op, term, sh} 23 | } 24 | 25 | func (c *Completer) Do(line []rune, pos int) ([][]rune, int) { 26 | const op = "Completer.Do" 27 | 28 | defer c.op.Refresh() 29 | defer c.term.PauseRead(false) 30 | 31 | fnDef, err := c.sh.GetFn("nash_complete") 32 | if err != nil { 33 | c.Log(op, "skipping autocompletion") 34 | return [][]rune{[]rune{'\t'}}, 0 35 | } 36 | 37 | nashFunc := fnDef.Build() 38 | lineArg := sh.NewStrObj(string(line)) 39 | posArg := sh.NewStrObj(strconv.Itoa(pos)) 40 | err = nashFunc.SetArgs([]sh.Obj{lineArg, posArg}) 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "%s:error setting args on autocomplete function:%v\n", op, err) 43 | return nil, 0 44 | } 45 | 46 | nashFunc.SetStdin(c.sh.Stdin()) 47 | nashFunc.SetStdout(c.sh.Stdout()) 48 | nashFunc.SetStderr(c.sh.Stderr()) 49 | 50 | if err = nashFunc.Start(); err != nil { 51 | fmt.Fprintf(os.Stderr, "%s:error starting autocomplete function:%v\n", op, err) 52 | return nil, 0 53 | } 54 | 55 | if err = nashFunc.Wait(); err != nil { 56 | fmt.Fprintf(os.Stderr, "%s:error waiting for autocomplete function:%v\n", op, err) 57 | return nil, 0 58 | } 59 | 60 | ret := nashFunc.Results() 61 | 62 | if len(ret) != 1 || ret[0].Type() != sh.ListType { 63 | fmt.Fprintf(os.Stderr, "%s:ignoring unexpected autocomplete func return (expected list):%+v\n", op, ret) 64 | return nil, 0 65 | } 66 | 67 | retlist := ret[0].(*sh.ListObj) 68 | 69 | if len(retlist.List()) != 2 { 70 | c.Log(op, "no results from autocomplete") 71 | return nil, pos 72 | } 73 | 74 | newline := retlist.List()[0] 75 | newpos := retlist.List()[1] 76 | 77 | if newline.Type() != sh.StringType || newpos.Type() != sh.StringType { 78 | fmt.Fprintf(os.Stderr, "%s:ignoring autocomplete value:(%s) (%s)\n", op, newline, newpos) 79 | return nil, 0 80 | } 81 | 82 | objline := newline.(*sh.StrObj) 83 | objpos := newpos.(*sh.StrObj) 84 | 85 | c.Log(op, "autocomplete result:line %q:pos %q", objline, objpos) 86 | 87 | offset, err := strconv.Atoi(objpos.Str()) 88 | 89 | if err != nil { 90 | fmt.Fprintf(os.Stderr, "%s:autocomplete func returned non number position:%v\n", op, err) 91 | return nil, 0 92 | } 93 | 94 | c.Log(op, "success:line %q:offset %d", objline, offset) 95 | return [][]rune{[]rune(objline.Str())}, offset 96 | } 97 | 98 | func (c *Completer) Log(op string, format string, args ...interface{}) { 99 | c.sh.Log(op+":"+format, args...) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/nash/env.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func NashPath() (string, error) { 10 | nashpath := os.Getenv("NASHPATH") 11 | if nashpath != "" { 12 | return nashpath, nil 13 | } 14 | h, err := home() 15 | return filepath.Join(h, "nash"), err 16 | } 17 | 18 | func NashRoot() (string, error) { 19 | nashroot, ok := os.LookupEnv("NASHROOT") 20 | if ok { 21 | return nashroot, nil 22 | } 23 | 24 | h, err := home() 25 | return filepath.Join(h, "nashroot"), err 26 | } 27 | 28 | func home() (string, error) { 29 | homedir, err := os.UserHomeDir() 30 | if err != nil { 31 | return "", err 32 | } 33 | if homedir == "" { 34 | return "", errors.New("invalid empty home dir") 35 | } 36 | return homedir, nil 37 | } 38 | -------------------------------------------------------------------------------- /cmd/nash/env_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | main "github.com/madlambda/nash/cmd/nash" 10 | ) 11 | 12 | // TODO: No idea on how to inject failures like empty HOME folders for now 13 | 14 | func TestLoadNASHPATH(t *testing.T) { 15 | 16 | defaultNashPath := filepath.Join(home(t), "nash") 17 | 18 | runTests(t, main.NashPath, []EnvTest{ 19 | { 20 | name: "Exported", 21 | env: map[string]string{ 22 | "NASHPATH": filepath.Join("etc", "nash"), 23 | }, 24 | want: filepath.Join("etc", "nash"), 25 | }, 26 | { 27 | name: "IgnoresNASHROOT", 28 | env: map[string]string{ 29 | "NASHROOT": "/etc/nashroot/tests", 30 | "HOME": home(t), 31 | }, 32 | want: defaultNashPath, 33 | }, 34 | { 35 | name: "UseUserHomeWhenUnset", 36 | env: map[string]string{ 37 | "NASHROOT": "/etc/nashroot/tests", 38 | "HOME": home(t), 39 | }, 40 | want: defaultNashPath, 41 | }, 42 | }) 43 | } 44 | 45 | func TestLoadNASHROOT(t *testing.T) { 46 | defaultNashRoot := filepath.Join(home(t), "nashroot") 47 | runTests(t, main.NashRoot, []EnvTest{ 48 | { 49 | name: "Exported", 50 | env: map[string]string{ 51 | "NASHROOT": filepath.Join("etc", "nashroot"), 52 | }, 53 | want: filepath.Join("etc", "nashroot"), 54 | }, 55 | { 56 | name: "IgnoresGOPATHIfSet", 57 | env: map[string]string{ 58 | "GOPATH": filepath.Join("go", "natel", "review"), 59 | "NASHROOT": filepath.Join("nashroot", "ignoredgopath"), 60 | }, 61 | want: filepath.Join("nashroot", "ignoredgopath"), 62 | }, 63 | { 64 | name: "UsesHOMEevenWhenGOPATHIsSet", 65 | env: map[string]string{ 66 | "HOME": home(t), 67 | "GOPATH": filepath.Join("go", "path"), 68 | }, 69 | want: defaultNashRoot, 70 | }, 71 | { 72 | name: "UsesUserHomeWhenNASHROOTAndGOPATHAreUnset", 73 | env: map[string]string{ 74 | "HOME": home(t), 75 | }, 76 | want: filepath.Join(home(t), "nashroot"), 77 | }, 78 | }) 79 | } 80 | 81 | func runTests(t *testing.T, testfunc func() (string, error), cases []EnvTest) { 82 | 83 | t.Helper() 84 | 85 | for _, c := range cases { 86 | t.Run(c.name, func(t *testing.T) { 87 | restore := clearenv(t) 88 | defer restore() 89 | 90 | export(t, c.env) 91 | got, err := testfunc() 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | if got != c.want { 96 | t.Fatalf("got[%s] != want[%s]", got, c.want) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | type EnvTest struct { 103 | name string 104 | env map[string]string 105 | want string 106 | } 107 | 108 | func clearenv(t *testing.T) func() { 109 | env := os.Environ() 110 | os.Clearenv() 111 | 112 | return func() { 113 | for _, envvar := range env { 114 | parsed := strings.Split(envvar, "=") 115 | name := parsed[0] 116 | val := strings.Join(parsed[1:], "=") 117 | 118 | err := os.Setenv(name, val) 119 | if err != nil { 120 | t.Fatalf("error[%s] restoring env var[%s]", err, envvar) 121 | } 122 | } 123 | } 124 | } 125 | 126 | func export(t *testing.T, env map[string]string) { 127 | t.Helper() 128 | 129 | for name, val := range env { 130 | err := os.Setenv(name, val) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | } 135 | } 136 | 137 | func home(t *testing.T) string { 138 | t.Helper() 139 | 140 | homedir, err := os.UserHomeDir() 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | return homedir 145 | } 146 | -------------------------------------------------------------------------------- /cmd/nash/example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | -rm -rf rootfs 4 | 5 | rfork upmis { 6 | mount -t proc proc /proc 7 | mkdir -p rootfs 8 | mount -t tmpfs -o size=1G tmpfs rootfs 9 | 10 | cd rootfs 11 | 12 | wget "https://busybox.net/downloads/binaries/latest/busybox-x86_64" -O busybox 13 | chmod +x busybox 14 | 15 | mkdir bin 16 | 17 | ./busybox --install ./bin 18 | 19 | mkdir -p proc 20 | mkdir -p dev 21 | mount -t proc proc proc 22 | mount -t tmpfs tmpfs dev 23 | 24 | cp ../nash . 25 | chroot . /bin/sh 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nash/install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func NashLibDir(nashpath string) string { 12 | //FIXME: This is sadly duplicated from the shell implementation =( 13 | return filepath.Join(nashpath, "lib") 14 | } 15 | 16 | func InstallLib(nashpath string, sourcepath string) error { 17 | nashlibdir := NashLibDir(nashpath) 18 | sourcepathAbs, err := filepath.Abs(sourcepath) 19 | if err != nil { 20 | return fmt.Errorf("error[%s] getting absolute path of [%s]", err, sourcepath) 21 | } 22 | if filepath.HasPrefix(sourcepathAbs, nashlibdir) { 23 | return fmt.Errorf( 24 | "lib source path[%s] can't be inside nash lib dir[%s]", sourcepath, nashlibdir) 25 | } 26 | return installLib(nashlibdir, sourcepathAbs) 27 | } 28 | 29 | func installLib(targetdir string, sourcepath string) error { 30 | f, err := os.Stat(sourcepath) 31 | if err != nil { 32 | return fmt.Errorf("error[%s] checking if path[%s] is dir", err, sourcepath) 33 | } 34 | 35 | if !f.IsDir() { 36 | return copyfile(targetdir, sourcepath) 37 | } 38 | 39 | basedir := filepath.Base(sourcepath) 40 | targetdir = filepath.Join(targetdir, basedir) 41 | 42 | files, err := ioutil.ReadDir(sourcepath) 43 | if err != nil { 44 | return fmt.Errorf("error[%s] reading dir[%s]", err, sourcepath) 45 | } 46 | 47 | for _, file := range files { 48 | err := installLib(targetdir, filepath.Join(sourcepath, file.Name())) 49 | if err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func copyfile(targetdir string, sourcefilepath string) error { 57 | fail := func(err error) error { 58 | return fmt.Errorf( 59 | "error[%s] trying to copy file[%s] to [%s]", err, sourcefilepath, targetdir) 60 | } 61 | 62 | err := os.MkdirAll(targetdir, os.ModePerm) 63 | if err != nil { 64 | return fail(err) 65 | } 66 | 67 | sourcefile, err := os.Open(sourcefilepath) 68 | if err != nil { 69 | return fail(err) 70 | } 71 | defer sourcefile.Close() 72 | 73 | targetfilepath := filepath.Join(targetdir, filepath.Base(sourcefilepath)) 74 | targetfile, err := os.Create(targetfilepath) 75 | if err != nil { 76 | return fail(err) 77 | } 78 | defer targetfile.Close() 79 | 80 | _, err = io.Copy(targetfile, sourcefile) 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /cmd/nash/main.go: -------------------------------------------------------------------------------- 1 | // Package main has two sides: 2 | // - User mode: shell 3 | // - tool mode: unix socket server for handling namespace operations 4 | // When started, the program choses their side based on the argv[0]. 5 | // The name "nash" indicates a user shell and the name -nashd- indicates 6 | // the namespace server tool. 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "os" 13 | 14 | "github.com/madlambda/nash" 15 | ) 16 | 17 | var ( 18 | // version is set at build time 19 | VersionString = "No version provided" 20 | 21 | version bool 22 | debug bool 23 | file string 24 | command string 25 | addr string 26 | noInit bool 27 | interactive bool 28 | install string 29 | ) 30 | 31 | func init() { 32 | flag.BoolVar(&version, "version", false, "Show version") 33 | flag.BoolVar(&debug, "debug", false, "enable debug") 34 | flag.BoolVar(&noInit, "noinit", false, "do not load init/init.sh file") 35 | flag.StringVar(&command, "c", "", "command to execute") 36 | flag.StringVar(&install, "install", "", "path of the library that you want to install (can be a single file)") 37 | flag.BoolVar(&interactive, "i", false, "Interactive mode (default if no args)") 38 | 39 | if os.Args[0] == "-nashd-" || (len(os.Args) > 1 && os.Args[1] == "-daemon") { 40 | flag.Bool("daemon", false, "force enable nashd mode") 41 | flag.StringVar(&addr, "addr", "", "rcd unix file") 42 | } 43 | } 44 | 45 | func main() { 46 | var args []string 47 | var shell *nash.Shell 48 | var err error 49 | 50 | flag.Parse() 51 | 52 | if version { 53 | fmt.Printf("build tag: %s\n", VersionString) 54 | return 55 | } 56 | 57 | if install != "" { 58 | fmt.Printf("installing library located at [%s]\n", install) 59 | np, err := NashPath() 60 | if err != nil { 61 | fmt.Printf("error[%s] getting NASHPATH, cant install library\n", err) 62 | os.Exit(1) 63 | } 64 | err = InstallLib(np, install) 65 | if err != nil { 66 | fmt.Printf("error[%s] installing library\n", err) 67 | os.Exit(1) 68 | } 69 | fmt.Println("installed with success") 70 | return 71 | } 72 | 73 | if len(flag.Args()) > 0 { 74 | args = flag.Args() 75 | file = args[0] 76 | } 77 | 78 | if shell, err = initShell(); err != nil { 79 | goto Error 80 | } 81 | 82 | shell.SetDebug(debug) 83 | 84 | if addr != "" { 85 | startNashd(shell, addr) 86 | return 87 | } 88 | 89 | if (file == "" && command == "") || interactive { 90 | if err = cli(shell); err != nil { 91 | goto Error 92 | } 93 | 94 | return 95 | } 96 | 97 | if file != "" { 98 | if err = shell.ExecFile(file, args...); err != nil { 99 | goto Error 100 | } 101 | } 102 | 103 | if command != "" { 104 | err = shell.ExecuteString("", command) 105 | if err != nil { 106 | goto Error 107 | } 108 | } 109 | 110 | Error: 111 | if err != nil { 112 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 113 | os.Exit(1) 114 | } 115 | } 116 | 117 | func initShell() (*nash.Shell, error) { 118 | 119 | nashpath, err := NashPath() 120 | if err != nil { 121 | return nil, err 122 | } 123 | nashroot, err := NashRoot() 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | os.Mkdir(nashpath, 0755) 129 | return nash.New(nashpath, nashroot) 130 | } 131 | -------------------------------------------------------------------------------- /cmd/nash/rpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "os" 8 | 9 | "github.com/madlambda/nash" 10 | ) 11 | 12 | func serveConn(sh *nash.Shell, conn net.Conn) { 13 | var data [1024]byte 14 | 15 | for { 16 | 17 | n, err := conn.Read(data[:]) 18 | 19 | if err != nil { 20 | if err == io.EOF { 21 | return 22 | } 23 | 24 | fmt.Printf("Failed to read data: %s", err.Error()) 25 | return 26 | } 27 | 28 | if string(data[0:n]) == "quit" { 29 | return 30 | } 31 | 32 | err = sh.ExecuteString("-nashd-", string(data[0:n])) 33 | 34 | if err != nil { 35 | fmt.Printf("nashd: %s\n", err.Error()) 36 | 37 | _, err = conn.Write([]byte("1")) 38 | 39 | if err != nil { 40 | fmt.Printf("Failed to send command status.\n") 41 | return 42 | } 43 | } else { 44 | _, err = conn.Write([]byte("0")) 45 | 46 | if err != nil { 47 | fmt.Printf("Failed to send command status.\n") 48 | return 49 | } 50 | } 51 | } 52 | } 53 | 54 | func startNashd(sh *nash.Shell, socketPath string) { 55 | os.Remove(socketPath) 56 | 57 | addr := &net.UnixAddr{ 58 | Net: "unix", 59 | Name: socketPath, 60 | } 61 | 62 | listener, err := net.ListenUnix("unix", addr) 63 | 64 | if err != nil { 65 | fmt.Printf("ERROR: %s\n", err.Error()) 66 | return 67 | } 68 | 69 | // Accept only one connection 70 | conn, err := listener.AcceptUnix() 71 | 72 | if err != nil { 73 | fmt.Printf("ERROR: %v", err.Error()) 74 | } 75 | 76 | serveConn(sh, conn) 77 | listener.Close() 78 | } 79 | -------------------------------------------------------------------------------- /cmd/nashfmt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | 10 | "github.com/madlambda/nash/parser" 11 | ) 12 | 13 | var ( 14 | overwrite bool 15 | version bool 16 | // version is set at build time 17 | VersionString = "No version provided" 18 | ) 19 | 20 | func init() { 21 | flag.BoolVar(&overwrite, "w", false, "overwrite file") 22 | flag.BoolVar(&version, "version", false, "Show version") 23 | } 24 | 25 | func main() { 26 | var ( 27 | file io.ReadCloser 28 | err error 29 | ) 30 | 31 | flag.Parse() 32 | 33 | if version { 34 | fmt.Printf("build tag: %s\n", VersionString) 35 | return 36 | } 37 | 38 | if len(flag.Args()) <= 0 { 39 | flag.PrintDefaults() 40 | return 41 | } 42 | 43 | fname := flag.Args()[0] 44 | 45 | file, err = os.Open(fname) 46 | 47 | if err != nil { 48 | fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) 49 | os.Exit(1) 50 | } 51 | 52 | content, err := ioutil.ReadAll(file) 53 | 54 | if err != nil { 55 | file.Close() 56 | fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) 57 | os.Exit(1) 58 | } 59 | 60 | parser := parser.NewParser("nashfmt", string(content)) 61 | 62 | ast, err := parser.Parse() 63 | 64 | if err != nil { 65 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 66 | file.Close() 67 | os.Exit(1) 68 | } 69 | 70 | file.Close() 71 | 72 | if !overwrite { 73 | fmt.Printf("%s\n", ast.String()) 74 | return 75 | } 76 | 77 | if ast.String() != string(content) { 78 | err = ioutil.WriteFile(fname, []byte(fmt.Sprintf("%s\n", ast.String())), 0666) 79 | 80 | if err != nil { 81 | fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) 82 | return 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/stdlib/fmt.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Table of Contents 4 | 5 | - [fmt](#fmt) 6 | - [fmt_println](#fmtprintln) 7 | 8 | 9 | 10 | # fmt 11 | 12 | ## fmt_println 13 | 14 | Same behavior as print but adds a newline on the end. 15 | 16 | ```nash 17 | import fmt 18 | 19 | fmt_println("hi") 20 | #Output:"hi\n" 21 | ``` 22 | -------------------------------------------------------------------------------- /errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/madlambda/nash/ast" 7 | "github.com/madlambda/nash/scanner" 8 | ) 9 | 10 | type ( 11 | NashError struct { 12 | reason string 13 | format string 14 | } 15 | 16 | unfinished struct{} 17 | 18 | unfinishedBlockError struct { 19 | *NashError 20 | unfinished 21 | } 22 | 23 | unfinishedListError struct { 24 | *NashError 25 | unfinished 26 | } 27 | 28 | unfinishedCmdError struct { 29 | *NashError 30 | unfinished 31 | } 32 | ) 33 | 34 | func NewError(format string, arg ...interface{}) *NashError { 35 | e := &NashError{} 36 | e.SetReason(format, arg...) 37 | return e 38 | } 39 | 40 | func NewEvalError(path string, node ast.Node, format string, arg ...interface{}) *NashError { 41 | linenum := fmt.Sprintf("%s:%d:%d: ", path, node.Line(), node.Column()) 42 | return NewError(linenum+format, arg...) 43 | } 44 | 45 | func (e *NashError) SetReason(format string, arg ...interface{}) { 46 | e.reason = fmt.Sprintf(format, arg...) 47 | } 48 | 49 | func (e *NashError) Error() string { return e.reason } 50 | 51 | func (e unfinished) Unfinished() bool { return true } 52 | 53 | func NewUnfinishedBlockError(name string, it scanner.Token) error { 54 | return &unfinishedBlockError{ 55 | NashError: NewError("%s:%d:%d: Statement's block '{' not finished", 56 | name, it.Line(), it.Column()), 57 | } 58 | } 59 | 60 | func NewUnfinishedListError(name string, it scanner.Token) error { 61 | return &unfinishedListError{ 62 | NashError: NewError("%s:%d:%d: List assignment not finished. Found %v", 63 | name, it.Line(), it.Column(), it), 64 | } 65 | } 66 | 67 | func NewUnfinishedCmdError(name string, it scanner.Token) error { 68 | return &unfinishedCmdError{ 69 | NashError: NewError("%s:%d:%d: Multi-line command not finished. Found %v but expect ')'", 70 | name, it.Line(), it.Column(), it), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/append.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | var example_list = () 4 | echo "appending string 1" 5 | example_list <= append($example_list, "1") 6 | echo $example_list 7 | echo "appending string 2" 8 | example_list <= append($example_list, "2") 9 | echo $example_list 10 | -------------------------------------------------------------------------------- /examples/args.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | print("iterating through the arguments list\n\n") 4 | for arg in $ARGS { 5 | print("%s\n", $arg) 6 | } 7 | 8 | print("\n") 9 | print("acessing individual parameter\n") 10 | var somearg = $ARGS[0] 11 | print("%s\n", $somearg) 12 | -------------------------------------------------------------------------------- /examples/init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | # Simple init script for you base your own 4 | # For a more complete and organized .nash see: 5 | # https://github.com/tiago4orion/dotnash 6 | 7 | # PROMPT is a special variable used by nash command line to 8 | # setup your prompt. 9 | var RED = "" 10 | var GREEN = "" 11 | var RESET = "" 12 | var PROMPTSYM = "λ> " 13 | var DEFPROMPT = $RED+$PROMPTSYM+$RESET 14 | 15 | setenv PROMPT = $DEFPROMPT 16 | 17 | # cd overrides built-in cd 18 | # Add the current branch before prompt (if current directory is a git repo) 19 | fn cd(path) { 20 | var branch = "" 21 | var prompt = "" 22 | 23 | if $path == "" { 24 | path = $HOME 25 | } 26 | 27 | chdir($path) 28 | 29 | var _, status <= test -d ./.git 30 | 31 | if $status != "0" { 32 | prompt = $DEFPROMPT 33 | } else { 34 | branch <= git rev-parse --abbrev-ref HEAD | xargs echo -n 35 | 36 | prompt = "("+$GREEN+$branch+$RESET+")"+$DEFPROMPT 37 | } 38 | 39 | setenv PROMPT = $prompt 40 | } 41 | 42 | bindfn cd cd 43 | 44 | # syntax sugar to cd into go project 45 | fn gocd(path) { 46 | cd $GOPATH+"/src/"+$path 47 | } 48 | 49 | bindfn gocd gocd 50 | -------------------------------------------------------------------------------- /examples/len.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | echo "args: " 4 | echo $ARGS 5 | 6 | if len($ARGS) == "1" { 7 | echo "one parameter passed" 8 | } else { 9 | echo "more parameters passed" 10 | } 11 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package nash_test 2 | 3 | import ( 4 | "os" 5 | "io/ioutil" 6 | 7 | "github.com/madlambda/nash" 8 | ) 9 | 10 | func Example() { 11 | 12 | nashpath,cleanup := tmpdir() 13 | defer cleanup() 14 | 15 | nashroot, cleanup := tmpdir() 16 | defer cleanup() 17 | 18 | nash, err := nash.New(nashpath, nashroot) 19 | 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // Execute a script from string 25 | err = nash.ExecuteString("-input-", `echo Hello World`) 26 | 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // Output: Hello World 32 | } 33 | 34 | func tmpdir() (string, func()) { 35 | dir, err := ioutil.TempDir("", "nash-tests") 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | return dir, func() { 41 | err := os.RemoveAll(dir) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package nash 4 | 5 | import "github.com/madlambda/nash/parser" 6 | 7 | func Fuzz(data []byte) int { 8 | p := parser.NewParser("fuzz", string(data)) 9 | 10 | tree, err := p.Parse() 11 | 12 | if err != nil { 13 | if tree != nil { 14 | panic("tree != nil") 15 | } 16 | 17 | return 0 18 | } 19 | 20 | return 1 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/madlambda/nash 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/chzyer/logex v1.1.10 // indirect 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 8 | golang.org/x/exp v0.0.0-20200513190911-00229845015e 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 8 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 9 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 10 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 11 | golang.org/x/exp v0.0.0-20200513190911-00229845015e h1:rMqLP+9XLy+LdbCXHjJHAmTfXCr93W7oruWA6Hq1Alc= 12 | golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= 13 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 14 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 15 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 16 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 17 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 18 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 26 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 27 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | -------------------------------------------------------------------------------- /hack/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | go test -race -coverprofile=coverage.txt ./... 6 | 7 | echo "running stdlib and stdbin tests" 8 | tests=$(find ./stdlib ./stdbin -name "*_test.sh") 9 | 10 | for t in ${tests[*]} 11 | do 12 | echo 13 | echo "running test: "$t 14 | ./cmd/nash/nash $t 15 | echo "success" 16 | done 17 | -------------------------------------------------------------------------------- /hack/install/unix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | which wget >/dev/null || { echo "wget not installed"; exit 1; } 8 | which tar >/dev/null || { echo "tar not installed"; exit 1; } 9 | which tr >/dev/null || { echo "tr not found"; exit 1; } 10 | 11 | NASHROOT=${NASHROOT:-$HOME/nashroot} 12 | VERSION="v1.1" 13 | ARCH="amd64" 14 | OS="$(uname | tr '[:upper:]' '[:lower:]')" 15 | 16 | if [ $# -eq 1 ]; then 17 | VERSION=$1 18 | fi 19 | 20 | echo "installing nash (${OS}): ${VERSION} at NASHROOT: ${NASHROOT}" 21 | 22 | mkdir -p $NASHROOT 23 | cd $NASHROOT 24 | tarfile="nash-${VERSION}-${OS}-${ARCH}.tar.gz" 25 | wget -c https://github.com/madlambda/nash/releases/download/$VERSION/$tarfile 26 | tar xvfz $tarfile 27 | rm -f $tarfile 28 | -------------------------------------------------------------------------------- /hack/releaser.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | if len($ARGS) != "2" { 4 | print("usage: %s \n\n", $ARGS[0]) 5 | exit("1") 6 | } 7 | 8 | var version = $ARGS[1] 9 | var supported_os = ("linux" "darwin" "windows") 10 | var supported_arch = ("amd64") 11 | 12 | # Guarantee passing tests at least on the host arch/os 13 | make test 14 | mkdir -p dist 15 | 16 | fn prepare_execs(distfiles, os) { 17 | if $os == "windows" { 18 | var newfiles = () 19 | 20 | for distfile in $distfiles { 21 | var src = $distfile[0] 22 | var dst = $distfile[1] 23 | var newsrc = $src+".exe" 24 | var newdst = $dst+".exe" 25 | 26 | newfiles <= append($newfiles, ($newsrc $newdst)) 27 | } 28 | 29 | return $newfiles 30 | } 31 | if $os == "linux" { 32 | for distfile in $distfiles { 33 | strip $distfile[0] 34 | } 35 | } 36 | 37 | return $distfiles 38 | } 39 | 40 | for os in $supported_os { 41 | for arch in $supported_arch { 42 | setenv GOOS = $os 43 | setenv GOARCH = $arch 44 | 45 | if $os == "linux" { 46 | setenv CGO_ENABLED = "1" 47 | } else { 48 | setenv CGO_ENABLED = "0" 49 | } 50 | 51 | echo "building OS: "+$GOOS+" ARCH : "+$GOARCH 52 | make build "version="+$version 53 | 54 | var pkgdir <= mktemp -d 55 | 56 | var bindir = $pkgdir+"/bin" 57 | var stdlibdir = $pkgdir+"/stdlib" 58 | 59 | mkdir -p $bindir 60 | mkdir -p $stdlibdir 61 | 62 | var nash_src = "./cmd/nash/nash" 63 | var nash_dst = $bindir+"/nash" 64 | var nashfmt_src = "./cmd/nashfmt/nashfmt" 65 | var nashfmt_dst = $bindir+"/nashfmt" 66 | var execfiles = ( 67 | ($nash_src $nash_dst) 68 | ($nashfmt_src $nashfmt_dst) 69 | ) 70 | 71 | var execfiles <= prepare_execs($execfiles, $os) 72 | 73 | # TODO: Improve with glob, right now have only two packages =) 74 | var distfiles <= append($execfiles, ("./stdlib/io.sh" $stdlibdir)) 75 | 76 | distfiles <= append($distfiles, ("./stdlib/map.sh" $stdlibdir)) 77 | 78 | for distfile in $distfiles { 79 | var src = $distfile[0] 80 | var dst = $distfile[1] 81 | 82 | cp -pr $src $dst 83 | } 84 | 85 | var projectdir <= pwd 86 | var distar <= format("%s/dist/nash-%s-%s-%s.tar.gz", $projectdir, $version, $os, $arch) 87 | 88 | chdir($pkgdir) 89 | 90 | var pkgraw <= ls 91 | var pkgfiles <= split($pkgraw, "\n") 92 | 93 | tar cvfz $distar $pkgfiles 94 | 95 | chdir($projectdir) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/sh/builtin.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/madlambda/nash/errors" 8 | "github.com/madlambda/nash/internal/sh/builtin" 9 | "github.com/madlambda/nash/sh" 10 | ) 11 | 12 | type ( 13 | // builtinFn maps a builtin function to a nash sh.FnDef 14 | // avoiding a lot of duplicated code and decoupling the 15 | // builtin functions of some unnecessary details on how 16 | // the sh.Fn works (lots of complexity to provide features of 17 | // other kinds of runners/functions). 18 | builtinFn struct { 19 | stdin io.Reader 20 | stdout, stderr io.Writer 21 | 22 | done chan struct{} 23 | err error 24 | results []sh.Obj 25 | 26 | name string 27 | fn builtin.Fn 28 | } 29 | ) 30 | 31 | func NewBuiltinFn( 32 | name string, 33 | fn builtin.Fn, 34 | in io.Reader, 35 | out io.Writer, 36 | outerr io.Writer, 37 | ) *builtinFn { 38 | return &builtinFn{ 39 | name: name, 40 | fn: fn, 41 | stdin: in, 42 | stdout: out, 43 | stderr: outerr, 44 | } 45 | } 46 | 47 | func (f *builtinFn) Name() string { 48 | return f.name 49 | } 50 | 51 | func (f *builtinFn) ArgNames() []sh.FnArg { 52 | return f.fn.ArgNames() 53 | } 54 | 55 | func (f *builtinFn) Start() error { 56 | f.done = make(chan struct{}) 57 | 58 | go func() { 59 | f.results, f.err = f.fn.Run(f.stdin, f.stdout, f.stderr) 60 | f.done <- struct{}{} 61 | }() 62 | 63 | return nil 64 | } 65 | 66 | func (f *builtinFn) Wait() error { 67 | <-f.done 68 | return f.err 69 | } 70 | 71 | func (f *builtinFn) Results() []sh.Obj { 72 | return f.results 73 | } 74 | 75 | func (f *builtinFn) String() string { 76 | return fmt.Sprintf("", f.Name()) 77 | } 78 | 79 | func (f *builtinFn) SetArgs(args []sh.Obj) error { 80 | return f.fn.SetArgs(args) 81 | } 82 | 83 | func (f *builtinFn) SetEnviron(env []string) { 84 | // do nothing 85 | // terrible design smell having functions that do nothing =/ 86 | } 87 | 88 | func (f *builtinFn) SetStdin(r io.Reader) { 89 | f.stdin = r 90 | } 91 | func (f *builtinFn) SetStderr(w io.Writer) { 92 | f.stderr = w 93 | } 94 | func (f *builtinFn) SetStdout(w io.Writer) { 95 | f.stdout = w 96 | } 97 | func (f *builtinFn) StdoutPipe() (io.ReadCloser, error) { 98 | return nil, errors.NewError("builtin functions doesn't works with pipes") 99 | } 100 | func (f *builtinFn) Stdin() io.Reader { return f.stdin } 101 | func (f *builtinFn) Stdout() io.Writer { return f.stdout } 102 | func (f *builtinFn) Stderr() io.Writer { return f.stderr } 103 | -------------------------------------------------------------------------------- /internal/sh/builtin/append.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/madlambda/nash/errors" 7 | "github.com/madlambda/nash/sh" 8 | ) 9 | 10 | type ( 11 | appendFn struct { 12 | obj []sh.Obj 13 | args []sh.Obj 14 | } 15 | ) 16 | 17 | func newAppend() *appendFn { 18 | return &appendFn{} 19 | } 20 | 21 | func (appendfn *appendFn) ArgNames() []sh.FnArg { 22 | return []sh.FnArg{ 23 | sh.NewFnArg("list", false), 24 | sh.NewFnArg("value...", true), // variadic 25 | } 26 | } 27 | 28 | func (appendfn *appendFn) Run(in io.Reader, out io.Writer, err io.Writer) ([]sh.Obj, error) { 29 | newobj := append(appendfn.obj, appendfn.args...) 30 | return []sh.Obj{sh.NewListObj(newobj)}, nil 31 | } 32 | 33 | func (appendfn *appendFn) SetArgs(args []sh.Obj) error { 34 | if len(args) < 2 { 35 | return errors.NewError("append expects at least two arguments") 36 | } 37 | 38 | obj := args[0] 39 | if obj.Type() != sh.ListType { 40 | return errors.NewError("append expects a list as first argument, but a %s was provided", 41 | obj.Type()) 42 | } 43 | 44 | values := args[1:] 45 | if objlist, ok := obj.(*sh.ListObj); ok { 46 | appendfn.obj = objlist.List() 47 | appendfn.args = values 48 | return nil 49 | } 50 | 51 | return errors.NewError("internal error: object of wrong type") 52 | } 53 | -------------------------------------------------------------------------------- /internal/sh/builtin/append_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/madlambda/nash/internal/sh/internal/fixture" 8 | ) 9 | 10 | type testcase struct { 11 | name string 12 | code string 13 | expectedErr string 14 | expectedStdout string 15 | expectedStderr string 16 | } 17 | 18 | func testAppend(t *testing.T, tc testcase) { 19 | sh, teardown := fixture.SetupShell(t) 20 | defer teardown() 21 | 22 | var ( 23 | outb, errb bytes.Buffer 24 | ) 25 | sh.SetStdout(&outb) 26 | sh.SetStderr(&errb) 27 | 28 | err := sh.Exec(tc.name, tc.code) 29 | stdout := string(outb.Bytes()) 30 | stderr := errb.String() 31 | 32 | if stdout != tc.expectedStdout { 33 | t.Errorf("String differs: '%s' != '%s'", tc.expectedStdout, stdout) 34 | return 35 | } 36 | if stderr != tc.expectedStderr { 37 | t.Errorf("String differs: '%s' != '%s'", tc.expectedStderr, stderr) 38 | return 39 | } 40 | 41 | if err != nil { 42 | if err.Error() != tc.expectedErr { 43 | t.Fatalf("Expected err '%s' but got '%s'", tc.expectedErr, err.Error()) 44 | } 45 | } else if tc.expectedErr != "" { 46 | t.Fatalf("Expected err '%s' but err is nil", tc.expectedErr) 47 | } 48 | } 49 | 50 | func TestAppend(t *testing.T) { 51 | for _, tc := range []testcase{ 52 | { 53 | name: "no argument fails", 54 | code: `append()`, 55 | expectedErr: ":1:0: append expects at least two arguments", 56 | }, 57 | { 58 | name: "one argument fails", 59 | code: `append("1")`, 60 | expectedErr: ":1:0: append expects at least two arguments", 61 | }, 62 | { 63 | name: "simple append", 64 | code: `var a = () 65 | a <= append($a, "hello") 66 | a <= append($a, "world") 67 | echo -n $a...`, 68 | expectedErr: "", 69 | expectedStdout: "hello world", 70 | expectedStderr: "", 71 | }, 72 | { 73 | name: "append is for lists", 74 | code: `var a = "something" 75 | a <= append($a, "other") 76 | echo -n $a...`, 77 | expectedErr: ":2:8: append expects a " + 78 | "list as first argument, but a StringType was provided", 79 | expectedStdout: "", 80 | expectedStderr: "", 81 | }, 82 | { 83 | name: "var args", 84 | code: `var a <= append((), "1", "2", "3", "4", "5", "6") 85 | echo -n $a...`, 86 | expectedErr: "", 87 | expectedStdout: "1 2 3 4 5 6", 88 | expectedStderr: "", 89 | }, 90 | { 91 | name: "append of lists", 92 | code: `var a <= append((), (), ()) 93 | if len($a) != "2" { 94 | print("wrong") 95 | } else if len($a[0]) != "0" { 96 | print("wrong") 97 | } else if len($a[1]) != "0" { 98 | print("wrong") 99 | } else { print("ok") }`, 100 | expectedErr: "", 101 | expectedStdout: "ok", 102 | }, 103 | } { 104 | tc := tc 105 | t.Run(tc.name, func(t *testing.T) { 106 | testAppend(t, tc) 107 | }) 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /internal/sh/builtin/chdir.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/madlambda/nash/errors" 9 | "github.com/madlambda/nash/sh" 10 | ) 11 | 12 | type ( 13 | chdirFn struct { 14 | arg string 15 | } 16 | ) 17 | 18 | func newChdir() *chdirFn { 19 | return &chdirFn{} 20 | } 21 | 22 | func (chdir *chdirFn) ArgNames() []sh.FnArg { 23 | return []sh.FnArg{ 24 | sh.NewFnArg("dir", false), 25 | } 26 | } 27 | 28 | func (chdir *chdirFn) Run(in io.Reader, out io.Writer, ioerr io.Writer) ([]sh.Obj, error) { 29 | err := os.Chdir(chdir.arg) 30 | if err != nil { 31 | err = fmt.Errorf("builtin: chdir: error[%s] path[%s]", err, chdir.arg) 32 | } 33 | return nil, err 34 | } 35 | 36 | func (chdir *chdirFn) SetArgs(args []sh.Obj) error { 37 | if len(args) != 1 { 38 | return errors.NewError("chdir expects one argument, but received %q", args) 39 | } 40 | 41 | obj := args[0] 42 | if obj.Type() != sh.StringType { 43 | return errors.NewError("chdir expects a string, but a %s was provided", obj.Type()) 44 | } 45 | 46 | if objstr, ok := obj.(*sh.StrObj); ok { 47 | chdir.arg = objstr.Str() 48 | return nil 49 | } 50 | 51 | return errors.NewError("internal error: object of wrong type") 52 | } 53 | -------------------------------------------------------------------------------- /internal/sh/builtin/doc.go: -------------------------------------------------------------------------------- 1 | // builtin is where all built in functions resides 2 | package builtin 3 | -------------------------------------------------------------------------------- /internal/sh/builtin/exec_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/madlambda/nash/internal/sh/internal/fixture" 7 | ) 8 | 9 | func execSuccess(t *testing.T, scriptContents string) string { 10 | shell, cleanup := fixture.SetupShell(t) 11 | defer cleanup() 12 | 13 | out, err := shell.ExecOutput("", scriptContents) 14 | 15 | if err != nil { 16 | t.Fatalf("unexpected err: %s", err) 17 | } 18 | return string(out) 19 | } 20 | 21 | func execFailure(t *testing.T, scriptContents string) { 22 | shell, cleanup := fixture.SetupShell(t) 23 | defer cleanup() 24 | 25 | out, err := shell.ExecOutput("", scriptContents) 26 | 27 | if err == nil { 28 | t.Fatalf("expected err, got success, output: %s", string(out)) 29 | } 30 | 31 | if len(out) > 0 { 32 | t.Fatalf("expected empty output, got: %s", string(out)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/sh/builtin/exit.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/madlambda/nash/errors" 9 | "github.com/madlambda/nash/sh" 10 | ) 11 | 12 | type ( 13 | exitFn struct { 14 | status int 15 | } 16 | ) 17 | 18 | func newExit() Fn { 19 | return &exitFn{} 20 | } 21 | 22 | func (e *exitFn) ArgNames() []sh.FnArg { 23 | return []sh.FnArg{ 24 | sh.NewFnArg("status", false), 25 | } 26 | } 27 | 28 | func (e *exitFn) Run(in io.Reader, out io.Writer, err io.Writer) ([]sh.Obj, error) { 29 | os.Exit(e.status) 30 | return nil, nil //Unrecheable code 31 | } 32 | 33 | func (e *exitFn) SetArgs(args []sh.Obj) error { 34 | if len(args) != 1 { 35 | return errors.NewError("exit expects 1 argument") 36 | } 37 | 38 | obj := args[0] 39 | if obj.Type() != sh.StringType { 40 | return errors.NewError( 41 | "exit expects a status string, but a %s was provided", 42 | obj.Type(), 43 | ) 44 | } 45 | statusstr := obj.(*sh.StrObj).Str() 46 | status, err := strconv.Atoi(statusstr) 47 | if err != nil { 48 | return errors.NewError( 49 | "exit:error[%s] converting status[%s] to int", 50 | err, 51 | statusstr, 52 | ) 53 | 54 | } 55 | e.status = status 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/sh/builtin/exit_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | ) 8 | 9 | func TestExit(t *testing.T) { 10 | type exitDesc struct { 11 | script string 12 | status string 13 | result int 14 | fail bool 15 | } 16 | 17 | tests := map[string]exitDesc{ 18 | "success": { 19 | script: "./testdata/exit.sh", 20 | status: "0", 21 | result: 0, 22 | }, 23 | "failure": { 24 | script: "./testdata/exit.sh", 25 | status: "1", 26 | result: 1, 27 | }, 28 | } 29 | 30 | // WHY: We need to run Exec because the script will call the exit syscall, 31 | // killing the process (the test process on this case). 32 | // When calling Exec we need to guarantee that we are using the nash 33 | // built directly from the project, not the one installed on the host. 34 | projectnash := "../../../cmd/nash/nash" 35 | 36 | for name, desc := range tests { 37 | t.Run(name, func(t *testing.T) { 38 | cmd := exec.Command(projectnash, desc.script, desc.status) 39 | cmd.Stdout = os.Stdout 40 | cmd.Stderr = os.Stderr // to know why scripts were failing 41 | cmd.Run() 42 | 43 | if cmd.ProcessState == nil { 44 | t.Fatalf("expected cmd[%v] to have a process state, can't validate status code", cmd) 45 | } 46 | got := cmd.ProcessState.ExitCode() 47 | if desc.result != got { 48 | t.Fatalf("expected[%d] got[%d]", desc.result, got) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/sh/builtin/format.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/madlambda/nash/errors" 8 | "github.com/madlambda/nash/sh" 9 | ) 10 | 11 | type ( 12 | formatFn struct { 13 | fmt string 14 | args []interface{} 15 | } 16 | ) 17 | 18 | func newFormat() *formatFn { 19 | return &formatFn{} 20 | } 21 | 22 | func (f *formatFn) ArgNames() []sh.FnArg { 23 | return []sh.FnArg{ 24 | sh.NewFnArg("fmt", false), 25 | sh.NewFnArg("arg...", true), 26 | } 27 | } 28 | 29 | func (f *formatFn) Run(in io.Reader, out io.Writer, err io.Writer) ([]sh.Obj, error) { 30 | return []sh.Obj{sh.NewStrObj(fmt.Sprintf(f.fmt, f.args...))}, nil 31 | } 32 | 33 | func (f *formatFn) SetArgs(args []sh.Obj) error { 34 | if len(args) == 0 { 35 | return errors.NewError("format expects at least 1 argument") 36 | } 37 | 38 | f.fmt = args[0].String() 39 | f.args = nil 40 | 41 | for _, arg := range args[1:] { 42 | f.args = append(f.args, arg.String()) 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/sh/builtin/format_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import "testing" 4 | 5 | func TestFormat(t *testing.T) { 6 | type formatDesc struct { 7 | script string 8 | output string 9 | } 10 | 11 | tests := map[string]formatDesc{ 12 | "textonly": { 13 | script: ` 14 | var r <= format("helloworld") 15 | echo $r 16 | `, 17 | output: "helloworld\n", 18 | }, 19 | "ncallsRegressionTest": { 20 | script: ` 21 | fn formatstuff() { 22 | var r <= format("hello%s", "world") 23 | echo $r 24 | } 25 | formatstuff() 26 | formatstuff() 27 | `, 28 | output: "helloworld\nhelloworld\n", 29 | }, 30 | "ncallsWithVarsRegressionTest": { 31 | script: ` 32 | fn formatstuff() { 33 | var b = "world" 34 | var r <= format("hello%s", $b) 35 | var s <= format("hackthe%s", $b) 36 | echo $r 37 | echo $s 38 | } 39 | formatstuff() 40 | formatstuff() 41 | `, 42 | output: "helloworld\nhacktheworld\nhelloworld\nhacktheworld\n", 43 | }, 44 | "fmtstring": { 45 | script: ` 46 | var r <= format("%s:%s", "hello", "world") 47 | echo $r 48 | `, 49 | output: "hello:world\n", 50 | }, 51 | "fmtlist": { 52 | script: ` 53 | var list = ("1" "2" "3") 54 | var r <= format("%s:%s", "list", $list) 55 | echo $r 56 | `, 57 | output: "list:1 2 3\n", 58 | }, 59 | "funconly": { 60 | script: ` 61 | fn func() {} 62 | var r <= format($func) 63 | echo $r 64 | `, 65 | output: "\n", 66 | }, 67 | "funcfmt": { 68 | script: ` 69 | fn func() {} 70 | var r <= format("calling:%s", $func) 71 | echo $r 72 | `, 73 | output: "calling:\n", 74 | }, 75 | "listonly": { 76 | script: ` 77 | var list = ("1" "2" "3") 78 | var r <= format($list) 79 | echo $r 80 | `, 81 | output: "1 2 3\n", 82 | }, 83 | "listoflists": { 84 | script: ` 85 | var list = (("1" "2" "3") ("4" "5" "6")) 86 | var r <= format("%s:%s", "listoflists", $list) 87 | echo $r 88 | `, 89 | output: "listoflists:1 2 3 4 5 6\n", 90 | }, 91 | "listasfmt": { 92 | script: ` 93 | var list = ("%s" "%s") 94 | var r <= format($list, "1", "2") 95 | echo $r 96 | `, 97 | output: "1 2\n", 98 | }, 99 | "invalidFmt": { 100 | script: ` 101 | var r <= format("%d%s", "invalid") 102 | echo $r 103 | `, 104 | output: "%!d(string=invalid)%!s(MISSING)\n", 105 | }, 106 | } 107 | 108 | for name, desc := range tests { 109 | t.Run(name, func(t *testing.T) { 110 | output := execSuccess(t, desc.script) 111 | if output != desc.output { 112 | t.Fatalf("got %q expected %q", output, desc.output) 113 | } 114 | }) 115 | } 116 | } 117 | 118 | func TestFormatfErrors(t *testing.T) { 119 | type formatDesc struct { 120 | script string 121 | } 122 | 123 | tests := map[string]formatDesc{ 124 | "noParams": {script: `format()`}, 125 | } 126 | 127 | for name, desc := range tests { 128 | t.Run(name, func(t *testing.T) { 129 | execFailure(t, desc.script) 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /internal/sh/builtin/glob.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path/filepath" 7 | 8 | "github.com/madlambda/nash/errors" 9 | "github.com/madlambda/nash/sh" 10 | ) 11 | 12 | type ( 13 | globFn struct { 14 | pattern string 15 | } 16 | ) 17 | 18 | func newGlob() *globFn { 19 | return &globFn{} 20 | } 21 | 22 | func (p *globFn) ArgNames() []sh.FnArg { 23 | return []sh.FnArg{sh.NewFnArg("pattern", false)} 24 | } 25 | 26 | func (g *globFn) Run(in io.Reader, out io.Writer, e io.Writer) ([]sh.Obj, error) { 27 | listobjs := []sh.Obj{} 28 | matches, err := filepath.Glob(g.pattern) 29 | if err != nil { 30 | return []sh.Obj{ 31 | sh.NewListObj([]sh.Obj{}), 32 | sh.NewStrObj(fmt.Sprintf("glob:error: %q", err)), 33 | }, nil 34 | } 35 | for _, match := range matches { 36 | listobjs = append(listobjs, sh.NewStrObj(match)) 37 | } 38 | return []sh.Obj{sh.NewListObj(listobjs), sh.NewStrObj("")}, nil 39 | } 40 | 41 | func (g *globFn) SetArgs(args []sh.Obj) error { 42 | if len(args) != 1 { 43 | return errors.NewError("glob expects 1 string argument (the pattern)") 44 | } 45 | 46 | obj := args[0] 47 | if obj.Type() != sh.StringType { 48 | return errors.NewError( 49 | "glob expects a pattern string, but a %s was provided", 50 | obj.Type(), 51 | ) 52 | } 53 | g.pattern = obj.(*sh.StrObj).Str() 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/sh/builtin/glob_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func setup(t *testing.T) (string, func()) { 12 | dir, err := ioutil.TempDir("", "globtest") 13 | if err != nil { 14 | t.Fatalf("error on setup: %s", err) 15 | } 16 | 17 | return dir, func() { 18 | os.RemoveAll(dir) 19 | } 20 | } 21 | 22 | func createFile(t *testing.T, path string) { 23 | f, err := os.Create(path) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | f.WriteString("hi") 28 | f.Close() 29 | } 30 | 31 | func TestGlobNoResult(t *testing.T) { 32 | dir, teardown := setup(t) 33 | defer teardown() 34 | 35 | pattern := dir + "/*.la" 36 | 37 | out := execSuccess(t, fmt.Sprintf(` 38 | var res, err <= glob("%s") 39 | print($res) 40 | print($err) 41 | print(len($res)) 42 | `, pattern)) 43 | 44 | if out != "0" { 45 | t.Fatalf("expected no results, got: %q", out) 46 | } 47 | } 48 | 49 | func TestGlobOneResult(t *testing.T) { 50 | dir, teardown := setup(t) 51 | defer teardown() 52 | 53 | filename := dir + "/whatever.go" 54 | createFile(t, filename) 55 | pattern := dir + "/*.go" 56 | 57 | out := execSuccess(t, fmt.Sprintf(` 58 | var res, err <= glob("%s") 59 | print($res) 60 | print($err) 61 | `, pattern)) 62 | 63 | if out != filename { 64 | t.Fatalf("expected %q, got: %q", filename, out) 65 | } 66 | } 67 | 68 | func TestGlobMultipleResults(t *testing.T) { 69 | dir, teardown := setup(t) 70 | defer teardown() 71 | 72 | filename1 := dir + "/whatever.h" 73 | filename2 := dir + "/whatever2.h" 74 | 75 | createFile(t, filename1) 76 | createFile(t, filename2) 77 | 78 | pattern := dir + "/*.h" 79 | 80 | out := execSuccess(t, fmt.Sprintf(` 81 | var res, err <= glob("%s") 82 | print($res) 83 | print($err) 84 | `, pattern)) 85 | 86 | res := strings.Split(out, " ") 87 | if len(res) != 2 { 88 | t.Fatalf("expected 2 results, got: %d", len(res)) 89 | } 90 | 91 | found1 := false 92 | found2 := false 93 | 94 | for _, r := range res { 95 | if r == filename1 { 96 | found1 = true 97 | } 98 | if r == filename2 { 99 | found2 = true 100 | } 101 | } 102 | 103 | if !found1 || !found2 { 104 | t.Fatalf("unable to found all files, got: %q", out) 105 | } 106 | } 107 | 108 | func TestGlobInvalidPatternError(t *testing.T) { 109 | out := execSuccess(t, ` 110 | var res, err <= glob("*[.go") 111 | print($res) 112 | if $err != "" { 113 | print("got error") 114 | } 115 | `) 116 | 117 | if out != "got error" { 118 | t.Fatalf("expected error message on glob, got nothing, output[%s]", out) 119 | } 120 | } 121 | 122 | func TestFatalFailure(t *testing.T) { 123 | type tcase struct { 124 | name string 125 | code string 126 | } 127 | cases := []tcase{ 128 | tcase{ 129 | name: "noParam", 130 | code: ` 131 | var res <= glob() 132 | print($res) 133 | `, 134 | }, 135 | tcase{ 136 | name: "multipleParams", 137 | code: ` 138 | var res <= glob("/a/*", "/b/*") 139 | print($res) 140 | `, 141 | }, 142 | tcase{ 143 | name: "wrongType", 144 | code: ` 145 | var param = ("hi") 146 | var res <= glob($param) 147 | print($res) 148 | `, 149 | }, 150 | } 151 | 152 | for _, c := range cases { 153 | t.Run(c.name, func(t *testing.T) { 154 | execFailure(t, c.code) 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /internal/sh/builtin/len.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io" 5 | "strconv" 6 | 7 | "github.com/madlambda/nash/errors" 8 | "github.com/madlambda/nash/sh" 9 | ) 10 | 11 | type ( 12 | lenFn struct { 13 | arg sh.Collection 14 | } 15 | ) 16 | 17 | func newLen() *lenFn { 18 | return &lenFn{} 19 | } 20 | 21 | func (l *lenFn) ArgNames() []sh.FnArg { 22 | return []sh.FnArg{ 23 | sh.NewFnArg("list", false), 24 | } 25 | } 26 | 27 | func lenresult(res int) []sh.Obj { 28 | return []sh.Obj{sh.NewStrObj(strconv.Itoa(res))} 29 | } 30 | 31 | func (l *lenFn) Run(in io.Reader, out io.Writer, err io.Writer) ([]sh.Obj, error) { 32 | return lenresult(l.arg.Len()), nil 33 | } 34 | 35 | func (l *lenFn) SetArgs(args []sh.Obj) error { 36 | if len(args) != 1 { 37 | return errors.NewError("lenfn expects one argument") 38 | } 39 | 40 | obj := args[0] 41 | col, err := sh.NewCollection(obj) 42 | if err != nil { 43 | return errors.NewError("len:error[%s]", err) 44 | } 45 | 46 | l.arg = col 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/sh/builtin/len_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/madlambda/nash/internal/sh/internal/fixture" 8 | ) 9 | 10 | func TestLen(t *testing.T) { 11 | sh, cleanup := fixture.SetupShell(t) 12 | defer cleanup() 13 | 14 | var out bytes.Buffer 15 | 16 | sh.SetStdout(&out) 17 | 18 | err := sh.Exec( 19 | "test len", 20 | `var a = (1 2 3 4 5 6 7 8 9 0) 21 | var len_a <= len($a) 22 | echo -n $len_a`, 23 | ) 24 | 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | 30 | if "10" != string(out.Bytes()) { 31 | t.Errorf("String differs: '%s' != '%s'", "10", string(out.Bytes())) 32 | return 33 | } 34 | 35 | // Having to reset is a bad example of why testing N stuff together 36 | // is asking for trouble :-). 37 | out.Reset() 38 | 39 | err = sh.Exec( 40 | "test len fail", 41 | `a = "test" 42 | var l <= len($a) 43 | echo -n $l 44 | `, 45 | ) 46 | 47 | if err != nil { 48 | t.Error(err) 49 | return 50 | } 51 | 52 | if "4" != string(out.Bytes()) { 53 | t.Errorf("String differs: '%s' != '%s'", "4", string(out.Bytes())) 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/sh/builtin/loader.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/madlambda/nash/sh" 7 | ) 8 | 9 | // Fn is the contract of a built in function, that is simpler 10 | // than the core nash Fn. 11 | type ( 12 | Fn interface { 13 | ArgNames() []sh.FnArg 14 | SetArgs(args []sh.Obj) error 15 | Run( 16 | stdin io.Reader, 17 | stdout io.Writer, 18 | stderr io.Writer, 19 | ) ([]sh.Obj, error) 20 | } 21 | 22 | Constructor func() Fn 23 | ) 24 | 25 | // Constructors returns a map of the builtin function name and its constructor 26 | func Constructors() map[string]Constructor { 27 | return map[string]Constructor{ 28 | "glob": func() Fn { return newGlob() }, 29 | "print": func() Fn { return newPrint() }, 30 | "format": func() Fn { return newFormat() }, 31 | "split": func() Fn { return newSplit() }, 32 | "len": func() Fn { return newLen() }, 33 | "chdir": func() Fn { return newChdir() }, 34 | "append": func() Fn { return newAppend() }, 35 | "exit": func() Fn { return newExit() }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/sh/builtin/print.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/madlambda/nash/errors" 8 | "github.com/madlambda/nash/sh" 9 | ) 10 | 11 | type ( 12 | printFn struct { 13 | fmt string 14 | args []interface{} 15 | } 16 | ) 17 | 18 | func newPrint() *printFn { 19 | return &printFn{} 20 | } 21 | 22 | func (p *printFn) ArgNames() []sh.FnArg { 23 | return []sh.FnArg{ 24 | sh.NewFnArg("fmt", false), 25 | sh.NewFnArg("arg...", true), 26 | } 27 | } 28 | 29 | func (p *printFn) Run(in io.Reader, out io.Writer, err io.Writer) ([]sh.Obj, error) { 30 | fmt.Fprintf(out, p.fmt, p.args...) 31 | return nil, nil 32 | } 33 | 34 | func (p *printFn) SetArgs(args []sh.Obj) error { 35 | if len(args) == 0 { 36 | return errors.NewError("print expects at least 1 argument") 37 | } 38 | 39 | p.fmt = args[0].String() 40 | p.args = nil 41 | for _, arg := range args[1:] { 42 | p.args = append(p.args, arg.String()) 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/sh/builtin/print_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import "testing" 4 | 5 | func TestPrint(t *testing.T) { 6 | type printDesc struct { 7 | script string 8 | output string 9 | } 10 | 11 | tests := map[string]printDesc{ 12 | "textonly": { 13 | script: `print("helloworld")`, 14 | output: "helloworld", 15 | }, 16 | "nCallsRegresion": { 17 | script: `print("%s:%s", "hello", "world")`, 18 | output: "hello:world", 19 | }, 20 | "fmtstring": { 21 | script: ` 22 | print("%s:%s", "hello", "world") 23 | print("%s:%s", "hello", "world") 24 | `, 25 | output: "hello:worldhello:world", 26 | }, 27 | "fmtlist": { 28 | script: ` 29 | var list = ("1" "2" "3") 30 | print("%s:%s", "list", $list) 31 | `, 32 | output: "list:1 2 3", 33 | }, 34 | "funconly": { 35 | script: ` 36 | fn func() {} 37 | print($func) 38 | `, 39 | output: "", 40 | }, 41 | "funcfmt": { 42 | script: ` 43 | fn func() {} 44 | print("calling:%s", $func) 45 | `, 46 | output: "calling:", 47 | }, 48 | "listonly": { 49 | script: ` 50 | var list = ("1" "2" "3") 51 | print($list) 52 | `, 53 | output: "1 2 3", 54 | }, 55 | "listoflists": { 56 | script: ` 57 | var list = (("1" "2" "3") ("4" "5" "6")) 58 | print("%s:%s", "listoflists", $list) 59 | `, 60 | output: "listoflists:1 2 3 4 5 6", 61 | }, 62 | "listasfmt": { 63 | script: ` 64 | var list = ("%s" "%s") 65 | print($list, "1", "2") 66 | `, 67 | output: "1 2", 68 | }, 69 | "invalidFmt": { 70 | script: `print("%d%s", "invalid")`, 71 | output: "%!d(string=invalid)%!s(MISSING)", 72 | }, 73 | } 74 | 75 | for name, desc := range tests { 76 | t.Run(name, func(t *testing.T) { 77 | output := execSuccess(t, desc.script) 78 | if output != desc.output { 79 | t.Fatalf("got %q expected %q", output, desc.output) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestPrintErrors(t *testing.T) { 86 | type printDesc struct { 87 | script string 88 | } 89 | 90 | tests := map[string]printDesc{ 91 | "noParams": { 92 | script: `print()`, 93 | }, 94 | } 95 | 96 | for name, desc := range tests { 97 | t.Run(name, func(t *testing.T) { 98 | execFailure(t, desc.script) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/sh/builtin/split.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/madlambda/nash/errors" 8 | "github.com/madlambda/nash/sh" 9 | ) 10 | 11 | type ( 12 | splitFn struct { 13 | content string 14 | sep sh.Obj 15 | } 16 | ) 17 | 18 | func newSplit() *splitFn { 19 | return &splitFn{} 20 | } 21 | 22 | func (s *splitFn) ArgNames() []sh.FnArg { 23 | return []sh.FnArg{ 24 | sh.NewFnArg("sep", false), 25 | sh.NewFnArg("content", false), 26 | } 27 | } 28 | 29 | func (s *splitFn) Run(in io.Reader, out io.Writer, err io.Writer) ([]sh.Obj, error) { 30 | var output []string 31 | 32 | content := s.content 33 | 34 | switch s.sep.Type() { 35 | case sh.StringType: 36 | sep := s.sep.(*sh.StrObj).Str() 37 | output = strings.Split(content, sep) 38 | case sh.ListType: 39 | sepList := s.sep.(*sh.ListObj).List() 40 | output = splitByList(content, sepList) 41 | case sh.FnType: 42 | sepFn := s.sep.(*sh.FnObj).Fn() 43 | output = splitByFn(content, sepFn) 44 | default: 45 | return nil, errors.NewError("Invalid separator value: %v", s.sep) 46 | } 47 | 48 | listobjs := make([]sh.Obj, len(output)) 49 | for i := 0; i < len(output); i++ { 50 | listobjs[i] = sh.NewStrObj(output[i]) 51 | } 52 | 53 | return []sh.Obj{sh.NewListObj(listobjs)}, nil 54 | } 55 | 56 | func (s *splitFn) SetArgs(args []sh.Obj) error { 57 | if len(args) != 2 { 58 | return errors.NewError("split: expects 2 parameters") 59 | } 60 | 61 | if args[0].Type() != sh.StringType { 62 | return errors.NewError("split: first parameter must be a string") 63 | } 64 | 65 | content := args[0].(*sh.StrObj) 66 | s.content = content.Str() 67 | s.sep = args[1] 68 | return nil 69 | } 70 | 71 | func splitByList(content string, delims []sh.Obj) []string { 72 | return strings.FieldsFunc(content, func(r rune) bool { 73 | for _, delim := range delims { 74 | if delim.Type() != sh.StringType { 75 | continue 76 | } 77 | 78 | objstr := delim.(*sh.StrObj) 79 | 80 | if len(objstr.Str()) > 0 && rune(objstr.Str()[0]) == r { 81 | return true 82 | } 83 | } 84 | 85 | return false 86 | }) 87 | } 88 | 89 | func splitByFn(content string, splitFunc sh.FnDef) []string { 90 | return strings.FieldsFunc(content, func(r rune) bool { 91 | fn := splitFunc.Build() 92 | arg := sh.NewStrObj(string(r)) 93 | fn.SetArgs([]sh.Obj{arg}) 94 | err := fn.Start() 95 | if err != nil { 96 | return false 97 | } 98 | 99 | err = fn.Wait() 100 | if err != nil { 101 | return false 102 | } 103 | 104 | results := fn.Results() 105 | if len(results) != 1 { 106 | // expects a single return fn 107 | return false 108 | } 109 | 110 | result := results[0] 111 | 112 | //FIXME: It would be cool to only accept booleans 113 | // since the splitter is a predicate 114 | if result.Type() != sh.StringType { 115 | return false 116 | } 117 | 118 | status := result.(*sh.StrObj) 119 | if status.Str() == "0" { 120 | return true 121 | } 122 | 123 | return false 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /internal/sh/builtin/split_test.go: -------------------------------------------------------------------------------- 1 | package builtin_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/madlambda/nash/internal/sh/internal/fixture" 8 | ) 9 | 10 | func TestSplit(t *testing.T) { 11 | type splitDesc struct { 12 | script string 13 | word string 14 | sep string 15 | result string 16 | } 17 | 18 | tests := map[string]splitDesc{ 19 | "space": { 20 | script: "./testdata/split.sh", 21 | word: "a b c", 22 | sep: " ", 23 | result: "a\nb\nc\n", 24 | }, 25 | "pipes": { 26 | script: "./testdata/split.sh", 27 | word: "1|2|3", 28 | sep: "|", 29 | result: "1\n2\n3\n", 30 | }, 31 | "nosplit": { 32 | script: "./testdata/split.sh", 33 | word: "nospaces", 34 | sep: " ", 35 | result: "nospaces\n", 36 | }, 37 | "splitfunc": { 38 | script: "./testdata/splitfunc.sh", 39 | word: "hah", 40 | sep: "a", 41 | result: "h\nh\n", 42 | }, 43 | } 44 | 45 | for name, desc := range tests { 46 | t.Run(name, func(t *testing.T) { 47 | var output bytes.Buffer 48 | shell, cleanup := fixture.SetupShell(t) 49 | defer cleanup() 50 | 51 | shell.SetStdout(&output) 52 | err := shell.ExecFile(desc.script, "mock cmd name", desc.word, desc.sep) 53 | 54 | if err != nil { 55 | t.Fatalf("unexpected err: %s", err) 56 | } 57 | 58 | if output.String() != desc.result { 59 | t.Fatalf("got %q expected %q", output.String(), desc.result) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestSplitingByFuncWrongWontPanic(t *testing.T) { 66 | // Regression for: https://github.com/madlambda/nash/issues/177 67 | 68 | badscripts := map[string]string{ 69 | "noReturnValue": ` 70 | fn b() { echo "oops" } 71 | split("lalala", $b) 72 | `, 73 | "returnValueIsList": ` 74 | fn b() { return () } 75 | split("lalala", $b) 76 | `, 77 | "returnValueIsFunc": ` 78 | fn b() { 79 | fn x () {} 80 | return $x 81 | } 82 | split("lalala", $b) 83 | `, 84 | } 85 | 86 | for testname, badscript := range badscripts { 87 | 88 | t.Run(testname, func(t *testing.T) { 89 | shell, cleanup := fixture.SetupShell(t) 90 | defer cleanup() 91 | 92 | _, err := shell.ExecOutput("whatever", badscript) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | }) 97 | } 98 | } 99 | 100 | 101 | -------------------------------------------------------------------------------- /internal/sh/builtin/testdata/exit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | exit($ARGS[1]) 4 | -------------------------------------------------------------------------------- /internal/sh/builtin/testdata/split.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | var word = $ARGS[1] 4 | var sep = $ARGS[2] 5 | var output <= split($word, $sep) 6 | for o in $output { 7 | echo $o 8 | } 9 | -------------------------------------------------------------------------------- /internal/sh/builtin/testdata/splitfunc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | var word = $ARGS[1] 4 | var sep = $ARGS[2] 5 | 6 | fn splitter(char) { 7 | if $char == $sep { 8 | return "0" 9 | } 10 | 11 | return "1" 12 | } 13 | 14 | var output <= split($word, $splitter) 15 | 16 | for o in $output { 17 | echo $o 18 | } 19 | -------------------------------------------------------------------------------- /internal/sh/cmd.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | "github.com/madlambda/nash/ast" 9 | "github.com/madlambda/nash/errors" 10 | "github.com/madlambda/nash/sh" 11 | ) 12 | 13 | type ( 14 | // Cmd is a nash command. It has maps of input and output file 15 | // descriptors that can be set by SetInputfd and SetOutputfd. 16 | // This can be used to pipe execution of Cmd commands. 17 | Cmd struct { 18 | *exec.Cmd 19 | 20 | argExprs []ast.Expr 21 | } 22 | 23 | // errCmdNotFound is an error indicating the command wasn't found. 24 | errCmdNotFound struct { 25 | *errors.NashError 26 | } 27 | ) 28 | 29 | func newCmdNotFound(format string, arg ...interface{}) error { 30 | e := &errCmdNotFound{ 31 | NashError: errors.NewError(format, arg...), 32 | } 33 | 34 | return e 35 | } 36 | 37 | func (e *errCmdNotFound) NotFound() bool { 38 | return true 39 | } 40 | 41 | func NewCmd(name string) (*Cmd, error) { 42 | var ( 43 | err error 44 | cmdPath = name 45 | ) 46 | 47 | cmd := Cmd{} 48 | 49 | if !filepath.IsAbs(name) { 50 | cmdPath, err = exec.LookPath(name) 51 | 52 | if err != nil { 53 | return nil, newCmdNotFound(err.Error()) 54 | } 55 | } 56 | 57 | cmd.Cmd = &exec.Cmd{ 58 | Path: cmdPath, 59 | } 60 | 61 | return &cmd, nil 62 | } 63 | 64 | func (c *Cmd) Stdin() io.Reader { return c.Cmd.Stdin } 65 | func (c *Cmd) Stdout() io.Writer { return c.Cmd.Stdout } 66 | func (c *Cmd) Stderr() io.Writer { return c.Cmd.Stderr } 67 | 68 | func (c *Cmd) SetStdin(in io.Reader) { c.Cmd.Stdin = in } 69 | func (c *Cmd) SetStdout(out io.Writer) { c.Cmd.Stdout = out } 70 | func (c *Cmd) SetStderr(err io.Writer) { c.Cmd.Stderr = err } 71 | 72 | func (c *Cmd) SetArgs(nodeArgs []sh.Obj) error { 73 | args := make([]string, 1, len(nodeArgs)+1) 74 | args[0] = c.Path 75 | 76 | for _, obj := range nodeArgs { 77 | if obj.Type() == sh.StringType { 78 | objstr := obj.(*sh.StrObj) 79 | args = append(args, objstr.Str()) 80 | } else if obj.Type() == sh.ListType { 81 | objlist := obj.(*sh.ListObj) 82 | values := objlist.List() 83 | 84 | for _, l := range values { 85 | if l.Type() != sh.StringType { 86 | return errors.NewError("Command arguments requires string or list of strings. But received '%v'", l.String()) 87 | } 88 | 89 | lstr := l.(*sh.StrObj) 90 | args = append(args, lstr.Str()) 91 | } 92 | } else if obj.Type() == sh.FnType { 93 | return errors.NewError("Function cannot be passed as argument to commands.") 94 | } else { 95 | return errors.NewError("Invalid command argument '%v'", obj) 96 | } 97 | } 98 | 99 | c.Cmd.Args = args 100 | return nil 101 | } 102 | 103 | func (c *Cmd) Args() []ast.Expr { return c.argExprs } 104 | 105 | func (c *Cmd) SetEnviron(env []string) { 106 | c.Cmd.Env = env 107 | } 108 | 109 | func (c *Cmd) Wait() error { 110 | err := c.Cmd.Wait() 111 | 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (c *Cmd) Start() error { 120 | err := c.Cmd.Start() 121 | if err != nil { 122 | return err 123 | } 124 | return nil 125 | } 126 | 127 | func (c *Cmd) Results() []sh.Obj { return nil } 128 | -------------------------------------------------------------------------------- /internal/sh/cmd_test.go: -------------------------------------------------------------------------------- 1 | package sh 2 | -------------------------------------------------------------------------------- /internal/sh/fndef.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/madlambda/nash/ast" 7 | "github.com/madlambda/nash/errors" 8 | "github.com/madlambda/nash/internal/sh/builtin" 9 | "github.com/madlambda/nash/sh" 10 | ) 11 | 12 | type ( 13 | fnDef struct { 14 | name string 15 | Parent *Shell 16 | Body *ast.Tree 17 | argNames []sh.FnArg 18 | 19 | stdin io.Reader 20 | stdout, stderr io.Writer 21 | environ []string 22 | } 23 | 24 | userFnDef struct { 25 | *fnDef 26 | } 27 | 28 | builtinFnDef struct { 29 | *fnDef 30 | constructor builtin.Constructor 31 | } 32 | ) 33 | 34 | // newFnDef creates a new function definition 35 | func newFnDef(name string, parent *Shell, args []*ast.FnArgNode, body *ast.Tree) (*fnDef, error) { 36 | fn := fnDef{ 37 | name: name, 38 | Parent: parent, 39 | Body: body, 40 | stdin: parent.stdin, 41 | stdout: parent.stdout, 42 | stderr: parent.stderr, 43 | } 44 | 45 | for i := 0; i < len(args); i++ { 46 | arg := args[i] 47 | 48 | if i < len(args)-1 && arg.IsVariadic { 49 | return nil, errors.NewEvalError(parent.filename, 50 | arg, "Vararg '%s' isn't the last argument", 51 | arg.String()) 52 | } 53 | 54 | fn.argNames = append(fn.argNames, sh.FnArg{arg.Name, arg.IsVariadic}) 55 | } 56 | return &fn, nil 57 | } 58 | 59 | func (fnDef *fnDef) Name() string { return fnDef.name } 60 | func (fnDef *fnDef) ArgNames() []sh.FnArg { return fnDef.argNames } 61 | func (fnDef *fnDef) Environ() []string { return fnDef.environ } 62 | 63 | func (fnDef *fnDef) SetEnviron(env []string) { 64 | fnDef.environ = env 65 | } 66 | 67 | func (fnDef *fnDef) SetStdin(r io.Reader) { 68 | fnDef.stdin = r 69 | } 70 | 71 | func (fnDef *fnDef) SetStderr(w io.Writer) { 72 | fnDef.stderr = w 73 | } 74 | 75 | func (fnDef *fnDef) SetStdout(w io.Writer) { 76 | fnDef.stdout = w 77 | } 78 | 79 | func (fnDef *fnDef) Stdin() io.Reader { return fnDef.stdin } 80 | func (fnDef *fnDef) Stdout() io.Writer { return fnDef.stdout } 81 | func (fnDef *fnDef) Stderr() io.Writer { return fnDef.stderr } 82 | 83 | func newUserFnDef(name string, parent *Shell, args []*ast.FnArgNode, body *ast.Tree) (*userFnDef, error) { 84 | fnDef, err := newFnDef(name, parent, args, body) 85 | if err != nil { 86 | return nil, err 87 | } 88 | ufndef := userFnDef{ 89 | fnDef: fnDef, 90 | } 91 | return &ufndef, nil 92 | } 93 | 94 | func (ufnDef *userFnDef) Build() sh.Fn { 95 | userfn := NewUserFn(ufnDef.Name(), ufnDef.ArgNames(), ufnDef.Body, ufnDef.Parent) 96 | userfn.SetStdin(ufnDef.stdin) 97 | userfn.SetStdout(ufnDef.stdout) 98 | userfn.SetStderr(ufnDef.stderr) 99 | userfn.SetEnviron(ufnDef.environ) 100 | return userfn 101 | } 102 | 103 | func newBuiltinFnDef(name string, parent *Shell, constructor builtin.Constructor) *builtinFnDef { 104 | return &builtinFnDef{ 105 | fnDef: &fnDef{ 106 | name: name, 107 | stdin: parent.stdin, 108 | stdout: parent.stdout, 109 | stderr: parent.stderr, 110 | }, 111 | constructor: constructor, 112 | } 113 | } 114 | 115 | func (bfnDef *builtinFnDef) Build() sh.Fn { 116 | return NewBuiltinFn(bfnDef.Name(), 117 | bfnDef.constructor(), 118 | bfnDef.stdin, 119 | bfnDef.stdout, 120 | bfnDef.stderr, 121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /internal/sh/functions_test.go: -------------------------------------------------------------------------------- 1 | package sh_test 2 | 3 | import "testing" 4 | 5 | func TestFunctionsClosures(t *testing.T) { 6 | for _, test := range []execTestCase{ 7 | { 8 | desc: "simpleClosure", 9 | code: ` 10 | fn func(a) { 11 | fn closure() { 12 | print($a) 13 | } 14 | return $closure 15 | } 16 | 17 | var x <= func("1") 18 | var y <= func("2") 19 | $x() 20 | $y() 21 | `, 22 | expectedStdout: "12", 23 | }, 24 | { 25 | desc: "eachCallCreatesNewVar", 26 | code: ` 27 | fn func() { 28 | var a = () 29 | fn add(elem) { 30 | a <= append($a, $elem) 31 | print("a:%s,",$a) 32 | } 33 | return $add 34 | } 35 | 36 | var add <= func() 37 | $add("1") 38 | $add("3") 39 | $add("5") 40 | `, 41 | expectedStdout: "a:1,a:1 3,a:1 3 5,", 42 | }, 43 | { 44 | desc: "adder example", 45 | code: ` 46 | fn makeAdder(x) { 47 | fn add(y) { 48 | var ret <= expr $x "+" $y 49 | return $ret 50 | } 51 | return $add 52 | } 53 | 54 | var add1 <= makeAdder("1") 55 | var add5 <= makeAdder("5") 56 | var add1000 <= makeAdder("1000") 57 | 58 | print("%s\n", add5("5")) 59 | print("%s\n", add5("10")) 60 | print("%s\n", add1("10")) 61 | print("%s\n", add1("2")) 62 | print("%s\n", add1000("50")) 63 | print("%s\n", add1000("100")) 64 | print("%s\n", add1("10")) 65 | `, 66 | expectedStdout: `10 67 | 15 68 | 11 69 | 3 70 | 1050 71 | 1100 72 | 11 73 | `, 74 | }, 75 | { 76 | desc: "logger", 77 | code: `fn getlogger(prefix) { 78 | fn log(fmt, args...) { 79 | print($prefix+$fmt+"\n", $args...) 80 | } 81 | 82 | return $log 83 | } 84 | 85 | var info <= getlogger("[info] ") 86 | var error <= getlogger("[error] ") 87 | var warn <= getlogger("[warn] ") 88 | 89 | $info("nuke initialized successfully") 90 | $warn("temperature above anormal circunstances: %s°", "870") 91 | $error("about to explode...") 92 | `, 93 | expectedStdout: `[info] nuke initialized successfully 94 | [warn] temperature above anormal circunstances: 870° 95 | [error] about to explode... 96 | `, 97 | }, 98 | } { 99 | t.Run(test.desc, func(t *testing.T) { 100 | testExec(t, test) 101 | }) 102 | } 103 | } 104 | 105 | func TestFunctionsVariables(t *testing.T) { 106 | for _, test := range []execTestCase{ 107 | { 108 | desc: "fn stored only as vars", 109 | code: ` 110 | fn func(a) { 111 | echo -n "hello" 112 | } 113 | 114 | func = "teste" 115 | echo -n $func 116 | func() 117 | `, 118 | expectedStdout: "teste", 119 | expectedErr: ":8:4: Identifier 'func' is not a function", 120 | }, 121 | } { 122 | t.Run(test.desc, func(t *testing.T) { 123 | testExec(t, test) 124 | }) 125 | } 126 | } 127 | 128 | func TestFunctionsStateless(t *testing.T) { 129 | for _, test := range []execTestCase{ 130 | { 131 | desc: "functions have no shared state", 132 | code: `fn iter(first, last, func) { 133 | var sequence <= seq $first $last 134 | var range <= split($sequence, "\n") 135 | for i in $range { 136 | $func($i) 137 | } 138 | } 139 | 140 | fn create_vm(index) { 141 | echo "create_vm: "+$index 142 | iter("1", "3", $create_disk) 143 | } 144 | 145 | fn create_disk(index) { 146 | echo "create_disk: " + $index 147 | } 148 | 149 | iter("1", "2", $create_vm) 150 | `, 151 | expectedStdout: `create_vm: 1 152 | create_disk: 1 153 | create_disk: 2 154 | create_disk: 3 155 | create_vm: 2 156 | create_disk: 1 157 | create_disk: 2 158 | create_disk: 3 159 | `, 160 | }, 161 | } { 162 | t.Run(test.desc, func(t *testing.T) { 163 | testExec(t, test) 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /internal/sh/internal/fixture/fixture.go: -------------------------------------------------------------------------------- 1 | package fixture 2 | 3 | import ( 4 | "testing" 5 | "path/filepath" 6 | 7 | "github.com/madlambda/nash" 8 | "github.com/madlambda/nash/internal/testing/fixture" 9 | ) 10 | 11 | 12 | type NashDirs struct { 13 | Path string 14 | Lib string 15 | Root string 16 | Stdlib string 17 | Cleanup func() 18 | } 19 | 20 | var MkdirAll func(*testing.T, string) = fixture.MkdirAll 21 | 22 | var Tmpdir func(*testing.T) (string, func()) = fixture.Tmpdir 23 | 24 | func SetupShell(t *testing.T) (*nash.Shell, func()) { 25 | dirs := SetupNashDirs(t) 26 | 27 | shell, err := nash.New(dirs.Path, dirs.Root) 28 | 29 | if err != nil { 30 | dirs.Cleanup() 31 | t.Fatal(err) 32 | } 33 | 34 | return shell, dirs.Cleanup 35 | } 36 | 37 | func SetupNashDirs(t *testing.T) NashDirs { 38 | testdir, rmdir := Tmpdir(t) 39 | 40 | nashpath := filepath.Join(testdir, "nashpath") 41 | nashroot := filepath.Join(testdir, "nashroot") 42 | 43 | nashlib := filepath.Join(nashpath, "lib") 44 | nashstdlib := filepath.Join(nashroot, "stdlib") 45 | 46 | MkdirAll(t, nashlib) 47 | MkdirAll(t, nashstdlib) 48 | 49 | return NashDirs{ 50 | Path: nashpath, 51 | Lib: nashlib, 52 | Root: nashroot, 53 | Stdlib: nashstdlib, 54 | Cleanup: rmdir, 55 | } 56 | } 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /internal/sh/ioutils_test.go: -------------------------------------------------------------------------------- 1 | package sh_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "io/ioutil" 7 | ) 8 | 9 | func writeFile(t *testing.T, filename string, data string) { 10 | err := ioutil.WriteFile(filename, []byte(data), os.ModePerm) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | } 15 | 16 | func chdir(t *testing.T, dir string) { 17 | t.Helper() 18 | 19 | err := os.Chdir(dir) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func getwd(t *testing.T) string { 26 | t.Helper() 27 | 28 | dir, err := os.Getwd() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | return dir 34 | } -------------------------------------------------------------------------------- /internal/sh/log.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | // LogFn is the logger type 9 | type LogFn func(format string, args ...interface{}) 10 | 11 | // NewLog creates a new nash logger 12 | func NewLog(ns string, enable bool) LogFn { 13 | logger := log.New(os.Stderr, "", 0) 14 | 15 | return func(format string, args ...interface{}) { 16 | if enable { 17 | logger.Printf("["+ns+"] "+format, args...) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/sh/rfork.go: -------------------------------------------------------------------------------- 1 | // +build !linux,!plan9 2 | 3 | // 4 | package sh 5 | 6 | import ( 7 | "github.com/madlambda/nash/ast" 8 | "github.com/madlambda/nash/errors" 9 | ) 10 | 11 | func (sh *Shell) executeRfork(rfork *ast.RforkNode) error { 12 | return errors.NewError("rfork only supported on Linux and Plan9") 13 | } 14 | -------------------------------------------------------------------------------- /internal/sh/rfork_linux_test.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package sh 4 | 5 | import ( 6 | "bytes" 7 | "strings" 8 | "syscall" 9 | "testing" 10 | ) 11 | 12 | func getletters() string { 13 | var a bytes.Buffer 14 | 15 | for i := 'a'; i < 'z'; i++ { 16 | a.Write(append([]byte{}, byte(i))) 17 | } 18 | 19 | all := string(a.Bytes()) 20 | allCap := strings.ToUpper(all) 21 | return all + allCap 22 | } 23 | 24 | func getvalid() string { 25 | return "cumnpsi" 26 | } 27 | 28 | func testTblFlagsOK(flagstr string, expected uintptr, t *testing.T) { 29 | flags, err := getflags(flagstr) 30 | 31 | if err != nil { 32 | t.Error(err) 33 | return 34 | } 35 | 36 | if flags != expected { 37 | t.Errorf("Flags differ: expected %08x but %08x", expected, flags) 38 | return 39 | } 40 | } 41 | 42 | func TestRforkFlags(t *testing.T) { 43 | _, err := getflags("") 44 | 45 | if err == nil { 46 | t.Error("Empty flags should return error") 47 | return 48 | } 49 | 50 | _, err = getflags("a") 51 | 52 | if err == nil { 53 | t.Error("Unknow flag a") 54 | return 55 | } 56 | 57 | allchars := getletters() 58 | 59 | _, err = getflags(allchars) 60 | 61 | if err == nil { 62 | t.Error("Should fail") 63 | return 64 | } 65 | 66 | testTblFlagsOK("u", syscall.CLONE_NEWUSER, t) 67 | testTblFlagsOK("m", syscall.CLONE_NEWNS, t) 68 | testTblFlagsOK("n", syscall.CLONE_NEWNET, t) 69 | testTblFlagsOK("i", syscall.CLONE_NEWIPC, t) 70 | testTblFlagsOK("s", syscall.CLONE_NEWUTS, t) 71 | testTblFlagsOK("p", syscall.CLONE_NEWPID, t) 72 | testTblFlagsOK("c", syscall.CLONE_NEWUSER| 73 | syscall.CLONE_NEWNS|syscall.CLONE_NEWNET| 74 | syscall.CLONE_NEWIPC|syscall.CLONE_NEWUTS| 75 | syscall.CLONE_NEWUSER|syscall.CLONE_NEWPID, t) 76 | testTblFlagsOK("um", syscall.CLONE_NEWUSER|syscall.CLONE_NEWNS, t) 77 | testTblFlagsOK("umn", syscall.CLONE_NEWUSER| 78 | syscall.CLONE_NEWNS| 79 | syscall.CLONE_NEWNET, t) 80 | testTblFlagsOK("umni", syscall.CLONE_NEWUSER| 81 | syscall.CLONE_NEWNS| 82 | syscall.CLONE_NEWNET| 83 | syscall.CLONE_NEWIPC, t) 84 | testTblFlagsOK("umnip", syscall.CLONE_NEWUSER| 85 | syscall.CLONE_NEWNS| 86 | syscall.CLONE_NEWNET| 87 | syscall.CLONE_NEWIPC| 88 | syscall.CLONE_NEWPID, t) 89 | testTblFlagsOK("umnips", syscall.CLONE_NEWUSER| 90 | syscall.CLONE_NEWNS| 91 | syscall.CLONE_NEWNET| 92 | syscall.CLONE_NEWIPC| 93 | syscall.CLONE_NEWPID| 94 | syscall.CLONE_NEWUTS, t) 95 | } 96 | -------------------------------------------------------------------------------- /internal/sh/rfork_plan9.go: -------------------------------------------------------------------------------- 1 | // +build plan9 2 | 3 | package sh 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | func (sh *Shell) executeRfork(rfork *RforkNode) error { 11 | return newError("Sorry. Plan9 rfork not implemented yet.") 12 | } 13 | 14 | // getflags converts to Plan9 flags 15 | func getflags(flags string) (uintptr, error) { 16 | var ( 17 | pflags uintptr 18 | ) 19 | 20 | for i := 0; i < len(flags); i++ { 21 | switch flags[i] { 22 | case 'n': 23 | pflags |= syscall.RFNAMEG 24 | case 'N': 25 | pflags |= syscall.RFCNAMEG 26 | case 'e': 27 | pflags |= syscall.RFENVG 28 | case 'E': 29 | pflags |= syscall.RFCENVG 30 | case 's': 31 | pflags |= syscall.RFNOTEG 32 | case 'f': 33 | pflags |= syscall.RFFDG 34 | case 'F': 35 | pflags |= syscall.RFCFDG 36 | default: 37 | return 0, fmt.Errorf("Wrong rfork flag: %c", flags[i]) 38 | } 39 | } 40 | 41 | if pflags == 0 { 42 | return 0, fmt.Errorf("Rfork requires some flag") 43 | } 44 | 45 | return pflags, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/sh/shell_linux_test.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package sh_test 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | var ( 16 | enableUserNS bool 17 | ) 18 | 19 | func init() { 20 | const usernsOk = "1" 21 | const kernelcfg = "CONFIG_USER_NS" 22 | 23 | logUsernsDetection := func(err error) { 24 | if enableUserNS { 25 | fmt.Printf("Linux user namespaces enabled!") 26 | return 27 | } 28 | 29 | fmt.Printf("Warning: Impossible to know if kernel support USER namespace.\n") 30 | fmt.Printf("Warning: USER namespace tests will not run.\n") 31 | if err != nil { 32 | fmt.Printf("ERROR: %s\n", err) 33 | } 34 | } 35 | 36 | usernsCfg := "/proc/sys/kernel/unprivileged_userns_clone" 37 | val, permerr := ioutil.ReadFile(usernsCfg) 38 | 39 | // Travis build doesn't support /proc/config.gz but kernel has userns 40 | if os.Getenv("TRAVIS_BUILD") == "1" { 41 | enableUserNS = permerr == nil && string(val) == usernsOk 42 | logUsernsDetection(permerr) 43 | return 44 | } 45 | 46 | if permerr == nil { 47 | enableUserNS = string(val) == usernsOk 48 | logUsernsDetection(permerr) 49 | return 50 | } 51 | 52 | // old kernels dont have sysctl configurations 53 | // than just checking the /proc/config suffices 54 | usernsCmd := exec.Command("zgrep", kernelcfg, "/proc/config.gz") 55 | 56 | content, err := usernsCmd.CombinedOutput() 57 | if err != nil { 58 | enableUserNS = false 59 | logUsernsDetection(fmt.Errorf("Failed to get kernel config: %s", err)) 60 | return 61 | } 62 | 63 | cfgVal := strings.Trim(string(content), "\n\t ") 64 | enableUserNS = cfgVal == kernelcfg+"=y" 65 | logUsernsDetection(fmt.Errorf("%s not enabled in kernel config", kernelcfg)) 66 | } 67 | 68 | func TestExecuteRforkUserNS(t *testing.T) { 69 | if !enableUserNS { 70 | t.Skip("User namespace not enabled") 71 | return 72 | } 73 | 74 | f, teardown := setup(t) 75 | defer teardown() 76 | 77 | err := f.shell.Exec("rfork test", ` 78 | rfork u { 79 | id -u 80 | } 81 | `) 82 | 83 | if err != nil { 84 | t.Error(err) 85 | return 86 | } 87 | 88 | if string(f.shellOut.Bytes()) != "0\n" { 89 | t.Errorf("User namespace not supported in your kernel: %s", string(f.shellOut.Bytes())) 90 | return 91 | } 92 | } 93 | 94 | func TestExecuteRforkEnvVars(t *testing.T) { 95 | if !enableUserNS { 96 | t.Skip("User namespace not enabled") 97 | return 98 | } 99 | 100 | f, teardown := setup(t) 101 | defer teardown() 102 | 103 | sh := f.shell 104 | 105 | err := sh.Exec("test env", `var abra = "cadabra" 106 | setenv abra 107 | rfork up { 108 | echo $abra 109 | }`) 110 | 111 | if err != nil { 112 | t.Error(err) 113 | return 114 | } 115 | } 116 | 117 | func TestExecuteRforkUserNSNested(t *testing.T) { 118 | if !enableUserNS { 119 | t.Skip("User namespace not enabled") 120 | return 121 | } 122 | 123 | var out bytes.Buffer 124 | f, teardown := setup(t) 125 | defer teardown() 126 | 127 | sh := f.shell 128 | 129 | sh.SetStdout(&out) 130 | 131 | err := sh.Exec("rfork userns nested", ` 132 | rfork u { 133 | id -u 134 | rfork u { 135 | id -u 136 | } 137 | } 138 | `) 139 | 140 | if err != nil { 141 | t.Error(err) 142 | return 143 | } 144 | 145 | if string(out.Bytes()) != "0\n0\n" { 146 | t.Errorf("User namespace not supported in your kernel") 147 | return 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /internal/sh/shell_regression_test.go: -------------------------------------------------------------------------------- 1 | package sh_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | "testing" 8 | "path" 9 | "fmt" 10 | ) 11 | 12 | func TestExecuteIssue68(t *testing.T) { 13 | f, cleanup := setup(t) 14 | defer cleanup() 15 | 16 | sh := f.shell 17 | 18 | tmpDir, err := ioutil.TempDir("", "nash-tests") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | file := path.Join(tmpDir, "la") 24 | err = sh.Exec("-input-", fmt.Sprintf(`echo lalalala | grep la > %s`, file)) 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | 30 | defer os.Remove(file) 31 | 32 | contents, err := ioutil.ReadFile(file) 33 | 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | contentStr := strings.TrimSpace(string(contents)) 39 | if contentStr != "lalalala" { 40 | t.Errorf("Strings differ: '%s' != '%s'", contentStr, "lalalala") 41 | return 42 | } 43 | } 44 | 45 | func TestExecuteErrorSuppression(t *testing.T) { 46 | f, cleanup := setup(t) 47 | defer cleanup() 48 | 49 | sh := f.shell 50 | err := sh.Exec("-input-", `-bllsdlfjlsd`) 51 | 52 | if err != nil { 53 | t.Errorf("Expected to not fail...: %s", err.Error()) 54 | return 55 | } 56 | 57 | // issue #72 58 | err = sh.Exec("-input-", `echo lalala | -grep lelele`) 59 | 60 | if err != nil { 61 | t.Errorf("Expected to not fail...:(%s)", err.Error()) 62 | return 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/sh/shell_var_test.go: -------------------------------------------------------------------------------- 1 | package sh_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/madlambda/nash/tests" 8 | ) 9 | 10 | func TestVarAssign(t *testing.T) { 11 | for _, test := range []execTestCase{ 12 | { 13 | desc: "simple init", 14 | code: `var a = "1"; echo -n $a`, 15 | expectedStdout: "1", 16 | }, 17 | { 18 | desc: "variable does not exists", 19 | code: `a = "1"; echo -n $a`, 20 | expectedErr: `:1:0: Variable 'a' is not initialized. Use 'var a = '`, 21 | }, 22 | { 23 | desc: "variable already initialized", 24 | code: `var a = "1"; var a = "2"; echo -n $a`, 25 | expectedStdout: "2", 26 | }, 27 | { 28 | desc: "variable set", 29 | code: `var a = "1"; a = "2"; echo -n $a`, 30 | expectedStdout: "2", 31 | }, 32 | { 33 | desc: "global variable set", 34 | code: `var global = "1" 35 | fn somefunc() { global = "2" } 36 | somefunc() 37 | echo -n $global`, 38 | expectedStdout: "2", 39 | }, 40 | } { 41 | t.Run(test.desc, func(t *testing.T) { 42 | testExec(t, test) 43 | }) 44 | } 45 | } 46 | 47 | func TestVarExecAssign(t *testing.T) { 48 | 49 | for _, test := range []execTestCase{ 50 | { 51 | desc: "simple exec var", 52 | code: `var heart <= echo -n "feed both wolves" 53 | echo -n $heart`, 54 | expectedStdout: "feed both wolves", 55 | }, 56 | { 57 | desc: "var do not exists", 58 | code: `__a <= echo -n "fury"`, 59 | expectedErr: ":1:0: Variable '__a' is not initialized. Use 'var __a = '", 60 | }, 61 | { 62 | desc: "multiple var same name", 63 | code: `var a = "1" 64 | var a = "2" 65 | var a = "3" 66 | echo -n $a`, 67 | expectedStdout: "3", 68 | }, 69 | { 70 | desc: "multiple var same name with exec", 71 | code: `var a <= echo -n "1" 72 | var a <= echo -n "hello" 73 | echo -n $a`, 74 | expectedStdout: "hello", 75 | }, 76 | { 77 | desc: "first variable is stdout", 78 | code: `var out <= echo -n "hello" 79 | echo -n $out`, 80 | expectedStdout: "hello", 81 | }, 82 | { 83 | desc: "two variable, first stdout and second is status", 84 | code: `var stdout, status <= echo -n "bleh" 85 | echo -n $stdout $status`, 86 | expectedStdout: "bleh 0", 87 | }, 88 | { 89 | desc: "three variables, stdout empty, stderr with data, status", 90 | code: fmt.Sprintf(`var out, err, st <= %s/write/write /dev/stderr "hello" 91 | echo $out 92 | echo $err 93 | echo -n $st`, tests.Stdbindir), 94 | expectedStdout: "\nhello\n0", 95 | }, 96 | { 97 | desc: "three variables, stdout with data, stderr empty, status", 98 | code: fmt.Sprintf(`var out, err, st <= %s/write/write /dev/stdout "hello" 99 | echo $out 100 | echo $err 101 | echo -n $st`, tests.Stdbindir), 102 | expectedStdout: "hello\n\n0", 103 | }, 104 | } { 105 | t.Run(test.desc, func(t *testing.T) { 106 | testExec(t, test) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/sh/util.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/madlambda/nash/sh" 14 | ) 15 | 16 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 17 | 18 | func init() { 19 | rand.Seed(time.Now().UnixNano()) 20 | } 21 | 22 | func randRunes(n int) string { 23 | b := make([]rune, n) 24 | for i := range b { 25 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 26 | } 27 | return string(b) 28 | } 29 | 30 | func buildenv(e Env) []string { 31 | env := make([]string, 0, len(e)) 32 | 33 | for k, v := range e { 34 | if v == nil { 35 | continue 36 | } 37 | 38 | if v.Type() != sh.ListType && 39 | v.Type() != sh.StringType { 40 | continue 41 | } 42 | 43 | if v.Type() == sh.ListType { 44 | vlist := v.(*sh.ListObj) 45 | env = append(env, k+"=("+vlist.String()+")") 46 | } else { 47 | vstr := v.(*sh.StrObj) 48 | env = append(env, k+"="+vstr.String()) 49 | } 50 | } 51 | 52 | return env 53 | } 54 | 55 | func printVar(out io.Writer, name string, val sh.Obj) { 56 | if val.Type() == sh.StringType { 57 | valstr := val.(*sh.StrObj) 58 | fmt.Fprintf(out, "%s = \"%s\"\n", name, valstr.Str()) 59 | } else if val.Type() == sh.ListType { 60 | vallist := val.(*sh.ListObj) 61 | fmt.Fprintf(out, "%s = (%s)\n", name, vallist.String()) 62 | } 63 | } 64 | 65 | func printEnv(out io.Writer, name string) { 66 | fmt.Fprintf(out, "setenv %s\n", name) 67 | } 68 | 69 | func getErrStatus(err error, def string) string { 70 | status := def 71 | 72 | if exiterr, ok := err.(*exec.ExitError); ok { 73 | if statusObj, ok := exiterr.Sys().(syscall.WaitStatus); ok { 74 | status = strconv.Itoa(statusObj.ExitStatus()) 75 | } 76 | } 77 | 78 | return status 79 | } 80 | 81 | func nashdAutoDiscover() string { 82 | path, err := os.Readlink("/proc/self/exe") 83 | 84 | if err != nil { 85 | path = os.Args[0] 86 | 87 | if _, err := os.Stat(path); err != nil { 88 | return "" 89 | } 90 | } 91 | 92 | return path 93 | } 94 | -------------------------------------------------------------------------------- /internal/sh/util_test.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/madlambda/nash/sh" 8 | ) 9 | 10 | func TestBuildEnv(t *testing.T) { 11 | env := Env{ 12 | "teste": nil, 13 | } 14 | 15 | penv := buildenv(env) 16 | if len(penv) != 0 { 17 | t.Errorf("Invalid env length") 18 | return 19 | } 20 | 21 | env = Env{ 22 | "PATH": sh.NewStrObj("/bin:/usr/bin"), 23 | } 24 | 25 | penv = buildenv(env) 26 | 27 | if len(penv) != 1 { 28 | t.Errorf("Invalid env length") 29 | return 30 | } 31 | 32 | if penv[0] != "PATH=/bin:/usr/bin" { 33 | t.Errorf("Invalid env value: %s", penv[0]) 34 | return 35 | } 36 | 37 | env = Env{ 38 | "PATH": sh.NewListObj([]sh.Obj{ 39 | sh.NewStrObj("/bin"), 40 | sh.NewStrObj("/usr/bin"), 41 | }), 42 | } 43 | 44 | penv = buildenv(env) 45 | 46 | if len(penv) != 1 { 47 | t.Errorf("Invalid env length") 48 | return 49 | } 50 | 51 | if penv[0] != "PATH=(/bin /usr/bin)" { 52 | t.Errorf("Invalid env value: %s", penv[0]) 53 | return 54 | } 55 | 56 | env = Env{ 57 | "PATH": sh.NewListObj([]sh.Obj{ 58 | sh.NewStrObj("/bin"), 59 | sh.NewStrObj("/usr/bin"), 60 | }), 61 | "path": sh.NewStrObj("abracadabra"), 62 | } 63 | 64 | penv = buildenv(env) 65 | 66 | if len(penv) != 2 { 67 | t.Errorf("Invalid env length") 68 | return 69 | } 70 | 71 | sort.Strings(penv) 72 | 73 | if penv[0] != "PATH=(/bin /usr/bin)" { 74 | t.Errorf("Invalid env value: '%s'", penv[0]) 75 | return 76 | } 77 | 78 | if penv[1] != "path=abracadabra" { 79 | t.Errorf("Invalid env value: '%s'", penv[1]) 80 | return 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/testing/fixture/io.go: -------------------------------------------------------------------------------- 1 | package fixture 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | // Tmpdir creates a temporary dir and returns a function that can be used 13 | // to remove it after usage. Any error on any operation returns on a Fatal 14 | // call on the given testing.T. 15 | func Tmpdir(t *testing.T) (string, func()) { 16 | t.Helper() 17 | 18 | dir, err := ioutil.TempDir("", "nash-tests") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | dir, err = filepath.EvalSymlinks(dir) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | return dir, func() { 29 | err := os.RemoveAll(dir) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | } 35 | 36 | // MkdirAll will do the same thing as os.Mkdirall but calling Fatal on 37 | // the given testing.T if something goes wrong. 38 | func MkdirAll(t *testing.T, nashlib string) { 39 | t.Helper() 40 | 41 | err := os.MkdirAll(nashlib, os.ModePerm) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | 47 | // CreateFiles will create all files and its dirs if 48 | // necessary calling Fatal on the given testing if anything goes wrong. 49 | // 50 | // The files contents will be randomly generated strings (not a lot random, 51 | // just for test purposes) and will be returned on the map that will map 52 | // the filepath to its contents 53 | func CreateFiles(t *testing.T, filepaths []string) map[string]string { 54 | t.Helper() 55 | 56 | createdFiles := map[string]string{} 57 | 58 | for _, f := range filepaths { 59 | contents := CreateFile(t, f) 60 | createdFiles[f] = contents 61 | } 62 | 63 | return createdFiles 64 | } 65 | 66 | // CreateFile will create the file and its dirs if 67 | // necessary calling Fatal on the given testing if anything goes wrong. 68 | // 69 | // The file content will be randomly generated strings (not a lot random, 70 | // just for test purposes) and will be returned on the map that will map 71 | // the filepath to its contents. 72 | // 73 | // Return the contents generated for the file (and that has been written on it). 74 | func CreateFile(t *testing.T, f string) string { 75 | t.Helper() 76 | 77 | dir := filepath.Dir(f) 78 | MkdirAll(t, dir) 79 | 80 | contents := fmt.Sprintf("randomContents=%d", rand.Int()) 81 | 82 | err := ioutil.WriteFile(f, []byte(contents), 0644) 83 | if err != nil { 84 | t.Fatalf("error[%s] writing file[%s]", err, f) 85 | } 86 | 87 | return contents 88 | } 89 | 90 | func WorkingDir(t *testing.T) string { 91 | t.Helper() 92 | 93 | wd, err := os.Getwd() 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | return wd 98 | } 99 | 100 | func ChangeDir(t *testing.T, path string) { 101 | t.Helper() 102 | 103 | err := os.Chdir(path) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | } 108 | 109 | func Chmod(t *testing.T, path string, mode os.FileMode) { 110 | t.Helper() 111 | 112 | err := os.Chmod(path, mode) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /nash_test.go: -------------------------------------------------------------------------------- 1 | package nash 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/madlambda/nash/sh" 10 | "github.com/madlambda/nash/tests" 11 | ) 12 | 13 | // only testing the public API 14 | // bypass to internal sh.Shell 15 | 16 | func TestExecuteFile(t *testing.T) { 17 | testfile := tests.Testdir + "/ex1.sh" 18 | 19 | var out bytes.Buffer 20 | shell, cleanup := newTestShell(t) 21 | defer cleanup() 22 | 23 | shell.SetNashdPath(tests.Nashcmd) 24 | shell.SetStdout(&out) 25 | shell.SetStderr(os.Stderr) 26 | shell.SetStdin(os.Stdin) 27 | 28 | err := shell.ExecuteFile(testfile) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | 34 | if string(out.Bytes()) != "hello world\n" { 35 | t.Errorf("Wrong command output: '%s'", string(out.Bytes())) 36 | return 37 | } 38 | } 39 | 40 | func TestExecuteString(t *testing.T) { 41 | shell, cleanup := newTestShell(t) 42 | defer cleanup() 43 | 44 | var out bytes.Buffer 45 | 46 | shell.SetStdout(&out) 47 | 48 | err := shell.ExecuteString("-ínput-", "echo -n AAA") 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | 54 | if string(out.Bytes()) != "AAA" { 55 | t.Errorf("Unexpected '%s'", string(out.Bytes())) 56 | return 57 | } 58 | 59 | out.Reset() 60 | 61 | err = shell.ExecuteString("-input-", ` 62 | PROMPT="humpback> " 63 | setenv PROMPT 64 | `) 65 | if err != nil { 66 | t.Error(err) 67 | return 68 | } 69 | 70 | prompt := shell.Prompt() 71 | if prompt != "humpback> " { 72 | t.Errorf("Invalid prompt = %s", prompt) 73 | return 74 | } 75 | 76 | } 77 | 78 | func TestSetvar(t *testing.T) { 79 | shell, cleanup := newTestShell(t) 80 | defer cleanup() 81 | 82 | shell.Newvar("__TEST__", sh.NewStrObj("something")) 83 | 84 | var out bytes.Buffer 85 | shell.SetStdout(&out) 86 | 87 | err := shell.Exec("TestSetvar", `echo -n $__TEST__`) 88 | 89 | if err != nil { 90 | t.Error(err) 91 | return 92 | } 93 | 94 | if string(out.Bytes()) != "something" { 95 | t.Errorf("Value differ: '%s' != '%s'", string(out.Bytes()), "something") 96 | return 97 | } 98 | 99 | val, ok := shell.Getvar("__TEST__") 100 | 101 | if !ok || val.String() != "something" { 102 | t.Errorf("Getvar doesn't work: '%s' != '%s'", val, "something") 103 | return 104 | } 105 | } 106 | 107 | func newTestShell(t *testing.T) (*Shell, func()) { 108 | t.Helper() 109 | 110 | nashpath, pathclean := tmpdir(t) 111 | nashroot, rootclean := tmpdir(t) 112 | 113 | s, err := NewAbort(nashpath, nashroot) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | return s, func() { 119 | pathclean() 120 | rootclean() 121 | } 122 | } 123 | 124 | func tmpdir(t *testing.T) (string, func()) { 125 | t.Helper() 126 | 127 | dir, err := ioutil.TempDir("", "nash-tests") 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | 132 | return dir, func() { 133 | err := os.RemoveAll(dir) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /readline/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chzyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /readline/complete_helper.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // Caller type for dynamic completion 9 | type DynamicCompleteFunc func(string) []string 10 | 11 | type PrefixCompleterInterface interface { 12 | Print(prefix string, level int, buf *bytes.Buffer) 13 | Do(line []rune, pos int) (newLine [][]rune, length int) 14 | GetName() []rune 15 | GetChildren() []PrefixCompleterInterface 16 | SetChildren(children []PrefixCompleterInterface) 17 | } 18 | 19 | type DynamicPrefixCompleterInterface interface { 20 | PrefixCompleterInterface 21 | IsDynamic() bool 22 | GetDynamicNames(line []rune) [][]rune 23 | } 24 | 25 | type PrefixCompleter struct { 26 | Name []rune 27 | Dynamic bool 28 | Callback DynamicCompleteFunc 29 | Children []PrefixCompleterInterface 30 | } 31 | 32 | func (p *PrefixCompleter) Tree(prefix string) string { 33 | buf := bytes.NewBuffer(nil) 34 | p.Print(prefix, 0, buf) 35 | return buf.String() 36 | } 37 | 38 | func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) { 39 | if strings.TrimSpace(string(p.GetName())) != "" { 40 | buf.WriteString(prefix) 41 | if level > 0 { 42 | buf.WriteString("├") 43 | buf.WriteString(strings.Repeat("─", (level*4)-2)) 44 | buf.WriteString(" ") 45 | } 46 | buf.WriteString(string(p.GetName()) + "\n") 47 | level++ 48 | } 49 | for _, ch := range p.GetChildren() { 50 | ch.Print(prefix, level, buf) 51 | } 52 | } 53 | 54 | func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) { 55 | Print(p, prefix, level, buf) 56 | } 57 | 58 | func (p *PrefixCompleter) IsDynamic() bool { 59 | return p.Dynamic 60 | } 61 | 62 | func (p *PrefixCompleter) GetName() []rune { 63 | return p.Name 64 | } 65 | 66 | func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune { 67 | var names = [][]rune{} 68 | for _, name := range p.Callback(string(line)) { 69 | names = append(names, []rune(name+" ")) 70 | } 71 | return names 72 | } 73 | 74 | func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface { 75 | return p.Children 76 | } 77 | 78 | func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) { 79 | p.Children = children 80 | } 81 | 82 | func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter { 83 | return PcItem("", pc...) 84 | } 85 | 86 | func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter { 87 | name += " " 88 | return &PrefixCompleter{ 89 | Name: []rune(name), 90 | Dynamic: false, 91 | Children: pc, 92 | } 93 | } 94 | 95 | func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter { 96 | return &PrefixCompleter{ 97 | Callback: callback, 98 | Dynamic: true, 99 | Children: pc, 100 | } 101 | } 102 | 103 | func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) { 104 | return doInternal(p, line, pos, line) 105 | } 106 | 107 | func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) { 108 | return doInternal(p, line, pos, line) 109 | } 110 | 111 | func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) { 112 | line = runes.TrimSpaceLeft(line[:pos]) 113 | goNext := false 114 | var lineCompleter PrefixCompleterInterface 115 | for _, child := range p.GetChildren() { 116 | childNames := make([][]rune, 1) 117 | 118 | childDynamic, ok := child.(DynamicPrefixCompleterInterface) 119 | if ok && childDynamic.IsDynamic() { 120 | childNames = childDynamic.GetDynamicNames(origLine) 121 | } else { 122 | childNames[0] = child.GetName() 123 | } 124 | 125 | for _, childName := range childNames { 126 | if len(line) >= len(childName) { 127 | if runes.HasPrefix(line, childName) { 128 | if len(line) == len(childName) { 129 | newLine = append(newLine, []rune{' '}) 130 | } else { 131 | newLine = append(newLine, childName) 132 | } 133 | offset = len(childName) 134 | lineCompleter = child 135 | goNext = true 136 | } 137 | } else { 138 | if runes.HasPrefix(childName, line) { 139 | newLine = append(newLine, childName[len(line):]) 140 | offset = len(line) 141 | lineCompleter = child 142 | } 143 | } 144 | } 145 | } 146 | 147 | if len(newLine) != 1 { 148 | return 149 | } 150 | 151 | tmpLine := make([]rune, 0, len(line)) 152 | for i := offset; i < len(line); i++ { 153 | if line[i] == ' ' { 154 | continue 155 | } 156 | 157 | tmpLine = append(tmpLine, line[i:]...) 158 | return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine) 159 | } 160 | 161 | if goNext { 162 | return doInternal(lineCompleter, nil, 0, origLine) 163 | } 164 | return 165 | } 166 | -------------------------------------------------------------------------------- /readline/complete_segment.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | type SegmentCompleter interface { 4 | // a 5 | // |- a1 6 | // |--- a11 7 | // |- a2 8 | // b 9 | // input: 10 | // DoTree([], 0) [a, b] 11 | // DoTree([a], 1) [a] 12 | // DoTree([a, ], 0) [a1, a2] 13 | // DoTree([a, a], 1) [a1, a2] 14 | // DoTree([a, a1], 2) [a1] 15 | // DoTree([a, a1, ], 0) [a11] 16 | // DoTree([a, a1, a], 1) [a11] 17 | DoSegment([][]rune, int) [][]rune 18 | } 19 | 20 | type dumpSegmentCompleter struct { 21 | f func([][]rune, int) [][]rune 22 | } 23 | 24 | func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune { 25 | return d.f(segment, n) 26 | } 27 | 28 | func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter { 29 | return &SegmentComplete{&dumpSegmentCompleter{f}} 30 | } 31 | 32 | func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete { 33 | return &SegmentComplete{ 34 | SegmentCompleter: completer, 35 | } 36 | } 37 | 38 | type SegmentComplete struct { 39 | SegmentCompleter 40 | } 41 | 42 | func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) { 43 | ret := make([][]rune, 0, len(cands)) 44 | lastSegment := segments[len(segments)-1] 45 | for _, cand := range cands { 46 | if !runes.HasPrefix(cand, lastSegment) { 47 | continue 48 | } 49 | ret = append(ret, cand[len(lastSegment):]) 50 | } 51 | return ret, idx 52 | } 53 | 54 | func SplitSegment(line []rune, pos int) ([][]rune, int) { 55 | segs := [][]rune{} 56 | lastIdx := -1 57 | line = line[:pos] 58 | pos = 0 59 | for idx, l := range line { 60 | if l == ' ' { 61 | pos = 0 62 | segs = append(segs, line[lastIdx+1:idx]) 63 | lastIdx = idx 64 | } else { 65 | pos++ 66 | } 67 | } 68 | segs = append(segs, line[lastIdx+1:]) 69 | return segs, pos 70 | } 71 | 72 | func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) { 73 | 74 | segment, idx := SplitSegment(line, pos) 75 | 76 | cands := c.DoSegment(segment, idx) 77 | newLine, offset = RetSegment(segment, cands, idx) 78 | for idx := range newLine { 79 | newLine[idx] = append(newLine[idx], ' ') 80 | } 81 | return newLine, offset 82 | } 83 | -------------------------------------------------------------------------------- /readline/complete_segment_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/chzyer/test" 8 | ) 9 | 10 | func rs(s [][]rune) []string { 11 | ret := make([]string, len(s)) 12 | for idx, ss := range s { 13 | ret[idx] = string(ss) 14 | } 15 | return ret 16 | } 17 | 18 | func sr(s ...string) [][]rune { 19 | ret := make([][]rune, len(s)) 20 | for idx, ss := range s { 21 | ret[idx] = []rune(ss) 22 | } 23 | return ret 24 | } 25 | 26 | func TestRetSegment(t *testing.T) { 27 | defer test.New(t) 28 | // a 29 | // |- a1 30 | // |--- a11 31 | // |--- a12 32 | // |- a2 33 | // |--- a21 34 | // b 35 | // add 36 | // adddomain 37 | ret := []struct { 38 | Segments [][]rune 39 | Cands [][]rune 40 | idx int 41 | Ret [][]rune 42 | pos int 43 | }{ 44 | {sr(""), sr("a", "b", "add", "adddomain"), 0, sr("a", "b", "add", "adddomain"), 0}, 45 | {sr("a"), sr("a", "add", "adddomain"), 1, sr("", "dd", "dddomain"), 1}, 46 | {sr("a", ""), sr("a1", "a2"), 0, sr("a1", "a2"), 0}, 47 | {sr("a", "a"), sr("a1", "a2"), 1, sr("1", "2"), 1}, 48 | {sr("a", "a1"), sr("a1"), 2, sr(""), 2}, 49 | {sr("add"), sr("add", "adddomain"), 2, sr("", "domain"), 2}, 50 | } 51 | for idx, r := range ret { 52 | ret, pos := RetSegment(r.Segments, r.Cands, r.idx) 53 | test.Equal(ret, r.Ret, fmt.Errorf("%v", idx)) 54 | test.Equal(pos, r.pos, fmt.Errorf("%v", idx)) 55 | } 56 | } 57 | 58 | func TestSplitSegment(t *testing.T) { 59 | defer test.New(t) 60 | // a 61 | // |- a1 62 | // |--- a11 63 | // |--- a12 64 | // |- a2 65 | // |--- a21 66 | // b 67 | ret := []struct { 68 | Line string 69 | Pos int 70 | Segments [][]rune 71 | Idx int 72 | }{ 73 | {"", 0, sr(""), 0}, 74 | {"a", 1, sr("a"), 1}, 75 | {"a ", 2, sr("a", ""), 0}, 76 | {"a a", 3, sr("a", "a"), 1}, 77 | {"a a1", 4, sr("a", "a1"), 2}, 78 | {"a a1 ", 5, sr("a", "a1", ""), 0}, 79 | } 80 | 81 | for i, r := range ret { 82 | ret, idx := SplitSegment([]rune(r.Line), r.Pos) 83 | test.Equal(rs(ret), rs(r.Segments), fmt.Errorf("%v", i)) 84 | test.Equal(idx, r.Idx, fmt.Errorf("%v", i)) 85 | } 86 | } 87 | 88 | type Tree struct { 89 | Name string 90 | Children []Tree 91 | } 92 | 93 | func TestSegmentCompleter(t *testing.T) { 94 | defer test.New(t) 95 | 96 | tree := Tree{"", []Tree{ 97 | {"a", []Tree{ 98 | {"a1", []Tree{ 99 | {"a11", nil}, 100 | {"a12", nil}, 101 | }}, 102 | {"a2", []Tree{ 103 | {"a21", nil}, 104 | }}, 105 | }}, 106 | {"b", nil}, 107 | {"route", []Tree{ 108 | {"add", nil}, 109 | {"adddomain", nil}, 110 | }}, 111 | }} 112 | s := SegmentFunc(func(ret [][]rune, n int) [][]rune { 113 | tree := tree 114 | main: 115 | for level := 0; level < len(ret)-1; { 116 | name := string(ret[level]) 117 | for _, t := range tree.Children { 118 | if t.Name == name { 119 | tree = t 120 | level++ 121 | continue main 122 | } 123 | } 124 | } 125 | 126 | ret = make([][]rune, len(tree.Children)) 127 | for idx, r := range tree.Children { 128 | ret[idx] = []rune(r.Name) 129 | } 130 | return ret 131 | }) 132 | 133 | // a 134 | // |- a1 135 | // |--- a11 136 | // |--- a12 137 | // |- a2 138 | // |--- a21 139 | // b 140 | ret := []struct { 141 | Line string 142 | Pos int 143 | Ret [][]rune 144 | Share int 145 | }{ 146 | {"", 0, sr("a", "b", "route"), 0}, 147 | {"a", 1, sr(""), 1}, 148 | {"a ", 2, sr("a1", "a2"), 0}, 149 | {"a a", 3, sr("1", "2"), 1}, 150 | {"a a1", 4, sr(""), 2}, 151 | {"a a1 ", 5, sr("a11", "a12"), 0}, 152 | {"a a1 a", 6, sr("11", "12"), 1}, 153 | {"a a1 a1", 7, sr("1", "2"), 2}, 154 | {"a a1 a11", 8, sr(""), 3}, 155 | {"route add", 9, sr("", "domain"), 3}, 156 | } 157 | for _, r := range ret { 158 | for idx, rr := range r.Ret { 159 | r.Ret[idx] = append(rr, ' ') 160 | } 161 | } 162 | for i, r := range ret { 163 | newLine, length := s.Do([]rune(r.Line), r.Pos) 164 | test.Equal(rs(newLine), rs(r.Ret), fmt.Errorf("%v", i)) 165 | test.Equal(length, r.Share, fmt.Errorf("%v", i)) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /readline/doc/shortcut.md: -------------------------------------------------------------------------------- 1 | ## Readline Shortcut 2 | 3 | `Meta`+`B` means press `Esc` and `n` separately. 4 | Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B` 5 | Notice: `Meta`+`B` is equals with `Alt`+`B` in windows. 6 | 7 | * Shortcut in normal mode 8 | 9 | | Shortcut | Comment | 10 | | ------------------ | --------------------------------- | 11 | | `Ctrl`+`A` | Beginning of line | 12 | | `Ctrl`+`B` / `←` | Backward one character | 13 | | `Meta`+`B` | Backward one word | 14 | | `Ctrl`+`C` | Send io.EOF | 15 | | `Ctrl`+`D` | Delete one character | 16 | | `Meta`+`D` | Delete one word | 17 | | `Ctrl`+`E` | End of line | 18 | | `Ctrl`+`F` / `→` | Forward one character | 19 | | `Meta`+`F` | Forward one word | 20 | | `Ctrl`+`G` | Cancel | 21 | | `Ctrl`+`H` | Delete previous character | 22 | | `Ctrl`+`I` / `Tab` | Command line completion | 23 | | `Ctrl`+`J` | Line feed | 24 | | `Ctrl`+`K` | Cut text to the end of line | 25 | | `Ctrl`+`L` | Clear screen | 26 | | `Ctrl`+`M` | Same as Enter key | 27 | | `Ctrl`+`N` / `↓` | Next line (in history) | 28 | | `Ctrl`+`P` / `↑` | Prev line (in history) | 29 | | `Ctrl`+`R` | Search backwards in history | 30 | | `Ctrl`+`S` | Search forwards in history | 31 | | `Ctrl`+`T` | Transpose characters | 32 | | `Meta`+`T` | Transpose words (TODO) | 33 | | `Ctrl`+`U` | Cut text to the beginning of line | 34 | | `Ctrl`+`W` | Cut previous word | 35 | | `Backspace` | Delete previous character | 36 | | `Meta`+`Backspace` | Cut previous word | 37 | | `Enter` | Line feed | 38 | 39 | 40 | * Shortcut in Search Mode (`Ctrl`+`S` or `Ctrl`+`r` to enter this mode) 41 | 42 | | Shortcut | Comment | 43 | | ----------------------- | --------------------------------------- | 44 | | `Ctrl`+`S` | Search forwards in history | 45 | | `Ctrl`+`R` | Search backwards in history | 46 | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Search Mode and revert the history | 47 | | `Backspace` | Delete previous character | 48 | | Other | Exit Search Mode | 49 | 50 | * Shortcut in Complete Select Mode (double `Tab` to enter this mode) 51 | 52 | | Shortcut | Comment | 53 | | ----------------------- | ---------------------------------------- | 54 | | `Ctrl`+`F` | Move Forward | 55 | | `Ctrl`+`B` | Move Backward | 56 | | `Ctrl`+`N` | Move to next line | 57 | | `Ctrl`+`P` | Move to previous line | 58 | | `Ctrl`+`A` | Move to the first candicate in current line | 59 | | `Ctrl`+`E` | Move to the last candicate in current line | 60 | | `Tab` / `Enter` | Use the word on cursor to complete | 61 | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode | 62 | | Other | Exit Complete Select Mode | -------------------------------------------------------------------------------- /readline/password.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | type opPassword struct { 4 | o *Operation 5 | backupCfg *Config 6 | } 7 | 8 | func newOpPassword(o *Operation) *opPassword { 9 | return &opPassword{o: o} 10 | } 11 | 12 | func (o *opPassword) ExitPasswordMode() { 13 | o.o.SetConfig(o.backupCfg) 14 | o.backupCfg = nil 15 | } 16 | 17 | func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) { 18 | o.backupCfg, err = o.o.SetConfig(cfg) 19 | return 20 | } 21 | 22 | func (o *opPassword) PasswordConfig() *Config { 23 | return &Config{ 24 | EnableMask: true, 25 | InterruptPrompt: "\n", 26 | EOFPrompt: "\n", 27 | HistoryLimit: -1, 28 | 29 | Stdout: o.o.cfg.Stdout, 30 | Stderr: o.o.cfg.Stderr, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /readline/rawreader_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import "unsafe" 6 | 7 | const ( 8 | VK_CANCEL = 0x03 9 | VK_BACK = 0x08 10 | VK_TAB = 0x09 11 | VK_RETURN = 0x0D 12 | VK_SHIFT = 0x10 13 | VK_CONTROL = 0x11 14 | VK_MENU = 0x12 15 | VK_ESCAPE = 0x1B 16 | VK_LEFT = 0x25 17 | VK_UP = 0x26 18 | VK_RIGHT = 0x27 19 | VK_DOWN = 0x28 20 | VK_DELETE = 0x2E 21 | VK_LSHIFT = 0xA0 22 | VK_RSHIFT = 0xA1 23 | VK_LCONTROL = 0xA2 24 | VK_RCONTROL = 0xA3 25 | ) 26 | 27 | // RawReader translate input record to ANSI escape sequence. 28 | // To provides same behavior as unix terminal. 29 | type RawReader struct { 30 | ctrlKey bool 31 | altKey bool 32 | } 33 | 34 | func NewRawReader() *RawReader { 35 | r := new(RawReader) 36 | return r 37 | } 38 | 39 | // only process one action in one read 40 | func (r *RawReader) Read(buf []byte) (int, error) { 41 | ir := new(_INPUT_RECORD) 42 | var read int 43 | var err error 44 | next: 45 | err = kernel.ReadConsoleInputW(stdin, 46 | uintptr(unsafe.Pointer(ir)), 47 | 1, 48 | uintptr(unsafe.Pointer(&read)), 49 | ) 50 | if err != nil { 51 | return 0, err 52 | } 53 | if ir.EventType != EVENT_KEY { 54 | goto next 55 | } 56 | ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0])) 57 | if ker.bKeyDown == 0 { // keyup 58 | if r.ctrlKey || r.altKey { 59 | switch ker.wVirtualKeyCode { 60 | case VK_RCONTROL, VK_LCONTROL: 61 | r.ctrlKey = false 62 | case VK_MENU: //alt 63 | r.altKey = false 64 | } 65 | } 66 | goto next 67 | } 68 | 69 | if ker.unicodeChar == 0 { 70 | var target rune 71 | switch ker.wVirtualKeyCode { 72 | case VK_RCONTROL, VK_LCONTROL: 73 | r.ctrlKey = true 74 | case VK_MENU: //alt 75 | r.altKey = true 76 | case VK_LEFT: 77 | target = CharBackward 78 | case VK_RIGHT: 79 | target = CharForward 80 | case VK_UP: 81 | target = CharPrev 82 | case VK_DOWN: 83 | target = CharNext 84 | } 85 | if target != 0 { 86 | return r.write(buf, target) 87 | } 88 | goto next 89 | } 90 | char := rune(ker.unicodeChar) 91 | if r.ctrlKey { 92 | switch char { 93 | case 'A': 94 | char = CharLineStart 95 | case 'E': 96 | char = CharLineEnd 97 | case 'R': 98 | char = CharBckSearch 99 | case 'S': 100 | char = CharFwdSearch 101 | } 102 | } else if r.altKey { 103 | switch char { 104 | case VK_BACK: 105 | char = CharBackspace 106 | } 107 | return r.writeEsc(buf, char) 108 | } 109 | return r.write(buf, char) 110 | } 111 | 112 | func (r *RawReader) writeEsc(b []byte, char rune) (int, error) { 113 | b[0] = '\033' 114 | n := copy(b[1:], []byte(string(char))) 115 | return n + 1, nil 116 | } 117 | 118 | func (r *RawReader) write(b []byte, char rune) (int, error) { 119 | n := copy(b, []byte(string(char))) 120 | return n, nil 121 | } 122 | 123 | func (r *RawReader) Close() error { 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /readline/readline_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRace(t *testing.T) { 9 | rl, err := NewEx(&Config{}) 10 | if err != nil { 11 | t.Fatal(err) 12 | return 13 | } 14 | 15 | go func() { 16 | for range time.Tick(time.Millisecond) { 17 | rl.SetPrompt("hello") 18 | } 19 | }() 20 | 21 | go func() { 22 | time.Sleep(100 * time.Millisecond) 23 | rl.Close() 24 | }() 25 | 26 | rl.Readline() 27 | } 28 | -------------------------------------------------------------------------------- /readline/runes.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "unicode" 6 | "unicode/utf8" 7 | ) 8 | 9 | var runes = Runes{} 10 | var TabWidth = 4 11 | 12 | type Runes struct{} 13 | 14 | func (Runes) EqualRune(a, b rune, fold bool) bool { 15 | if a == b { 16 | return true 17 | } 18 | if !fold { 19 | return false 20 | } 21 | if a > b { 22 | a, b = b, a 23 | } 24 | if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' { 25 | if b == a+'a'-'A' { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | 32 | func (r Runes) EqualRuneFold(a, b rune) bool { 33 | return r.EqualRune(a, b, true) 34 | } 35 | 36 | func (r Runes) EqualFold(a, b []rune) bool { 37 | if len(a) != len(b) { 38 | return false 39 | } 40 | for i := 0; i < len(a); i++ { 41 | if r.EqualRuneFold(a[i], b[i]) { 42 | continue 43 | } 44 | return false 45 | } 46 | 47 | return true 48 | } 49 | 50 | func (Runes) Equal(a, b []rune) bool { 51 | if len(a) != len(b) { 52 | return false 53 | } 54 | for i := 0; i < len(a); i++ { 55 | if a[i] != b[i] { 56 | return false 57 | } 58 | } 59 | return true 60 | } 61 | 62 | func (rs Runes) IndexAllBckEx(r, sub []rune, fold bool) int { 63 | for i := len(r) - len(sub); i >= 0; i-- { 64 | found := true 65 | for j := 0; j < len(sub); j++ { 66 | if !rs.EqualRune(r[i+j], sub[j], fold) { 67 | found = false 68 | break 69 | } 70 | } 71 | if found { 72 | return i 73 | } 74 | } 75 | return -1 76 | } 77 | 78 | // Search in runes from end to front 79 | func (rs Runes) IndexAllBck(r, sub []rune) int { 80 | return rs.IndexAllBckEx(r, sub, false) 81 | } 82 | 83 | // Search in runes from front to end 84 | func (rs Runes) IndexAll(r, sub []rune) int { 85 | return rs.IndexAllEx(r, sub, false) 86 | } 87 | 88 | func (rs Runes) IndexAllEx(r, sub []rune, fold bool) int { 89 | for i := 0; i < len(r); i++ { 90 | found := true 91 | if len(r[i:]) < len(sub) { 92 | return -1 93 | } 94 | for j := 0; j < len(sub); j++ { 95 | if !rs.EqualRune(r[i+j], sub[j], fold) { 96 | found = false 97 | break 98 | } 99 | } 100 | if found { 101 | return i 102 | } 103 | } 104 | return -1 105 | } 106 | 107 | func (Runes) Index(r rune, rs []rune) int { 108 | for i := 0; i < len(rs); i++ { 109 | if rs[i] == r { 110 | return i 111 | } 112 | } 113 | return -1 114 | } 115 | 116 | func (Runes) ColorFilter(r []rune) []rune { 117 | newr := make([]rune, 0, len(r)) 118 | for pos := 0; pos < len(r); pos++ { 119 | if r[pos] == '\033' && r[pos+1] == '[' { 120 | idx := runes.Index('m', r[pos+2:]) 121 | if idx == -1 { 122 | continue 123 | } 124 | pos += idx + 2 125 | continue 126 | } 127 | newr = append(newr, r[pos]) 128 | } 129 | return newr 130 | } 131 | 132 | var zeroWidth = []*unicode.RangeTable{ 133 | unicode.Mn, 134 | unicode.Me, 135 | unicode.Cc, 136 | unicode.Cf, 137 | } 138 | 139 | var doubleWidth = []*unicode.RangeTable{ 140 | unicode.Han, 141 | unicode.Hangul, 142 | unicode.Hiragana, 143 | unicode.Katakana, 144 | } 145 | 146 | func (Runes) Width(r rune) int { 147 | if r == '\t' { 148 | return TabWidth 149 | } 150 | if unicode.IsOneOf(zeroWidth, r) { 151 | return 0 152 | } 153 | if unicode.IsOneOf(doubleWidth, r) { 154 | return 2 155 | } 156 | return 1 157 | } 158 | 159 | func (Runes) WidthAll(r []rune) (length int) { 160 | for i := 0; i < len(r); i++ { 161 | length += runes.Width(r[i]) 162 | } 163 | return 164 | } 165 | 166 | func (Runes) Backspace(r []rune) []byte { 167 | return bytes.Repeat([]byte{'\b'}, runes.WidthAll(r)) 168 | } 169 | 170 | func (Runes) Copy(r []rune) []rune { 171 | n := make([]rune, len(r)) 172 | copy(n, r) 173 | return n 174 | } 175 | 176 | func (Runes) HasPrefixFold(r, prefix []rune) bool { 177 | if len(r) < len(prefix) { 178 | return false 179 | } 180 | return runes.EqualFold(r[:len(prefix)], prefix) 181 | } 182 | 183 | func (Runes) HasPrefix(r, prefix []rune) bool { 184 | if len(r) < len(prefix) { 185 | return false 186 | } 187 | return runes.Equal(r[:len(prefix)], prefix) 188 | } 189 | 190 | func (Runes) Aggregate(candicate [][]rune) (same []rune, size int) { 191 | for i := 0; i < len(candicate[0]); i++ { 192 | for j := 0; j < len(candicate)-1; j++ { 193 | if i >= len(candicate[j]) || i >= len(candicate[j+1]) { 194 | goto aggregate 195 | } 196 | if candicate[j][i] != candicate[j+1][i] { 197 | goto aggregate 198 | } 199 | } 200 | size = i + 1 201 | } 202 | aggregate: 203 | if size > 0 { 204 | same = runes.Copy(candicate[0][:size]) 205 | for i := 0; i < len(candicate); i++ { 206 | n := runes.Copy(candicate[i]) 207 | copy(n, n[size:]) 208 | candicate[i] = n[:len(n)-size] 209 | } 210 | } 211 | return 212 | } 213 | 214 | func (Runes) TrimSpaceLeft(in []rune) []rune { 215 | firstIndex := len(in) 216 | for i, r := range in { 217 | if unicode.IsSpace(r) == false { 218 | firstIndex = i 219 | break 220 | } 221 | } 222 | return in[firstIndex:] 223 | } 224 | -------------------------------------------------------------------------------- /readline/runes/runes.go: -------------------------------------------------------------------------------- 1 | // deprecated. 2 | // see https://github.com/chzyer/readline/issues/43 3 | // use github.com/chzyer/readline/runes.go 4 | package runes 5 | 6 | import ( 7 | "bytes" 8 | "unicode" 9 | ) 10 | 11 | func Equal(a, b []rune) bool { 12 | if len(a) != len(b) { 13 | return false 14 | } 15 | for i := 0; i < len(a); i++ { 16 | if a[i] != b[i] { 17 | return false 18 | } 19 | } 20 | return true 21 | } 22 | 23 | // Search in runes from end to front 24 | func IndexAllBck(r, sub []rune) int { 25 | for i := len(r) - len(sub); i >= 0; i-- { 26 | found := true 27 | for j := 0; j < len(sub); j++ { 28 | if r[i+j] != sub[j] { 29 | found = false 30 | break 31 | } 32 | } 33 | if found { 34 | return i 35 | } 36 | } 37 | return -1 38 | } 39 | 40 | // Search in runes from front to end 41 | func IndexAll(r, sub []rune) int { 42 | for i := 0; i < len(r); i++ { 43 | found := true 44 | if len(r[i:]) < len(sub) { 45 | return -1 46 | } 47 | for j := 0; j < len(sub); j++ { 48 | if r[i+j] != sub[j] { 49 | found = false 50 | break 51 | } 52 | } 53 | if found { 54 | return i 55 | } 56 | } 57 | return -1 58 | } 59 | 60 | func Index(r rune, rs []rune) int { 61 | for i := 0; i < len(rs); i++ { 62 | if rs[i] == r { 63 | return i 64 | } 65 | } 66 | return -1 67 | } 68 | 69 | func ColorFilter(r []rune) []rune { 70 | newr := make([]rune, 0, len(r)) 71 | for pos := 0; pos < len(r); pos++ { 72 | if r[pos] == '\033' && r[pos+1] == '[' { 73 | idx := Index('m', r[pos+2:]) 74 | if idx == -1 { 75 | continue 76 | } 77 | pos += idx + 2 78 | continue 79 | } 80 | newr = append(newr, r[pos]) 81 | } 82 | return newr 83 | } 84 | 85 | var zeroWidth = []*unicode.RangeTable{ 86 | unicode.Mn, 87 | unicode.Me, 88 | unicode.Cc, 89 | unicode.Cf, 90 | } 91 | 92 | var doubleWidth = []*unicode.RangeTable{ 93 | unicode.Han, 94 | unicode.Hangul, 95 | unicode.Hiragana, 96 | unicode.Katakana, 97 | } 98 | 99 | func Width(r rune) int { 100 | if unicode.IsOneOf(zeroWidth, r) { 101 | return 0 102 | } 103 | if unicode.IsOneOf(doubleWidth, r) { 104 | return 2 105 | } 106 | return 1 107 | } 108 | 109 | func WidthAll(r []rune) (length int) { 110 | for i := 0; i < len(r); i++ { 111 | length += Width(r[i]) 112 | } 113 | return 114 | } 115 | 116 | func Backspace(r []rune) []byte { 117 | return bytes.Repeat([]byte{'\b'}, WidthAll(r)) 118 | } 119 | 120 | func Copy(r []rune) []rune { 121 | n := make([]rune, len(r)) 122 | copy(n, r) 123 | return n 124 | } 125 | 126 | func HasPrefix(r, prefix []rune) bool { 127 | if len(r) < len(prefix) { 128 | return false 129 | } 130 | return Equal(r[:len(prefix)], prefix) 131 | } 132 | 133 | func Aggregate(candicate [][]rune) (same []rune, size int) { 134 | for i := 0; i < len(candicate[0]); i++ { 135 | for j := 0; j < len(candicate)-1; j++ { 136 | if i >= len(candicate[j]) || i >= len(candicate[j+1]) { 137 | goto aggregate 138 | } 139 | if candicate[j][i] != candicate[j+1][i] { 140 | goto aggregate 141 | } 142 | } 143 | size = i + 1 144 | } 145 | aggregate: 146 | if size > 0 { 147 | same = Copy(candicate[0][:size]) 148 | for i := 0; i < len(candicate); i++ { 149 | n := Copy(candicate[i]) 150 | copy(n, n[size:]) 151 | candicate[i] = n[:len(n)-size] 152 | } 153 | } 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /readline/runes/runes_test.go: -------------------------------------------------------------------------------- 1 | package runes 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type twidth struct { 9 | r []rune 10 | length int 11 | } 12 | 13 | func TestRuneWidth(t *testing.T) { 14 | runes := []twidth{ 15 | {[]rune("☭"), 1}, 16 | {[]rune("a"), 1}, 17 | {[]rune("你"), 2}, 18 | {ColorFilter([]rune("☭\033[13;1m你")), 3}, 19 | } 20 | for _, r := range runes { 21 | if w := WidthAll(r.r); w != r.length { 22 | t.Fatal("result not expect", r.r, r.length, w) 23 | } 24 | } 25 | } 26 | 27 | type tagg struct { 28 | r [][]rune 29 | e [][]rune 30 | length int 31 | } 32 | 33 | func TestAggRunes(t *testing.T) { 34 | runes := []tagg{ 35 | { 36 | [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, 37 | [][]rune{[]rune("b"), []rune(""), []rune("bc")}, 38 | 1, 39 | }, 40 | { 41 | [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, 42 | [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, 43 | 1, 44 | }, 45 | { 46 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 47 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 48 | 0, 49 | }, 50 | { 51 | [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, 52 | [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, 53 | 2, 54 | }, 55 | } 56 | for _, r := range runes { 57 | same, off := Aggregate(r.r) 58 | if off != r.length { 59 | t.Fatal("result not expect", off) 60 | } 61 | if len(same) != off { 62 | t.Fatal("result not expect", same) 63 | } 64 | if !reflect.DeepEqual(r.r, r.e) { 65 | t.Fatal("result not expect") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /readline/runes_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type twidth struct { 9 | r []rune 10 | length int 11 | } 12 | 13 | func TestRuneWidth(t *testing.T) { 14 | rs := []twidth{ 15 | {[]rune("☭"), 1}, 16 | {[]rune("a"), 1}, 17 | {[]rune("你"), 2}, 18 | {runes.ColorFilter([]rune("☭\033[13;1m你")), 3}, 19 | } 20 | for _, r := range rs { 21 | if w := runes.WidthAll(r.r); w != r.length { 22 | t.Fatal("result not expect", r.r, r.length, w) 23 | } 24 | } 25 | } 26 | 27 | type tagg struct { 28 | r [][]rune 29 | e [][]rune 30 | length int 31 | } 32 | 33 | func TestAggRunes(t *testing.T) { 34 | rs := []tagg{ 35 | { 36 | [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, 37 | [][]rune{[]rune("b"), []rune(""), []rune("bc")}, 38 | 1, 39 | }, 40 | { 41 | [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, 42 | [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, 43 | 1, 44 | }, 45 | { 46 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 47 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 48 | 0, 49 | }, 50 | { 51 | [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, 52 | [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, 53 | 2, 54 | }, 55 | } 56 | for _, r := range rs { 57 | same, off := runes.Aggregate(r.r) 58 | if off != r.length { 59 | t.Fatal("result not expect", off) 60 | } 61 | if len(same) != off { 62 | t.Fatal("result not expect", same) 63 | } 64 | if !reflect.DeepEqual(r.r, r.e) { 65 | t.Fatal("result not expect") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /readline/std.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | Stdin io.ReadCloser = os.Stdin 11 | Stdout io.WriteCloser = os.Stdout 12 | Stderr io.WriteCloser = os.Stderr 13 | ) 14 | 15 | var ( 16 | std *Instance 17 | stdOnce sync.Once 18 | ) 19 | 20 | // global instance will not submit history automatic 21 | func getInstance() *Instance { 22 | stdOnce.Do(func() { 23 | std, _ = NewEx(&Config{ 24 | DisableAutoSaveHistory: true, 25 | }) 26 | }) 27 | return std 28 | } 29 | 30 | // let readline load history from filepath 31 | // and try to persist history into disk 32 | // set fp to "" to prevent readline persisting history to disk 33 | // so the `AddHistory` will return nil error forever. 34 | func SetHistoryPath(fp string) { 35 | ins := getInstance() 36 | cfg := ins.Config.Clone() 37 | cfg.HistoryFile = fp 38 | ins.SetConfig(cfg) 39 | } 40 | 41 | // set auto completer to global instance 42 | func SetAutoComplete(completer AutoCompleter) { 43 | ins := getInstance() 44 | cfg := ins.Config.Clone() 45 | cfg.AutoComplete = completer 46 | ins.SetConfig(cfg) 47 | } 48 | 49 | // add history to global instance manually 50 | // raise error only if `SetHistoryPath` is set with a non-empty path 51 | func AddHistory(content string) error { 52 | ins := getInstance() 53 | return ins.SaveHistory(content) 54 | } 55 | 56 | func Password(prompt string) ([]byte, error) { 57 | ins := getInstance() 58 | return ins.ReadPassword(prompt) 59 | } 60 | 61 | // readline with global configs 62 | func Line(prompt string) (string, error) { 63 | ins := getInstance() 64 | ins.SetPrompt(prompt) 65 | return ins.Readline() 66 | } 67 | 68 | type CancelableStdin struct { 69 | r io.Reader 70 | mutex sync.Mutex 71 | stop chan struct{} 72 | notify chan struct{} 73 | data []byte 74 | read int 75 | err error 76 | } 77 | 78 | func NewCancelableStdin(r io.Reader) *CancelableStdin { 79 | c := &CancelableStdin{ 80 | r: r, 81 | notify: make(chan struct{}), 82 | stop: make(chan struct{}), 83 | } 84 | go c.ioloop() 85 | return c 86 | } 87 | 88 | func (c *CancelableStdin) ioloop() { 89 | loop: 90 | for { 91 | select { 92 | case <-c.notify: 93 | c.read, c.err = c.r.Read(c.data) 94 | c.notify <- struct{}{} 95 | case <-c.stop: 96 | break loop 97 | } 98 | } 99 | } 100 | 101 | func (c *CancelableStdin) Read(b []byte) (n int, err error) { 102 | c.mutex.Lock() 103 | defer c.mutex.Unlock() 104 | 105 | c.data = b 106 | c.notify <- struct{}{} 107 | select { 108 | case <-c.notify: 109 | return c.read, c.err 110 | case <-c.stop: 111 | return 0, io.EOF 112 | } 113 | } 114 | 115 | func (c *CancelableStdin) Close() error { 116 | close(c.stop) 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /readline/std_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | func init() { 6 | Stdin = NewRawReader() 7 | Stdout = NewANSIWriter(Stdout) 8 | Stderr = NewANSIWriter(Stderr) 9 | } 10 | -------------------------------------------------------------------------------- /readline/term.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd 6 | 7 | // Package terminal provides support functions for dealing with terminals, as 8 | // commonly found on UNIX systems. 9 | // 10 | // Putting a terminal into raw mode is the most common requirement: 11 | // 12 | // oldState, err := terminal.MakeRaw(0) 13 | // if err != nil { 14 | // panic(err) 15 | // } 16 | // defer terminal.Restore(0, oldState) 17 | package readline 18 | 19 | import ( 20 | "io" 21 | "syscall" 22 | "unsafe" 23 | ) 24 | 25 | // State contains the state of a terminal. 26 | type State struct { 27 | termios syscall.Termios 28 | } 29 | 30 | // IsTerminal returns true if the given file descriptor is a terminal. 31 | func IsTerminal(fd int) bool { 32 | var termios syscall.Termios 33 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 34 | return err == 0 35 | } 36 | 37 | // MakeRaw put the terminal connected to the given file descriptor into raw 38 | // mode and returns the previous state of the terminal so that it can be 39 | // restored. 40 | func MakeRaw(fd int) (*State, error) { 41 | var oldState State 42 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { 43 | return nil, err 44 | } 45 | 46 | newState := oldState.termios 47 | // This attempts to replicate the behaviour documented for cfmakeraw in 48 | // the termios(3) manpage. 49 | newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON 50 | // newState.Oflag &^= syscall.OPOST 51 | newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN 52 | newState.Cflag &^= syscall.CSIZE | syscall.PARENB 53 | newState.Cflag |= syscall.CS8 54 | 55 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { 56 | return nil, err 57 | } 58 | 59 | return &oldState, nil 60 | } 61 | 62 | // GetState returns the current state of a terminal which may be useful to 63 | // restore the terminal after a signal. 64 | func GetState(fd int) (*State, error) { 65 | var oldState State 66 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { 67 | return nil, err 68 | } 69 | 70 | return &oldState, nil 71 | } 72 | 73 | // Restore restores the terminal connected to the given file descriptor to a 74 | // previous state. 75 | func restoreTerm(fd int, state *State) error { 76 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) 77 | return err 78 | } 79 | 80 | // GetSize returns the dimensions of the given terminal. 81 | func GetSize(fd int) (width, height int, err error) { 82 | var dimensions [4]uint16 83 | 84 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { 85 | return -1, -1, err 86 | } 87 | return int(dimensions[1]), int(dimensions[0]), nil 88 | } 89 | 90 | // ReadPassword reads a line of input from a terminal without local echo. This 91 | // is commonly used for inputting passwords and other sensitive data. The slice 92 | // returned does not include the \n. 93 | func ReadPassword(fd int) ([]byte, error) { 94 | var oldState syscall.Termios 95 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { 96 | return nil, err 97 | } 98 | 99 | newState := oldState 100 | newState.Lflag &^= syscall.ECHO 101 | newState.Lflag |= syscall.ICANON | syscall.ISIG 102 | newState.Iflag |= syscall.ICRNL 103 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { 104 | return nil, err 105 | } 106 | 107 | defer func() { 108 | syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) 109 | }() 110 | 111 | var buf [16]byte 112 | var ret []byte 113 | for { 114 | n, err := syscall.Read(fd, buf[:]) 115 | if err != nil { 116 | return nil, err 117 | } 118 | if n == 0 { 119 | if len(ret) == 0 { 120 | return nil, io.EOF 121 | } 122 | break 123 | } 124 | if buf[n-1] == '\n' { 125 | n-- 126 | } 127 | ret = append(ret, buf[:n]...) 128 | if n < len(buf) { 129 | break 130 | } 131 | } 132 | 133 | return ret, nil 134 | } 135 | -------------------------------------------------------------------------------- /readline/term_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd netbsd openbsd 6 | 7 | package readline 8 | 9 | import "syscall" 10 | 11 | const ioctlReadTermios = syscall.TIOCGETA 12 | const ioctlWriteTermios = syscall.TIOCSETA 13 | -------------------------------------------------------------------------------- /readline/term_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package readline 6 | 7 | // These constants are declared here, rather than importing 8 | // them from the syscall package as some syscall packages, even 9 | // on linux, for example gccgo, do not declare them. 10 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 11 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 12 | -------------------------------------------------------------------------------- /readline/utils_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | -------------------------------------------------------------------------------- /readline/utils_unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd 2 | 3 | package readline 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | type winsize struct { 15 | Row uint16 16 | Col uint16 17 | Xpixel uint16 18 | Ypixel uint16 19 | } 20 | 21 | // SuspendMe use to send suspend signal to myself, when we in the raw mode. 22 | // For OSX it need to send to parent's pid 23 | // For Linux it need to send to myself 24 | func SuspendMe() { 25 | p, _ := os.FindProcess(os.Getppid()) 26 | p.Signal(syscall.SIGTSTP) 27 | p, _ = os.FindProcess(os.Getpid()) 28 | p.Signal(syscall.SIGTSTP) 29 | } 30 | 31 | // get width of the terminal 32 | func getWidth(stdoutFd int) int { 33 | ws := &winsize{} 34 | retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, 35 | uintptr(stdoutFd), 36 | uintptr(syscall.TIOCGWINSZ), 37 | uintptr(unsafe.Pointer(ws))) 38 | 39 | if int(retCode) == -1 { 40 | _ = errno 41 | return -1 42 | } 43 | return int(ws.Col) 44 | } 45 | 46 | func GetScreenWidth() int { 47 | w := getWidth(syscall.Stdout) 48 | if w < 0 { 49 | w = getWidth(syscall.Stderr) 50 | } 51 | return w 52 | } 53 | 54 | // ClearScreen clears the console screen 55 | func ClearScreen(w io.Writer) (int, error) { 56 | return w.Write([]byte("\033[H")) 57 | } 58 | 59 | func DefaultIsTerminal() bool { 60 | return IsTerminal(syscall.Stdin) && (IsTerminal(syscall.Stdout) || IsTerminal(syscall.Stderr)) 61 | } 62 | 63 | func GetStdin() int { 64 | return syscall.Stdin 65 | } 66 | 67 | // ----------------------------------------------------------------------------- 68 | 69 | var ( 70 | widthChange sync.Once 71 | widthChangeCallback func() 72 | ) 73 | 74 | func DefaultOnWidthChanged(f func()) { 75 | widthChangeCallback = f 76 | widthChange.Do(func() { 77 | ch := make(chan os.Signal, 1) 78 | signal.Notify(ch, syscall.SIGWINCH) 79 | 80 | go func() { 81 | for { 82 | _, ok := <-ch 83 | if !ok { 84 | break 85 | } 86 | widthChangeCallback() 87 | } 88 | }() 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /readline/utils_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import ( 6 | "io" 7 | "syscall" 8 | ) 9 | 10 | func SuspendMe() { 11 | } 12 | 13 | func GetStdin() int { 14 | return int(syscall.Stdin) 15 | } 16 | 17 | func init() { 18 | isWindows = true 19 | } 20 | 21 | // get width of the terminal 22 | func GetScreenWidth() int { 23 | info, _ := GetConsoleScreenBufferInfo() 24 | if info == nil { 25 | return -1 26 | } 27 | return int(info.dwSize.x) 28 | } 29 | 30 | // ClearScreen clears the console screen 31 | func ClearScreen(_ io.Writer) error { 32 | return SetConsoleCursorPosition(&_COORD{0, 0}) 33 | } 34 | 35 | func DefaultIsTerminal() bool { 36 | return true 37 | } 38 | 39 | func DefaultOnWidthChanged(func()) { 40 | 41 | } 42 | -------------------------------------------------------------------------------- /readline/vim.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | const ( 4 | VIM_NORMAL = iota 5 | VIM_INSERT 6 | VIM_VISUAL 7 | ) 8 | 9 | type opVim struct { 10 | cfg *Config 11 | op *Operation 12 | vimMode int 13 | } 14 | 15 | func newVimMode(op *Operation) *opVim { 16 | ov := &opVim{ 17 | cfg: op.cfg, 18 | op: op, 19 | } 20 | ov.SetVimMode(ov.cfg.VimMode) 21 | return ov 22 | } 23 | 24 | func (o *opVim) SetVimMode(on bool) { 25 | if o.cfg.VimMode && !on { // turn off 26 | o.ExitVimMode() 27 | } 28 | o.cfg.VimMode = on 29 | o.vimMode = VIM_INSERT 30 | } 31 | 32 | func (o *opVim) ExitVimMode() { 33 | o.vimMode = VIM_INSERT 34 | } 35 | 36 | func (o *opVim) IsEnableVimMode() bool { 37 | return o.cfg.VimMode 38 | } 39 | 40 | func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { 41 | rb := o.op.buf 42 | handled = true 43 | switch r { 44 | case 'h': 45 | t = CharBackward 46 | case 'j': 47 | t = CharNext 48 | case 'k': 49 | t = CharPrev 50 | case 'l': 51 | t = CharForward 52 | case '0', '^': 53 | rb.MoveToLineStart() 54 | case '$': 55 | rb.MoveToLineEnd() 56 | case 'x': 57 | rb.Delete() 58 | if rb.IsCursorInEnd() { 59 | rb.MoveBackward() 60 | } 61 | case 'r': 62 | rb.Replace(readNext()) 63 | case 'd': 64 | next := readNext() 65 | switch next { 66 | case 'd': 67 | rb.Erase() 68 | case 'w': 69 | rb.DeleteWord() 70 | case 'h': 71 | rb.Backspace() 72 | case 'l': 73 | rb.Delete() 74 | } 75 | case 'b', 'B': 76 | rb.MoveToPrevWord() 77 | case 'w', 'W': 78 | rb.MoveToNextWord() 79 | case 'e', 'E': 80 | rb.MoveToEndWord() 81 | case 'f', 'F', 't', 'T': 82 | next := readNext() 83 | prevChar := r == 't' || r == 'T' 84 | reverse := r == 'F' || r == 'T' 85 | switch next { 86 | case CharEsc: 87 | default: 88 | rb.MoveTo(next, prevChar, reverse) 89 | } 90 | default: 91 | return r, false 92 | } 93 | return t, true 94 | } 95 | 96 | func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) { 97 | rb := o.op.buf 98 | handled = true 99 | switch r { 100 | case 'i': 101 | case 'I': 102 | rb.MoveToLineStart() 103 | case 'a': 104 | rb.MoveForward() 105 | case 'A': 106 | rb.MoveToLineEnd() 107 | case 's': 108 | rb.Delete() 109 | case 'S': 110 | rb.Erase() 111 | case 'c': 112 | next := readNext() 113 | switch next { 114 | case 'c': 115 | rb.Erase() 116 | case 'w': 117 | rb.DeleteWord() 118 | case 'h': 119 | rb.Backspace() 120 | case 'l': 121 | rb.Delete() 122 | } 123 | default: 124 | return r, false 125 | } 126 | 127 | o.EnterVimInsertMode() 128 | return 129 | } 130 | 131 | func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) { 132 | switch r { 133 | case CharEnter, CharInterrupt: 134 | o.ExitVimMode() 135 | return r 136 | } 137 | 138 | if r, handled := o.handleVimNormalMovement(r, readNext); handled { 139 | return r 140 | } 141 | 142 | if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled { 143 | return r 144 | } 145 | 146 | // invalid operation 147 | o.op.t.Bell() 148 | return 0 149 | } 150 | 151 | func (o *opVim) EnterVimInsertMode() { 152 | o.vimMode = VIM_INSERT 153 | } 154 | 155 | func (o *opVim) ExitVimInsertMode() { 156 | o.vimMode = VIM_NORMAL 157 | } 158 | 159 | func (o *opVim) HandleVim(r rune, readNext func() rune) rune { 160 | if o.vimMode == VIM_NORMAL { 161 | return o.HandleVimNormal(r, readNext) 162 | } 163 | if r == CharEsc { 164 | o.ExitVimInsertMode() 165 | return 0 166 | } 167 | 168 | switch o.vimMode { 169 | case VIM_INSERT: 170 | return r 171 | case VIM_VISUAL: 172 | } 173 | return r 174 | } 175 | -------------------------------------------------------------------------------- /readline/windows_api.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import ( 6 | "reflect" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | kernel = NewKernel() 13 | stdout = uintptr(syscall.Stdout) 14 | stdin = uintptr(syscall.Stdin) 15 | ) 16 | 17 | type Kernel struct { 18 | SetConsoleCursorPosition, 19 | SetConsoleTextAttribute, 20 | FillConsoleOutputCharacterW, 21 | FillConsoleOutputAttribute, 22 | ReadConsoleInputW, 23 | GetConsoleScreenBufferInfo, 24 | GetConsoleCursorInfo, 25 | GetStdHandle CallFunc 26 | } 27 | 28 | type short int16 29 | type word uint16 30 | type dword uint32 31 | type wchar uint16 32 | 33 | type _COORD struct { 34 | x short 35 | y short 36 | } 37 | 38 | func (c *_COORD) ptr() uintptr { 39 | return uintptr(*(*int32)(unsafe.Pointer(c))) 40 | } 41 | 42 | const ( 43 | EVENT_KEY = 0x0001 44 | EVENT_MOUSE = 0x0002 45 | EVENT_WINDOW_BUFFER_SIZE = 0x0004 46 | EVENT_MENU = 0x0008 47 | EVENT_FOCUS = 0x0010 48 | ) 49 | 50 | type _KEY_EVENT_RECORD struct { 51 | bKeyDown int32 52 | wRepeatCount word 53 | wVirtualKeyCode word 54 | wVirtualScanCode word 55 | unicodeChar wchar 56 | dwControlKeyState dword 57 | } 58 | 59 | // KEY_EVENT_RECORD KeyEvent; 60 | // MOUSE_EVENT_RECORD MouseEvent; 61 | // WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; 62 | // MENU_EVENT_RECORD MenuEvent; 63 | // FOCUS_EVENT_RECORD FocusEvent; 64 | type _INPUT_RECORD struct { 65 | EventType word 66 | Padding uint16 67 | Event [16]byte 68 | } 69 | 70 | type _CONSOLE_SCREEN_BUFFER_INFO struct { 71 | dwSize _COORD 72 | dwCursorPosition _COORD 73 | wAttributes word 74 | srWindow _SMALL_RECT 75 | dwMaximumWindowSize _COORD 76 | } 77 | 78 | type _SMALL_RECT struct { 79 | left short 80 | top short 81 | right short 82 | bottom short 83 | } 84 | 85 | type _CONSOLE_CURSOR_INFO struct { 86 | dwSize dword 87 | bVisible bool 88 | } 89 | 90 | type CallFunc func(u ...uintptr) error 91 | 92 | func NewKernel() *Kernel { 93 | k := &Kernel{} 94 | kernel32 := syscall.NewLazyDLL("kernel32.dll") 95 | v := reflect.ValueOf(k).Elem() 96 | t := v.Type() 97 | for i := 0; i < t.NumField(); i++ { 98 | name := t.Field(i).Name 99 | f := kernel32.NewProc(name) 100 | v.Field(i).Set(reflect.ValueOf(k.Wrap(f))) 101 | } 102 | return k 103 | } 104 | 105 | func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc { 106 | return func(args ...uintptr) error { 107 | var r0 uintptr 108 | var e1 syscall.Errno 109 | size := uintptr(len(args)) 110 | if len(args) <= 3 { 111 | buf := make([]uintptr, 3) 112 | copy(buf, args) 113 | r0, _, e1 = syscall.Syscall(p.Addr(), size, 114 | buf[0], buf[1], buf[2]) 115 | } else { 116 | buf := make([]uintptr, 6) 117 | copy(buf, args) 118 | r0, _, e1 = syscall.Syscall6(p.Addr(), size, 119 | buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], 120 | ) 121 | } 122 | 123 | if int(r0) == 0 { 124 | if e1 != 0 { 125 | return error(e1) 126 | } else { 127 | return syscall.EINVAL 128 | } 129 | } 130 | return nil 131 | } 132 | 133 | } 134 | 135 | func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) { 136 | t := new(_CONSOLE_SCREEN_BUFFER_INFO) 137 | err := kernel.GetConsoleScreenBufferInfo( 138 | stdout, 139 | uintptr(unsafe.Pointer(t)), 140 | ) 141 | return t, err 142 | } 143 | 144 | func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) { 145 | t := new(_CONSOLE_CURSOR_INFO) 146 | err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t))) 147 | return t, err 148 | } 149 | 150 | func SetConsoleCursorPosition(c *_COORD) error { 151 | return kernel.SetConsoleCursorPosition(stdout, c.ptr()) 152 | } 153 | -------------------------------------------------------------------------------- /scanner/examples_test.go: -------------------------------------------------------------------------------- 1 | package scanner_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/madlambda/nash/scanner" 7 | ) 8 | 9 | func Example() { 10 | lex := scanner.Lex("-input-", `echo "hello world"`) 11 | 12 | for tok := range lex.Tokens { 13 | fmt.Println(tok) 14 | } 15 | 16 | // Output: 17 | // IDENT 18 | // STRING 19 | // ; 20 | // EOF 21 | 22 | } 23 | -------------------------------------------------------------------------------- /sh/obj.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import "fmt" 4 | 5 | //go:generate stringer -type=objType 6 | const ( 7 | StringType objType = iota + 1 8 | FnType 9 | ListType 10 | ) 11 | 12 | type ( 13 | objType int 14 | 15 | Obj interface { 16 | Type() objType 17 | String() string 18 | } 19 | 20 | ListObj struct { 21 | objType 22 | list []Obj 23 | } 24 | 25 | FnObj struct { 26 | objType 27 | fn FnDef 28 | } 29 | 30 | StrObj struct { 31 | objType 32 | runes []rune 33 | } 34 | 35 | Collection interface { 36 | Len() int 37 | Get(index int) (Obj, error) 38 | } 39 | 40 | WriteableCollection interface { 41 | Set(index int, val Obj) error 42 | } 43 | ) 44 | 45 | func NewCollection(o Obj) (Collection, error) { 46 | sizer, ok := o.(Collection) 47 | if !ok { 48 | return nil, fmt.Errorf( 49 | "SizeError: trying to get size from type %s which is not a collection", 50 | o.Type(), 51 | ) 52 | } 53 | return sizer, nil 54 | } 55 | 56 | func NewWriteableCollection(o Obj) (WriteableCollection, error) { 57 | indexer, ok := o.(WriteableCollection) 58 | if !ok { 59 | return nil, fmt.Errorf( 60 | "IndexError: trying to use a non write/indexable type %s to write on index: ", 61 | o.Type(), 62 | ) 63 | } 64 | return indexer, nil 65 | } 66 | 67 | func (o objType) Type() objType { 68 | return o 69 | } 70 | 71 | func NewStrObj(val string) *StrObj { 72 | return &StrObj{ 73 | runes: []rune(val), 74 | objType: StringType, 75 | } 76 | } 77 | 78 | func (o *StrObj) Str() string { return string(o.runes) } 79 | 80 | func (o *StrObj) String() string { return o.Str() } 81 | 82 | func (o *StrObj) Get(index int) (Obj, error) { 83 | if index >= o.Len() { 84 | return nil, fmt.Errorf( 85 | "IndexError: Index[%d] out of range, string size[%d]", 86 | index, 87 | o.Len(), 88 | ) 89 | } 90 | 91 | return NewStrObj(string(o.runes[index])), nil 92 | } 93 | 94 | func (o *StrObj) Len() int { 95 | return len(o.runes) 96 | } 97 | 98 | func NewFnObj(val FnDef) *FnObj { 99 | return &FnObj{ 100 | fn: val, 101 | objType: FnType, 102 | } 103 | } 104 | 105 | func (o *FnObj) Fn() FnDef { return o.fn } 106 | 107 | func (o *FnObj) String() string { return fmt.Sprintf("", o.fn.Name()) } 108 | 109 | func NewListObj(val []Obj) *ListObj { 110 | return &ListObj{ 111 | list: val, 112 | objType: ListType, 113 | } 114 | } 115 | 116 | func (o *ListObj) Len() int { 117 | return len(o.list) 118 | } 119 | 120 | func (o *ListObj) Set(index int, value Obj) error { 121 | if index >= len(o.list) { 122 | return fmt.Errorf( 123 | "IndexError: Index[%d] out of range, list size[%d]", 124 | index, 125 | len(o.list), 126 | ) 127 | } 128 | o.list[index] = value 129 | return nil 130 | } 131 | 132 | func (o *ListObj) Get(index int) (Obj, error) { 133 | if index >= len(o.list) { 134 | return nil, fmt.Errorf( 135 | "IndexError: Index out of bounds, index[%d] but list size[%d]", 136 | index, 137 | len(o.list), 138 | ) 139 | } 140 | return o.list[index], nil 141 | } 142 | 143 | func (o *ListObj) List() []Obj { return o.list } 144 | 145 | func (o *ListObj) String() string { 146 | result := "" 147 | list := o.List() 148 | for i := 0; i < len(list); i++ { 149 | l := list[i] 150 | 151 | result += l.String() 152 | 153 | if i < len(list)-1 { 154 | result += " " 155 | } 156 | } 157 | 158 | return result 159 | } 160 | -------------------------------------------------------------------------------- /sh/objtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=objType"; DO NOT EDIT 2 | 3 | package sh 4 | 5 | import "fmt" 6 | 7 | const _objType_name = "StringTypeFnTypeListType" 8 | 9 | var _objType_index = [...]uint8{0, 10, 16, 24} 10 | 11 | func (i objType) String() string { 12 | i -= 1 13 | if i < 0 || i >= objType(len(_objType_index)-1) { 14 | return fmt.Sprintf("objType(%d)", i+1) 15 | } 16 | return _objType_name[_objType_index[i]:_objType_index[i+1]] 17 | } 18 | -------------------------------------------------------------------------------- /sh/shell.go: -------------------------------------------------------------------------------- 1 | package sh 2 | 3 | import "io" 4 | 5 | type ( 6 | Runner interface { 7 | Start() error 8 | Wait() error 9 | Results() []Obj 10 | 11 | SetArgs([]Obj) error 12 | SetEnviron([]string) 13 | SetStdin(io.Reader) 14 | SetStdout(io.Writer) 15 | SetStderr(io.Writer) 16 | 17 | StdoutPipe() (io.ReadCloser, error) 18 | 19 | Stdin() io.Reader 20 | Stdout() io.Writer 21 | Stderr() io.Writer 22 | } 23 | 24 | FnArg struct { 25 | Name string 26 | IsVariadic bool 27 | } 28 | 29 | Fn interface { 30 | Name() string 31 | ArgNames() []FnArg 32 | 33 | Runner 34 | 35 | String() string 36 | } 37 | 38 | FnDef interface { 39 | Name() string 40 | ArgNames() []FnArg 41 | Build() Fn 42 | } 43 | ) 44 | 45 | func NewFnArg(name string, isVariadic bool) FnArg { 46 | return FnArg{ 47 | Name: name, 48 | IsVariadic: isVariadic, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /spec.ebnf: -------------------------------------------------------------------------------- 1 | /* Nash program */ 2 | program = { statement } . 3 | 4 | /* Statement */ 5 | statement = varDecl | command | fnInv | builtin | comment . 6 | 7 | /* Variable declaration */ 8 | varDecl = assignValue | assignCmdOut . 9 | assignValue = identifierList "=" varSpecList . 10 | identifierList = identifier [ "," identifierList ] . 11 | varSpecList = varSpec [ "," varSpecList ] . 12 | varSpec = ( list | string ) . 13 | string = stringLit | ( stringConcat { stringConcat } ) . 14 | assignCmdOut = identifier "<=" ( command | fnInv ) . 15 | 16 | /* Command */ 17 | command = ( [ "(" ] cmdpart [ ")" ] | pipe ) . 18 | cmdpart = [ "-" ] ( cmdname | abscmd ) { argument } { redirect } . 19 | cmdname = identifier . 20 | abscmd = filename . 21 | argument = ( unicode_char { unicode_char } ) | stringLit . 22 | pipe = [ "(" ] cmdpart "|" cmdpart [ { "|" cmdpart } ] [ ")" ] . 23 | redirect = ( ">" ( filename | uri | variable ) | 24 | ">" "[" unicode_digit "]" ( filename | uri | variable ) | 25 | ">" "[" unicode_digit "=" ( unicode_digit | identifier ) "]" | 26 | ">" "[" unicode_digit "=" "]" ) . 27 | 28 | /* Builtin */ 29 | builtin = importDecl | rforkDecl | ifDecl | forDecl | setenvDecl | 30 | fnDecl | bindfn | dump . 31 | 32 | /* Import statement */ 33 | importDecl = "import" ( filename | stringLit ) . 34 | 35 | /* Rfork scope */ 36 | rforkDecl = "rfork" rforkFlags "{" program "}" . 37 | rforkFlags = { identifier } . 38 | 39 | /* If-else-if */ 40 | ifDecl = "if" ( variable | string ) comparison 41 | ( variable | string ) "{" program "}" 42 | [ "else" "{" program "}" ] 43 | [ "else" ifDecl ] . 44 | 45 | /* For loop */ 46 | forDecl = "for" [ identifier "in" ( list | variable | fnInv) ] "{" program "}" . 47 | 48 | /* Function declaration */ 49 | fnDecl = "fn" identifier "(" fnArgs ")" "{" 50 | program [ returnDecl ] 51 | "}" . 52 | fnArgs = { fnArg [ "," ] } . 53 | fnArg = identifier [ "..." ] . 54 | 55 | /* return declaration */ 56 | returnDecl = "return" [ ( variable | stringLit | list | fnInv ) ] . 57 | 58 | /* Function invocation */ 59 | fnInv = ( variable | identifier ) "(" fnArgValues ")" . 60 | 61 | fnArgValues = { fnArgValue [ "," ] } . 62 | fnArgValue = [ stringLit | stringConcat | list | (variable [ "..." ]) | (list [ "..." ]) fnInv ] . 63 | 64 | /* Function binding */ 65 | bindfn = "bindfn" identifier identifier . 66 | 67 | /* dump shell state */ 68 | dump = "dump" [ filename ] . 69 | 70 | /* Set environment variable */ 71 | setenvDecl = "setenv" ( identifier | varDecl ) . 72 | 73 | /* Comment */ 74 | comment = "#" { unicode_char } . 75 | 76 | /* Lists */ 77 | list = "(" { argument } ")" . 78 | 79 | letter = unicode_letter | "_" . 80 | filename = { [ "/" ] { unicode_letter } } . 81 | ipaddr = unicode_digit { unicode_digit } "." 82 | unicode_digit { unicode_digit } "." 83 | unicode_digit { unicode_digit } "." 84 | unicode_digit { unicode_digit } "." . 85 | port = unicode_digit { unicode_digit } . 86 | networkaddr = ipaddr ":" port . 87 | location = filename | networkaddr . 88 | schema = "file" | "tcp" | "udp" | "unix" . 89 | uri = schema "://" location . 90 | 91 | identifier = letter { letter | unicode_digit } . 92 | variable = "$" identifier . 93 | 94 | comparison = "==" | "!=" . 95 | 96 | stringLit = "\"" { unicode_char | newline } "\"" . 97 | 98 | stringConcat = ( stringLit | variable ) "+" (stringLit | variable ) . 99 | 100 | /* terminals */ 101 | newline = /* the Unicode code point U+000A */ . 102 | unicode_char = /* an arbitrary Unicode code point except newline */ . 103 | unicode_letter = /* a Unicode code point classified as "Letter" */ . 104 | unicode_digit = /* a Unicode code point classified as "Number, decimal digit" */ . 105 | -------------------------------------------------------------------------------- /spec_test.go: -------------------------------------------------------------------------------- 1 | package nash 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/madlambda/nash/tests" 10 | "golang.org/x/exp/ebnf" 11 | ) 12 | 13 | func TestSpecificationIsSane(t *testing.T) { 14 | filename := filepath.Join(tests.Projectpath, "spec.ebnf") 15 | content, err := ioutil.ReadFile(filename) 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | 21 | buf := bytes.NewBuffer(content) 22 | grammar, err := ebnf.Parse(filename, buf) 23 | if err != nil { 24 | t.Error(err) 25 | return 26 | } 27 | 28 | err = ebnf.Verify(grammar, "program") 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stdbin/mkdir/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | ) 7 | 8 | func mkdirs(dirnames []string) error { 9 | for _, d := range dirnames { 10 | if err := os.MkdirAll(d, 0644); err != nil { 11 | return err 12 | } 13 | } 14 | 15 | return nil 16 | } 17 | 18 | func main() { 19 | if len(os.Args) < 2 { 20 | fmt.Fprintf(os.Stderr, "usage: %s ...\n", os.Args[0]) 21 | os.Exit(1) 22 | } 23 | err := mkdirs(os.Args[1:]) 24 | if err != nil { 25 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 26 | os.Exit(1) 27 | } 28 | } -------------------------------------------------------------------------------- /stdbin/mkdir/mkdir_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "path" 6 | "path/filepath" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | type testcase struct { 12 | dirs []string 13 | } 14 | 15 | func testMkdir(t *testing.T, tc testcase) { 16 | tmpDir, err := ioutil.TempDir("", "nash-mkdir") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | defer os.RemoveAll(tmpDir) 22 | var dirs []string 23 | for _, p := range tc.dirs { 24 | dirs = append(dirs, filepath.FromSlash(path.Join(tmpDir, p))) 25 | } 26 | 27 | err = mkdirs(dirs) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | for _, d := range dirs { 33 | if s, err := os.Stat(d); err != nil { 34 | t.Fatal(err) 35 | } else if s.Mode()&os.ModeDir != os.ModeDir { 36 | t.Fatalf("Invalid directory mode: %v", s.Mode()) 37 | } 38 | } 39 | } 40 | 41 | func TestMkdir(t *testing.T) { 42 | for _, tc := range []testcase{ 43 | { 44 | dirs: []string{}, 45 | }, 46 | { 47 | dirs: []string{ 48 | "1", "2", "3", "4", "5", 49 | "some", "thing", "_random_", 50 | "_", 51 | }, 52 | }, 53 | { 54 | dirs: []string{"a", "a"}, // directory already exists, silently works 55 | }, 56 | } { 57 | tc := tc 58 | testMkdir(t, tc) 59 | } 60 | } -------------------------------------------------------------------------------- /stdbin/pwd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | wd, err := os.Getwd() 10 | if err != nil { 11 | fmt.Fprintf(os.Stderr, "error: %s", err) 12 | os.Exit(1) 13 | } 14 | fmt.Println(wd) 15 | } 16 | -------------------------------------------------------------------------------- /stdbin/strings/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | 11 | const defaultMinTextSize = 4 12 | var minTextSize uint 13 | 14 | flag.UintVar( 15 | &minTextSize, 16 | "s", 17 | defaultMinTextSize, 18 | "the minimum size in runes to characterize as a text", 19 | ) 20 | 21 | scanner := Do(os.Stdin, minTextSize) 22 | for scanner.Scan() { 23 | fmt.Println(scanner.Text()) 24 | } 25 | if scanner.Err() != nil { 26 | fmt.Fprintf(os.Stderr, "error: %s", scanner.Err()) 27 | os.Exit(1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stdbin/write/common_test.sh: -------------------------------------------------------------------------------- 1 | # common test routines 2 | 3 | fn fatal(msg) { 4 | print($msg) 5 | exit("1") 6 | } 7 | 8 | fn assert(expected, got, desc) { 9 | if $expected != $got { 10 | fatal(format("%s: FAILED. Expected[%s] but got[%s]\n", $desc, $expected, $got)) 11 | } 12 | } -------------------------------------------------------------------------------- /stdbin/write/fd.go: -------------------------------------------------------------------------------- 1 | //+build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "os" 8 | ) 9 | 10 | func specialFile(path string) (io.WriteCloser, bool) { 11 | if path == "/dev/stdout" { 12 | return os.Stdout, true 13 | } else if path == "/dev/stderr" { 14 | return os.Stderr, true 15 | } 16 | return nil, false 17 | } 18 | -------------------------------------------------------------------------------- /stdbin/write/fd_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | func specialFile(path string) (io.WriteCloser, bool) { 9 | if path == "CON" { // holycrap! 10 | return os.Stdout, true 11 | } 12 | return nil, false 13 | } -------------------------------------------------------------------------------- /stdbin/write/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | var banner = fmt.Sprintf("%s \n", os.Args[0]) 11 | 12 | func fatal(msg string) { 13 | fmt.Fprintf(os.Stderr, "%s", msg) 14 | os.Exit(1) 15 | } 16 | 17 | func main() { 18 | if len(os.Args) <= 1 || 19 | len(os.Args) > 3 { 20 | fatal(banner) 21 | } 22 | 23 | var ( 24 | fname = os.Args[1] 25 | in io.Reader 26 | ) 27 | 28 | if len(os.Args) == 2 { 29 | in = os.Stdin 30 | } else { 31 | in = bytes.NewBufferString(os.Args[2]) 32 | } 33 | 34 | err := write(fname, in) 35 | if err != nil { 36 | fatal(err.Error()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /stdbin/write/write.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func toabs(path string) (string, error) { 10 | if !filepath.IsAbs(path) { 11 | wd, err := os.Getwd() 12 | if err != nil { 13 | return "", err 14 | } 15 | path = filepath.Join(wd, path) 16 | } 17 | return path, nil 18 | } 19 | 20 | func outfd(fname string) (io.WriteCloser, error) { 21 | fname, err := toabs(fname) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | var out io.WriteCloser 27 | 28 | out, ok := specialFile(fname) 29 | if !ok { 30 | f, err := os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) 31 | if err != nil { 32 | return nil, err 33 | } 34 | out = f 35 | } 36 | return out, nil 37 | } 38 | 39 | func write(fname string, in io.Reader) (err error) { 40 | out, err := outfd(fname) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | defer func() { 46 | err2 := out.Close() 47 | if err == nil { 48 | err = err2 49 | } 50 | }() 51 | 52 | _, err = io.Copy(out, in) 53 | return err 54 | } 55 | -------------------------------------------------------------------------------- /stdbin/write/write_linux_test.sh: -------------------------------------------------------------------------------- 1 | # linux tests of write command 2 | 3 | import "./common_test.sh" 4 | 5 | # this test uses only the write binary 6 | setenv PATH = "./stdbin/write" 7 | 8 | # (desc (out err status)) 9 | var tests = ( 10 | ("standard out" ("/dev/stdout" "hello world" "" "0")) 11 | ("standard err" ("/dev/stderr" "" "hello world" "0")) 12 | ) 13 | 14 | var outstr = "hello world" 15 | 16 | for test in $tests { 17 | var desc = $test[0] 18 | var tc = $test[1] 19 | 20 | print("testing %s\n", $desc) 21 | 22 | var device = $tc[0] 23 | var expectedOut = $tc[1] 24 | var expectedErr = $tc[2] 25 | var expectedSts = $tc[3] 26 | 27 | var out, err, status <= write $device $outstr 28 | assert($expectedSts, $status, "status code") 29 | assert($expectedOut, $out, "standard output") 30 | assert($expectedErr, $err, "standard error") 31 | } 32 | -------------------------------------------------------------------------------- /stdbin/write/write_test.sh: -------------------------------------------------------------------------------- 1 | import "./common_test.sh" 2 | 3 | setenv PATH = "./stdbin/write:/bin" 4 | 5 | # FIXME: we need our mktemp 6 | var nonExistentFile = "./here-be-dragons" 7 | 8 | fn clean() { 9 | _, _ <= rm -f $nonExistentFile 10 | } 11 | 12 | clean() 13 | 14 | var out, err, status <= write $nonExistentFile "hello" 15 | assert("", $out, "standard out isnt empty") 16 | assert("", $err, "standard err isnt empty") 17 | assert("0", $status, "status is not success") 18 | 19 | var content, status <= cat $nonExistentFile 20 | assert("0", $status, "status is not success") 21 | assert("hello", $content, "file content is wrong") 22 | 23 | # test append 24 | out, err, status <= write $nonExistentFile "1" 25 | assert("", $out, "standard out isnt empty") 26 | assert("", $err, "standard err isnt empty") 27 | assert("0", $status, "status is not success") 28 | 29 | content, status <= cat $nonExistentFile 30 | assert("0", $status, "status is not success") 31 | assert("hello1", $content, "file content is wrong") 32 | 33 | clean() 34 | -------------------------------------------------------------------------------- /stdlib/io.sh: -------------------------------------------------------------------------------- 1 | 2 | fn io_println(msg, args...) { 3 | print($msg + "\n", $args...) 4 | } 5 | -------------------------------------------------------------------------------- /stdlib/io_example.sh: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | if len($ARGS) == "2" { 4 | io_println($ARGS[1]) 5 | exit("0") 6 | } 7 | 8 | io_println($ARGS[1], $ARGS[2]) 9 | -------------------------------------------------------------------------------- /stdlib/io_test.sh: -------------------------------------------------------------------------------- 1 | 2 | fn run_example(args...) { 3 | var got, status <= ./cmd/nash/nash ./stdlib/io_example.sh $args 4 | return $got, $status 5 | } 6 | 7 | fn assert_success(expected, got, status) { 8 | if $status != "0" { 9 | print("expected success, but got status code: %s\n", $status) 10 | exit("1") 11 | } 12 | if $got != $expected { 13 | print("expected [%s] got [%s]\n", $expected, $got) 14 | exit("1") 15 | } 16 | } 17 | 18 | fn test_println_format() { 19 | var got, status <= run_example("hello %s", "world") 20 | 21 | assert_success("hello world", $got, $status) 22 | } 23 | 24 | fn test_println() { 25 | var expected = "pazu" 26 | var got, status <= run_example($expected) 27 | 28 | assert_success($expected, $got, $status) 29 | } 30 | 31 | test_println_format() 32 | test_println() 33 | -------------------------------------------------------------------------------- /stdlib/map.sh: -------------------------------------------------------------------------------- 1 | fn map_new() { 2 | return () 3 | } 4 | 5 | fn map_get(map, key) { 6 | return map_get_default($map, $key, "") 7 | } 8 | 9 | fn map_iter(map, func) { 10 | for entry in $map { 11 | $func($entry[0], $entry[1]) 12 | } 13 | } 14 | 15 | fn map_get_default(map, key, default) { 16 | for entry in $map { 17 | if $entry[0] == $key { 18 | return $entry[1] 19 | } 20 | } 21 | 22 | return $default 23 | } 24 | 25 | fn map_add(map, key, val) { 26 | for entry in $map { 27 | if $entry[0] == $key { 28 | entry[1] = $val 29 | return $map 30 | } 31 | } 32 | 33 | var tuple = ($key $val) 34 | map <= append($map, $tuple) 35 | return $map 36 | } 37 | 38 | fn map_del(map, key) { 39 | var newmap = () 40 | 41 | for entry in $map { 42 | if $entry[0] != $key { 43 | var tuple = ($entry[0] $entry[1]) 44 | newmap <= append($newmap, $tuple) 45 | } 46 | } 47 | 48 | return $newmap 49 | } 50 | -------------------------------------------------------------------------------- /stdlib/map_test.sh: -------------------------------------------------------------------------------- 1 | import map 2 | 3 | fn expect(map, key, want) { 4 | var got <= map_get($map, $key) 5 | if $got != $want { 6 | echo "error: got["+$got+"] want["+$want+"]" 7 | exit("1") 8 | } 9 | } 10 | 11 | fn test_adding_keys() { 12 | var map <= map_new() 13 | 14 | map <= map_add($map, "key", "value") 15 | expect($map, "key", "value") 16 | 17 | map <= map_add($map, "key2", "value2") 18 | expect($map, "key2", "value2") 19 | 20 | map <= map_add($map, "key", "override") 21 | expect($map, "key", "override") 22 | } 23 | 24 | fn test_absent_key_will_have_empty_string_value() { 25 | var map <= map_new() 26 | expect($map, "absent", "") 27 | } 28 | 29 | fn test_absent_key_with_custom_default_value() { 30 | var map <= map_new() 31 | var want = "hi" 32 | var got <= map_get_default($map, "absent", $want) 33 | if $got != $want { 34 | echo "error: got["+$got+"] want["+$want+"]" 35 | exit("1") 36 | } 37 | } 38 | 39 | fn test_iterates_map() { 40 | var map <= map_new() 41 | map <= map_add($map, "key", "value") 42 | map <= map_add($map, "key2", "value2") 43 | 44 | var got <= map_new() 45 | 46 | fn iter(key, val) { 47 | got <= map_add($got, $key, $val) 48 | } 49 | 50 | map_iter($map, $iter) 51 | 52 | expect($map, "key", "value") 53 | expect($map, "key2", "value2") 54 | } 55 | 56 | fn test_removing_key() { 57 | var map <= map_new() 58 | 59 | map <= map_add($map, "key", "value") 60 | map <= map_add($map, "key2", "value2") 61 | 62 | expect($map, "key", "value") 63 | expect($map, "key2", "value2") 64 | 65 | map <= map_del($map, "key") 66 | expect($map, "key", "") 67 | expect($map, "key2", "value2") 68 | } 69 | 70 | fn test_removing_absent_key() { 71 | var map <= map_new() 72 | 73 | expect($map, "key", "") 74 | map <= map_del($map, "key") 75 | expect($map, "key", "") 76 | } 77 | 78 | test_adding_keys() 79 | test_absent_key_will_have_empty_string_value() 80 | test_absent_key_with_custom_default_value() 81 | test_iterates_map() 82 | test_removing_key() 83 | test_removing_absent_key() 84 | -------------------------------------------------------------------------------- /testfiles/ex1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/cnt 2 | 3 | echo "hello world" 4 | -------------------------------------------------------------------------------- /testfiles/fibonacci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | # Recursive fibonacci implementation to find the value 4 | # at index n in the sequence. 5 | 6 | # Some times: 7 | # λ> time ./testfiles/fibonacci.sh 1 8 | # 1 9 | # 0.00u 0.01s 0.01r ./testfiles/fibonacci.sh 1 10 | # λ> time ./testfiles/fibonacci.sh 2 11 | # 1 2 12 | # 0.01u 0.01s 0.02r ./testfiles/fibonacci.sh 2 13 | # λ> time ./testfiles/fibonacci.sh 3 14 | # 1 2 3 15 | # 0.01u 0.03s 0.03r ./testfiles/fibonacci.sh 3 16 | # λ> time ./testfiles/fibonacci.sh 4 17 | # 1 2 3 5 18 | # 0.04u 0.04s 0.07r ./testfiles/fibonacci.sh 4 19 | # λ> time ./testfiles/fibonacci.sh 5 20 | # 1 2 3 5 8 21 | # 0.09u 0.07s 0.13r ./testfiles/fibonacci.sh 5 22 | # λ> time ./testfiles/fibonacci.sh 10 23 | # 1 2 3 5 8 13 21 34 55 89 24 | # 1.31u 1.18s 2.03r ./testfiles/fibonacci.sh 10 25 | # λ> time ./testfiles/fibonacci.sh 15 26 | # 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 27 | # 15.01u 13.49s 22.55r ./testfiles/fibonacci.sh 15 28 | # λ> time ./testfiles/fibonacci.sh 20 29 | # 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 30 | # 169.27u 155.50s 265.19r ./testfiles/fibonacci.sh 20 31 | 32 | # a is lower or equal than b? 33 | fn lte(a, b) { 34 | var _, st <= test $a -le $b 35 | 36 | return $st 37 | } 38 | 39 | fn fib(n) { 40 | if lte($n, "1") == "0" { 41 | return "1" 42 | } 43 | 44 | var a, _ <= expr $n - 1 45 | var b, _ <= expr $n - 2 46 | var _a <= fib($a) 47 | var _b <= fib($b) 48 | var ret, _ <= expr $_a "+" $_b 49 | 50 | return $ret 51 | } 52 | 53 | fn range(start, end) { 54 | var seq, _ <= seq $start $end 55 | var lst <= split($seq, "\n") 56 | 57 | return $lst 58 | } 59 | 60 | for i in range("1", $ARGS[1]) { 61 | print("%s ", fib($i)) 62 | } 63 | 64 | print("\n") 65 | -------------------------------------------------------------------------------- /testfiles/sieve.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nash 2 | 3 | # Sieve of Erathostenes 4 | 5 | fn lt(a, b) { 6 | var _, st <= test $a -lt $b 7 | 8 | return $st 9 | } 10 | 11 | fn gt(a, b) { 12 | var _, st <= test $a -gt $b 13 | 14 | return $st 15 | } 16 | 17 | fn le(a, b) { 18 | var _, st <= test $a -le $b 19 | 20 | return $st 21 | } 22 | 23 | fn sqrt(n) { 24 | var v, _ <= expr $n * $n 25 | 26 | return $v 27 | } 28 | 29 | fn range(start, end) { 30 | var values, _ <= seq $start $end 31 | var list <= split($values, "\n") 32 | 33 | return $list 34 | } 35 | 36 | fn xrange(start, condfn) { 37 | var out = () 38 | 39 | if $condfn($start) == "0" { 40 | out = ($start) 41 | } else { 42 | return () 43 | } 44 | 45 | var next = $start 46 | 47 | for { 48 | next, _ <= expr $next "+" 1 49 | 50 | if $condfn($next) == "0" { 51 | out <= append($out, $next) 52 | } else { 53 | return $out 54 | } 55 | } 56 | 57 | unreachable 58 | } 59 | 60 | fn sieve(n) { 61 | if lt($n, "2") == "0" { 62 | return () 63 | } 64 | if $n == "2" { 65 | return ("2") 66 | } 67 | 68 | var tries = ("0" "0") 69 | 70 | for i in range("2", $n) { 71 | tries <= append($tries, "1") 72 | } 73 | 74 | fn untilSqrtRoot(v) { 75 | return le(sqrt($v), $n) 76 | } 77 | 78 | for i in xrange("2", $untilSqrtRoot) { 79 | if $tries[$i] == "1" { 80 | for j in range("0", $n) { 81 | # arithmetic seems cryptic without integers =( 82 | var k, _ <= expr $i * $i "+" "(" $j * $i ")" 83 | 84 | if gt($k, $n) != "0" { 85 | tries[$k] = "0" 86 | } 87 | } 88 | } 89 | } 90 | 91 | var primes = () 92 | 93 | for i in range("2", $n) { 94 | if $tries[$i] == "1" { 95 | primes <= append($primes, $i) 96 | } 97 | } 98 | 99 | return $primes 100 | } 101 | 102 | for prime in sieve($ARGS[1]) { 103 | print("%s ", $prime) 104 | } 105 | 106 | print("\n") 107 | -------------------------------------------------------------------------------- /tests/cfg.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | var ( 11 | // Nashcmd is the nash's absolute binary path in source 12 | Nashcmd string 13 | 14 | // Projectpath is the path to nash source code 15 | Projectpath string 16 | 17 | // Testdir is the test assets directory 18 | Testdir string 19 | 20 | // Stdbindir is the stdbin directory 21 | Stdbindir string 22 | ) 23 | 24 | func init() { 25 | 26 | Projectpath = findProjectRoot() 27 | Testdir = filepath.Join(Projectpath, "testfiles") 28 | Nashcmd = filepath.Join(Projectpath, "cmd", "nash", "nash") 29 | Stdbindir = filepath.Join(Projectpath, "stdbin") 30 | 31 | if runtime.GOOS == "windows" { 32 | Nashcmd += ".exe" 33 | } 34 | 35 | if _, err := os.Stat(Nashcmd); err != nil { 36 | msg := fmt.Sprintf("Unable to find nash command at %q.\n", Nashcmd) 37 | msg += "Please, run make build before running tests" 38 | panic(msg) 39 | } 40 | } 41 | 42 | func findProjectRoot() string { 43 | // We used to use GOPATH as a way to infer the root of the 44 | // project, now with Go modules this doesn't work anymore. 45 | // Since module definition files only appear on the root 46 | // of the project we use them instead, recursively going 47 | // backwards in the file system until we find them. 48 | // 49 | // From: https://blog.golang.org/using-go-modules 50 | // A module is a collection of Go packages stored in a file tree with a go.mod file at its root 51 | // 52 | // RIP GOPATH :-( 53 | 54 | separator := string(filepath.Separator) 55 | dir, err := os.Getwd() 56 | if err != nil { 57 | panic(fmt.Sprintf("failed to get current working directory:%v", err)) 58 | } 59 | 60 | for !hasGoModFile(dir) { 61 | if dir == separator { 62 | // FIXME: not sure if this will work on all OS's, perhaps we need some 63 | // other protection against infinite loops... or just trust go test timeout. 64 | panic("reached root of file system without finding project root") 65 | } 66 | dir = filepath.Dir(dir) 67 | } 68 | 69 | return dir 70 | } 71 | 72 | func hasGoModFile(path string) bool { 73 | info, err := os.Stat(path) 74 | if err != nil { 75 | return false 76 | } 77 | if !info.IsDir() { 78 | return false 79 | } 80 | 81 | gomodpath := filepath.Join(path, "go.mod") 82 | modinfo, err := os.Stat(gomodpath) 83 | if err != nil { 84 | return false 85 | } 86 | if modinfo.IsDir() { 87 | return false 88 | } 89 | return true 90 | } 91 | -------------------------------------------------------------------------------- /tests/doc.go: -------------------------------------------------------------------------------- 1 | // Package tests contains all nash tests that are blackbox. 2 | // What would be blackbox ? These are tests that are targeted 3 | // directly on top of the language using only the shell API, 4 | // they are end to end in the sense that they will exercise 5 | // a lot of different packages on a single test. 6 | // 7 | // The objective of these tests is to have a compreensive set 8 | // of tests that are coupled only the language specification 9 | // and not to how the language is implemented. These allows 10 | // extremely aggressive refactorings to be made without 11 | // incurring in any changes on the tests. 12 | // 13 | // There are disadvantages but discussing integration VS unit 14 | // testing here is not the point (there are also unit tests). 15 | // 16 | // Here even tests that involves the script calling syscalls like 17 | // exit are allowed without interfering with the results of other tests 18 | package tests 19 | -------------------------------------------------------------------------------- /tests/internal/assert/doc.go: -------------------------------------------------------------------------------- 1 | // Package assert has some common assert functions 2 | package assert 3 | -------------------------------------------------------------------------------- /tests/internal/assert/equal.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func EqualStrings(t *testing.T, want string, got string) { 9 | // TODO: could use t.Helper here, but only on Go 1.9 10 | if want != got { 11 | t.Fatalf("wanted[%s] but got[%s]", want, got) 12 | } 13 | } 14 | 15 | func ContainsString(t *testing.T, str string, sub string) { 16 | // TODO: could use t.Helper here, but only on Go 1.9 17 | if !strings.Contains(str, sub) { 18 | t.Fatalf("[%s] is not a substring of [%s]", sub, str) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/internal/assert/error.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "testing" 4 | 5 | func NoError(t *testing.T, err error, operation string) { 6 | if err != nil { 7 | t.Fatalf("error[%s] %s", err, operation) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/internal/sh/shell.go: -------------------------------------------------------------------------------- 1 | // Package shell makes it easier to run nash scripts for test purposes 2 | package sh 3 | 4 | import ( 5 | "bytes" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "testing" 11 | 12 | "github.com/madlambda/nash/tests/internal/assert" 13 | ) 14 | 15 | // Exec runs the script code and returns the result of it. 16 | func Exec( 17 | t *testing.T, 18 | nashpath string, 19 | scriptcode string, 20 | scriptargs ...string, 21 | ) (string, string, error) { 22 | scriptfile, err := ioutil.TempFile("", "testshell") 23 | assert.NoError(t, err, "creating tmp file") 24 | 25 | defer func() { 26 | err := scriptfile.Close() 27 | assert.NoError(t, err, "closing tmp file") 28 | err = os.Remove(scriptfile.Name()) 29 | assert.NoError(t, err, "deleting tmp file") 30 | }() 31 | 32 | _, err = io.Copy(scriptfile, bytes.NewBufferString(scriptcode)) 33 | assert.NoError(t, err, "writing script code to tmp file") 34 | 35 | scriptargs = append([]string{scriptfile.Name()}, scriptargs...) 36 | cmd := exec.Command(nashpath, scriptargs...) 37 | 38 | stdout := bytes.Buffer{} 39 | stderr := bytes.Buffer{} 40 | 41 | cmd.Stdout = &stdout 42 | cmd.Stderr = &stderr 43 | 44 | err = cmd.Run() 45 | return stdout.String(), stderr.String(), err 46 | } 47 | -------------------------------------------------------------------------------- /tests/internal/tester/tester.go: -------------------------------------------------------------------------------- 1 | // Package tester makes it easy to run multiple 2 | // script test cases. 3 | package tester 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/madlambda/nash/tests/internal/assert" 9 | "github.com/madlambda/nash/tests/internal/sh" 10 | ) 11 | 12 | type TestCase struct { 13 | Name string 14 | ScriptCode string 15 | ExpectStdout string 16 | ExpectStderrToContain string 17 | Fails bool 18 | } 19 | 20 | func Run(t *testing.T, nashcmd string, cases ...TestCase) { 21 | for _, tcase := range cases { 22 | t.Run(tcase.Name, func(t *testing.T) { 23 | stdout, stderr, err := sh.Exec(t, nashcmd, tcase.ScriptCode) 24 | if !tcase.Fails { 25 | if err != nil { 26 | t.Fatalf( 27 | "error[%s] stdout[%s] stderr[%s]", 28 | err, 29 | stdout, 30 | stderr, 31 | ) 32 | } 33 | 34 | if stderr != "" { 35 | t.Fatalf( 36 | "unexpected stderr[%s], on success no stderr is expected", 37 | stderr, 38 | ) 39 | } 40 | } 41 | 42 | if tcase.ExpectStdout != "" { 43 | assert.EqualStrings(t, tcase.ExpectStdout, stdout) 44 | } 45 | 46 | if tcase.ExpectStderrToContain != "" { 47 | assert.ContainsString(t, stderr, tcase.ExpectStderrToContain) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/listindex_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/madlambda/nash/tests/internal/tester" 7 | ) 8 | 9 | func TestListIndexing(t *testing.T) { 10 | tester.Run(t, Nashcmd, 11 | tester.TestCase{ 12 | Name: "PositionalAccess", 13 | ScriptCode: ` 14 | var a = ("1" "2") 15 | echo $a[0] 16 | echo $a[1] 17 | `, 18 | ExpectStdout: "1\n2\n", 19 | }, 20 | tester.TestCase{ 21 | Name: "PositionalAssigment", 22 | ScriptCode: ` 23 | var a = ("1" "2") 24 | a[0] = "9" 25 | a[1] = "p" 26 | echo $a[0] + $a[1] 27 | `, 28 | ExpectStdout: "9p\n", 29 | }, 30 | tester.TestCase{ 31 | Name: "PositionalAccessWithVar", 32 | ScriptCode: ` 33 | var a = ("1" "2") 34 | var i = "0" 35 | echo $a[$i] 36 | i = "1" 37 | echo $a[$i] 38 | `, 39 | ExpectStdout: "1\n2\n", 40 | }, 41 | tester.TestCase{ 42 | Name: "Iteration", 43 | ScriptCode: ` 44 | var a = ("1" "2" "3") 45 | for x in $a { 46 | echo $x 47 | } 48 | `, 49 | ExpectStdout: "1\n2\n3\n", 50 | }, 51 | tester.TestCase{ 52 | Name: "IterateEmpty", 53 | ScriptCode: ` 54 | var a = () 55 | for x in $a { 56 | exit("1") 57 | } 58 | echo "ok" 59 | `, 60 | ExpectStdout: "ok\n", 61 | }, 62 | tester.TestCase{ 63 | Name: "IndexOutOfRangeFails", 64 | ScriptCode: ` 65 | var a = ("1" "2" "3") 66 | echo $a[3] 67 | `, 68 | Fails: true, 69 | ExpectStderrToContain: "IndexError", 70 | }, 71 | tester.TestCase{ 72 | Name: "IndexEmptyFails", 73 | ScriptCode: ` 74 | var a = () 75 | echo $a[0] 76 | `, 77 | Fails: true, 78 | ExpectStderrToContain: "IndexError", 79 | }, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /tests/stringindex_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/madlambda/nash/tests/internal/tester" 7 | ) 8 | 9 | func TestStringIndexing(t *testing.T) { 10 | tester.Run(t, Nashcmd, 11 | tester.TestCase{ 12 | Name: "IterateEmpty", 13 | ScriptCode: ` 14 | var a = "" 15 | for x in $a { 16 | exit("1") 17 | } 18 | echo "ok" 19 | `, 20 | ExpectStdout: "ok\n", 21 | }, 22 | tester.TestCase{ 23 | Name: "IndexEmptyFails", 24 | ScriptCode: ` 25 | var a = "" 26 | echo $a[0] 27 | `, 28 | Fails: true, 29 | ExpectStderrToContain: "IndexError", 30 | }, 31 | tester.TestCase{ 32 | Name: "IsImmutable", 33 | ScriptCode: ` 34 | var a = "12" 35 | a[0] = "2" 36 | echo $a 37 | `, 38 | Fails: true, 39 | ExpectStderrToContain: "IndexError", 40 | }, 41 | ) 42 | } 43 | func TestStringIndexingASCII(t *testing.T) { 44 | tester.Run(t, Nashcmd, 45 | tester.TestCase{Name: "PositionalAccess", 46 | ScriptCode: ` 47 | var a = "12" 48 | echo $a[0] 49 | echo $a[1] 50 | `, 51 | ExpectStdout: "1\n2\n", 52 | }, 53 | tester.TestCase{ 54 | Name: "PositionalAccessReturnsString", 55 | ScriptCode: ` 56 | var a = "12" 57 | var x = $a[0] + $a[1] 58 | echo $x 59 | `, 60 | ExpectStdout: "12\n", 61 | }, 62 | tester.TestCase{ 63 | Name: "Len", 64 | ScriptCode: ` 65 | var a = "12" 66 | var l <= len($a) 67 | echo $l 68 | `, 69 | ExpectStdout: "2\n", 70 | }, 71 | tester.TestCase{ 72 | Name: "Iterate", 73 | ScriptCode: ` 74 | var a = "123" 75 | for x in $a { 76 | echo $x 77 | } 78 | `, 79 | ExpectStdout: "1\n2\n3\n", 80 | }, 81 | tester.TestCase{ 82 | Name: "IndexOutOfRangeFails", 83 | ScriptCode: ` 84 | var a = "123" 85 | echo $a[3] 86 | `, 87 | Fails: true, 88 | ExpectStderrToContain: "IndexError", 89 | }, 90 | ) 91 | } 92 | 93 | func TestStringIndexingNonASCII(t *testing.T) { 94 | tester.Run(t, Nashcmd, 95 | tester.TestCase{Name: "PositionalAccess", 96 | ScriptCode: ` 97 | var a = "⌘⌘" 98 | echo $a[0] 99 | echo $a[1] 100 | `, 101 | ExpectStdout: "⌘\n⌘\n", 102 | }, 103 | tester.TestCase{ 104 | Name: "Iterate", 105 | ScriptCode: ` 106 | var a = "⌘⌘" 107 | for x in $a { 108 | echo $x 109 | } 110 | `, 111 | ExpectStdout: "⌘\n⌘\n", 112 | }, 113 | tester.TestCase{ 114 | Name: "PositionalAccessReturnsString", 115 | ScriptCode: ` 116 | var a = "⌘⌘" 117 | var x = $a[0] + $a[1] 118 | echo $x 119 | `, 120 | ExpectStdout: "⌘⌘\n", 121 | }, 122 | tester.TestCase{ 123 | Name: "Len", 124 | ScriptCode: ` 125 | var a = "⌘⌘" 126 | var l <= len($a) 127 | echo $l 128 | `, 129 | ExpectStdout: "2\n", 130 | }, 131 | tester.TestCase{ 132 | Name: "IndexOutOfRangeFails", 133 | ScriptCode: ` 134 | var a = "⌘⌘" 135 | echo $a[2] 136 | `, 137 | Fails: true, 138 | ExpectStderrToContain: "IndexError", 139 | }, 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import "strconv" 4 | 5 | type ( 6 | Token int 7 | 8 | FileInfo struct { 9 | line, column int 10 | } 11 | ) 12 | 13 | const ( 14 | Illegal Token = iota + 1 // error ocurred 15 | EOF 16 | Comment 17 | 18 | literal_beg 19 | 20 | Ident 21 | String // "" 22 | Number // [0-9]+ 23 | Arg 24 | 25 | literal_end 26 | 27 | operator_beg 28 | 29 | Assign // = 30 | AssignCmd // <= 31 | Equal // == 32 | NotEqual // != 33 | Plus // + 34 | Minus // - 35 | Gt // > 36 | Lt // < 37 | 38 | Colon // , 39 | Semicolon // ; 40 | 41 | operator_end 42 | 43 | LBrace // { 44 | RBrace // } 45 | LParen // ( 46 | RParen // ) 47 | LBrack // [ 48 | RBrack // ] 49 | Pipe 50 | 51 | Comma 52 | Dotdotdot 53 | 54 | Variable 55 | 56 | keyword_beg 57 | 58 | Import 59 | SetEnv 60 | ShowEnv 61 | BindFn // "bindfn 62 | Dump // "dump" [ file ] 63 | Return 64 | If 65 | Else 66 | For 67 | Rfork 68 | Fn 69 | Var 70 | 71 | keyword_end 72 | ) 73 | 74 | var tokens = [...]string{ 75 | Illegal: "ILLEGAL", 76 | EOF: "EOF", 77 | Comment: "COMMENT", 78 | 79 | Ident: "IDENT", 80 | String: "STRING", 81 | Number: "NUMBER", 82 | Arg: "ARG", 83 | 84 | Assign: "=", 85 | AssignCmd: "<=", 86 | Equal: "==", 87 | NotEqual: "!=", 88 | Plus: "+", 89 | Minus: "-", 90 | Gt: ">", 91 | Lt: "<", 92 | 93 | Colon: ",", 94 | Semicolon: ";", 95 | 96 | LBrace: "{", 97 | RBrace: "}", 98 | LParen: "(", 99 | RParen: ")", 100 | LBrack: "[", 101 | RBrack: "]", 102 | Pipe: "|", 103 | 104 | Comma: ",", 105 | Dotdotdot: "...", 106 | 107 | Variable: "VARIABLE", 108 | 109 | Import: "import", 110 | SetEnv: "setenv", 111 | ShowEnv: "showenv", 112 | BindFn: "bindfn", 113 | Dump: "dump", 114 | Return: "return", 115 | If: "if", 116 | Else: "else", 117 | For: "for", 118 | Rfork: "rfork", 119 | Fn: "fn", 120 | Var: "var", 121 | } 122 | 123 | var keywords map[string]Token 124 | 125 | func init() { 126 | keywords = make(map[string]Token) 127 | for i := keyword_beg + 1; i < keyword_end; i++ { 128 | keywords[tokens[i]] = i 129 | } 130 | } 131 | 132 | func Lookup(ident string) Token { 133 | if tok, isKeyword := keywords[ident]; isKeyword { 134 | return tok 135 | } 136 | 137 | return Ident 138 | } 139 | 140 | func IsKeyword(t Token) bool { 141 | if t > keyword_beg && t < keyword_end { 142 | return true 143 | } 144 | 145 | return false 146 | } 147 | 148 | func NewFileInfo(l, c int) FileInfo { return FileInfo{l, c} } 149 | func (info FileInfo) Line() int { return info.line } 150 | func (info FileInfo) Column() int { return info.column } 151 | 152 | func (tok Token) String() string { 153 | s := "" 154 | 155 | if 0 < tok && tok < Token(len(tokens)) { 156 | s = tokens[tok] 157 | } 158 | if s == "" { 159 | s = "token(" + strconv.Itoa(int(tok)) + ")" 160 | } 161 | return s 162 | } 163 | -------------------------------------------------------------------------------- /vendor/github.com/chzyer/logex/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /vendor/github.com/chzyer/logex/Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -v ./... 3 | 4 | install: 5 | go install ./... 6 | -------------------------------------------------------------------------------- /vendor/github.com/chzyer/logex/README.md: -------------------------------------------------------------------------------- 1 | Logex 2 | ======= 3 | [![Build Status](https://travis-ci.org/go-logex/logex.svg?branch=master)](https://travis-ci.org/go-logex/logex) 4 | [![GoDoc](https://godoc.org/gopkg.in/logex.v1?status.svg)](https://godoc.org/gopkg.in/logex.v1) 5 | [![Join the chat at https://gitter.im/go-logex/logex](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-logex/logex?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | An golang log lib, supports tracing and level, wrap by standard log lib 8 | 9 | How To Get 10 | ======= 11 | shell 12 | ``` 13 | go get gopkg.in/logex.v1 14 | ``` 15 | 16 | source code 17 | ```{go} 18 | import "gopkg.in/logex.v1" // package name is logex 19 | 20 | func main() { 21 | logex.Info("Hello!") 22 | } 23 | ``` 24 | 25 | Level 26 | ======= 27 | 28 | ```{go} 29 | import "gopkg.in/logex.v1" 30 | 31 | func main() { 32 | logex.Println("") 33 | logex.Debug("debug staff.") // Only show if has an "DEBUG" named env variable(whatever value). 34 | logex.Info("info") 35 | logex.Warn("") 36 | logex.Fatal("") // also trigger exec "os.Exit(1)" 37 | logex.Error(err) // print error 38 | logex.Struct(obj) // print objs follow such layout "%T(%+v)" 39 | logex.Pretty(obj) // print objs as JSON-style, more readable and hide non-publish properties, just JSON 40 | } 41 | ``` 42 | 43 | Extendability 44 | ====== 45 | 46 | source code 47 | ```{go} 48 | type MyStruct struct { 49 | BiteMe bool 50 | } 51 | ``` 52 | 53 | may change to 54 | 55 | ```{go} 56 | type MyStruct struct { 57 | BiteMe bool 58 | logex.Logger // just this 59 | } 60 | 61 | func main() { 62 | ms := new(MyStruct) 63 | ms.Info("woo!") 64 | } 65 | ``` 66 | 67 | Runtime Tracing 68 | ====== 69 | All log will attach theirs stack info. Stack Info will shown by an layout, `{packageName}.{FuncName}:{FileName}:{FileLine}` 70 | 71 | ```{go} 72 | package main 73 | 74 | import "gopkg.in/logex.v1" 75 | 76 | func test() { 77 | logex.Pretty("hello") 78 | } 79 | 80 | func main() { 81 | test() 82 | } 83 | ``` 84 | 85 | response 86 | ``` 87 | 2014/10/10 15:17:14 [main.test:testlog.go:6][PRETTY] "hello" 88 | ``` 89 | 90 | Error Tracing 91 | ====== 92 | You can trace an error if you want. 93 | 94 | ```{go} 95 | package main 96 | 97 | import ( 98 | "gopkg.in/logex.v1" 99 | "os" 100 | ) 101 | 102 | func openfile() (*os.File, error) { 103 | f, err := os.Open("xxx") 104 | if err != nil { 105 | err = logex.Trace(err) 106 | } 107 | return f, err 108 | } 109 | 110 | func test() error { 111 | f, err := openfile() 112 | if err != nil { 113 | return logex.Trace(err) 114 | } 115 | f.Close() 116 | return nil 117 | } 118 | 119 | func main() { 120 | err := test() 121 | if err != nil { 122 | logex.Error(err) 123 | return 124 | } 125 | logex.Info("test success") 126 | } 127 | ``` 128 | 129 | 130 | response 131 | ``` 132 | 2014/10/10 15:22:29 [main.main:testlog.go:28][ERROR] [main.openfile:11;main.test:19] open xxx: no such file or directory 133 | ``` 134 | -------------------------------------------------------------------------------- /vendor/github.com/chzyer/logex/err.go: -------------------------------------------------------------------------------- 1 | package logex 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "path" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func Define(info string) *traceError { 14 | return &traceError{ 15 | error: errors.New(info), 16 | } 17 | } 18 | 19 | func NewError(info ...interface{}) *traceError { 20 | return TraceEx(1, errors.New(fmt.Sprint(info...))) 21 | } 22 | 23 | func NewErrorf(format string, info ...interface{}) *traceError { 24 | return TraceEx(1, fmt.Errorf(format, info...)) 25 | } 26 | 27 | func EqualAny(e error, es []error) bool { 28 | for i := 0; i < len(es); i++ { 29 | if Equal(e, es[i]) { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | func Equal(e1, e2 error) bool { 37 | if e, ok := e1.(*traceError); ok { 38 | e1 = e.error 39 | } 40 | if e, ok := e2.(*traceError); ok { 41 | e2 = e.error 42 | } 43 | return e1 == e2 44 | } 45 | 46 | type traceError struct { 47 | error 48 | format []interface{} 49 | stack []string 50 | code *int 51 | } 52 | 53 | func (t *traceError) SetCode(code int) *traceError { 54 | if t.stack == nil { 55 | t = TraceEx(1, t) 56 | } 57 | t.code = &code 58 | return t 59 | } 60 | 61 | func (t *traceError) GetCode() int { 62 | if t.code == nil { 63 | return 500 64 | } 65 | return *t.code 66 | } 67 | 68 | func (t *traceError) Error() string { 69 | if t == nil { 70 | return "" 71 | } 72 | if t.format == nil { 73 | if t.error == nil { 74 | panic(t.stack) 75 | } 76 | return t.error.Error() 77 | } 78 | return fmt.Sprintf(t.error.Error(), t.format...) 79 | } 80 | 81 | func (t *traceError) Trace(info ...interface{}) *traceError { 82 | return TraceEx(1, t, info...) 83 | } 84 | 85 | func (t *traceError) Follow(err error) *traceError { 86 | if t == nil { 87 | return nil 88 | } 89 | if te, ok := err.(*traceError); ok { 90 | if len(te.stack) > 0 { 91 | te.stack[len(te.stack)-1] += ":" + err.Error() 92 | } 93 | t.stack = append(te.stack, t.stack...) 94 | } 95 | return t 96 | } 97 | 98 | func (t *traceError) Format(obj ...interface{}) *traceError { 99 | if t.stack == nil { 100 | t = TraceEx(1, t) 101 | } 102 | t.format = obj 103 | return t 104 | } 105 | 106 | func (t *traceError) StackError() string { 107 | if t == nil { 108 | return t.Error() 109 | } 110 | if len(t.stack) == 0 { 111 | return t.Error() 112 | } 113 | return fmt.Sprintf("[%s] %s", strings.Join(t.stack, ";"), t.Error()) 114 | } 115 | 116 | func Tracef(err error, obj ...interface{}) *traceError { 117 | e := TraceEx(1, err).Format(obj...) 118 | return e 119 | } 120 | 121 | // set runtime info to error 122 | func TraceError(err error, info ...interface{}) *traceError { 123 | return TraceEx(1, err, info...) 124 | } 125 | 126 | func Trace(err error, info ...interface{}) error { 127 | if err == nil { 128 | return nil 129 | } 130 | return TraceEx(1, err, info...) 131 | } 132 | 133 | func joinInterface(info []interface{}, ch string) string { 134 | ret := bytes.NewBuffer(make([]byte, 0, 512)) 135 | for idx, o := range info { 136 | if idx > 0 { 137 | ret.WriteString(ch) 138 | } 139 | ret.WriteString(fmt.Sprint(o)) 140 | } 141 | return ret.String() 142 | } 143 | 144 | func TraceEx(depth int, err error, info ...interface{}) *traceError { 145 | if err == nil { 146 | return nil 147 | } 148 | pc, _, line, _ := runtime.Caller(1 + depth) 149 | name := runtime.FuncForPC(pc).Name() 150 | name = path.Base(name) 151 | stack := name + ":" + strconv.Itoa(line) 152 | if len(info) > 0 { 153 | stack += "(" + joinInterface(info, ",") + ")" 154 | } 155 | if te, ok := err.(*traceError); ok { 156 | if te.stack == nil { // define 157 | return &traceError{ 158 | error: te.error, 159 | stack: []string{stack}, 160 | } 161 | } 162 | te.stack = append(te.stack, stack) 163 | return te 164 | } 165 | return &traceError{err, nil, []string{stack}, nil} 166 | } 167 | -------------------------------------------------------------------------------- /vendor/github.com/chzyer/test/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 chzyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/chzyer/test/README.md: -------------------------------------------------------------------------------- 1 | # test -------------------------------------------------------------------------------- /vendor/github.com/chzyer/test/disk.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/hex" 5 | "io" 6 | ) 7 | 8 | type MemDisk struct { 9 | data [][]byte 10 | size int64 11 | woff, roff int 12 | } 13 | 14 | func NewMemDisk() *MemDisk { 15 | return &MemDisk{} 16 | } 17 | 18 | func (w *MemDisk) Write(b []byte) (int, error) { 19 | n, err := w.WriteAt(b, int64(w.woff)) 20 | w.woff += n 21 | return n, err 22 | } 23 | 24 | func (w *MemDisk) getData(off int64) []byte { 25 | idx := int(off >> 20) 26 | if idx >= cap(w.data) { 27 | newdata := make([][]byte, idx+1) 28 | copy(newdata, w.data) 29 | w.data = newdata 30 | } 31 | if len(w.data[idx]) == 0 { 32 | w.data[idx] = make([]byte, 1<<20) 33 | } 34 | 35 | return w.data[idx][off&((1<<20)-1):] 36 | } 37 | 38 | func (w *MemDisk) WriteAt(b []byte, off int64) (int, error) { 39 | n := len(b) 40 | for len(b) > 0 { 41 | buf := w.getData(off) 42 | m := copy(buf, b) 43 | if off+int64(m) > w.size { 44 | w.size = off + int64(m) 45 | } 46 | b = b[m:] 47 | off += int64(m) 48 | } 49 | return n, nil 50 | } 51 | 52 | func (w *MemDisk) ReadAt(b []byte, off int64) (int, error) { 53 | byteRead := 0 54 | for byteRead < len(b) { 55 | if off >= w.size { 56 | return 0, io.EOF 57 | } 58 | buf := w.getData(off) 59 | if int64(len(buf))+off > w.size { 60 | buf = buf[:w.size-off] 61 | } 62 | if len(buf) == 0 { 63 | return byteRead, io.EOF 64 | } 65 | n := copy(b[byteRead:], buf) 66 | off += int64(n) 67 | byteRead += n 68 | } 69 | return byteRead, nil 70 | } 71 | 72 | func (w *MemDisk) Dump() string { 73 | return hex.Dump(w.getData(0)) 74 | } 75 | 76 | func (w *MemDisk) SeekRead(offset int64, whence int) (ret int64) { 77 | switch whence { 78 | case 0: 79 | w.roff += int(offset) 80 | case 1: 81 | w.roff = int(offset) 82 | default: 83 | } 84 | return int64(w.roff) 85 | } 86 | 87 | func (w *MemDisk) SeekWrite(offset int64, whence int) (ret int64) { 88 | switch whence { 89 | case 0: 90 | w.woff += int(offset) 91 | case 1: 92 | w.woff = int(offset) 93 | default: 94 | } 95 | return int64(w.woff) 96 | } 97 | 98 | func (w *MemDisk) Read(b []byte) (int, error) { 99 | n, err := w.ReadAt(b, int64(w.roff)) 100 | w.roff += n 101 | return n, err 102 | } 103 | 104 | func (w *MemDisk) Close() error { 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /vendor/golang.org/x/exp/AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /vendor/golang.org/x/exp/CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This source code was written by the Go contributors. 2 | # The master list of contributors is in the main Go distribution, 3 | # visible at http://tip.golang.org/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /vendor/golang.org/x/exp/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /vendor/golang.org/x/exp/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/chzyer/logex v1.1.10 2 | github.com/chzyer/logex 3 | # github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 4 | github.com/chzyer/test 5 | # golang.org/x/exp v0.0.0-20200513190911-00229845015e 6 | golang.org/x/exp/ebnf 7 | --------------------------------------------------------------------------------