Yaegi is Another Elegant Go Interpreter

Yaegi

release Build Status GoDoc Discourse status

Yaegi is Another Elegant Go Interpreter. It powers executable Go scripts and plugins, in embedded interpreters or interactive shells, on top of the Go runtime.

Features

  • Complete support of Go specification
  • Written in pure Go, using only the standard library
  • Simple interpreter API: New(), Eval(), Use()
  • Works everywhere Go works
  • All Go & runtime resources accessible from script (with control)
  • Security: unsafe and syscall packages neither used nor exported by default
  • Support Go 1.13 and Go 1.14 (the latest 2 major releases)

Install

Go package

import "github.com/traefik/yaegi/interp"

Command-line executable

go get -u github.com/traefik/yaegi/cmd/yaegi

Note that you can use rlwrap (install with your favorite package manager), and alias the yaegi command in alias yaegi='rlwrap yaegi' in your ~/.bashrc, to have history and command line edition.

CI Integration

curl -sfL https://raw.githubusercontent.com/traefik/yaegi/master/install.sh | bash -s -- -b $GOPATH/bin v0.9.0

Usage

As an embedded interpreter

Create an interpreter with New(), run Go code with Eval():

package main

import (
	"github.com/traefik/yaegi/interp"
	"github.com/traefik/yaegi/stdlib"
)

func main() {
	i := interp.New(interp.Options{})

	i.Use(stdlib.Symbols)

	_, err := i.Eval(`import "fmt"`)
	if err != nil {
		panic(err)
	}

	_, err = i.Eval(`fmt.Println("Hello Yaegi")`)
	if err != nil {
		panic(err)
	}
}

Go Playground

As a dynamic extension framework

The following program is compiled ahead of time, except bar() which is interpreted, with the following steps:

  1. use of i.Eval(src) to evaluate the script in the context of interpreter
  2. use of v, err := i.Eval("foo.Bar") to get the symbol from the interpreter context, as a reflect.Value
  3. application of Interface() method and type assertion to convert v into bar, as if it was compiled
package main

import "github.com/traefik/yaegi/interp"

const src = `package foo
func Bar(s string) string { return s + "-Foo" }`

func main() {
	i := interp.New(interp.Options{})

	_, err := i.Eval(src)
	if err != nil {
		panic(err)
	}

	v, err := i.Eval("foo.Bar")
	if err != nil {
		panic(err)
	}

	bar := v.Interface().(func(string) string)

	r := bar("Kung")
	println(r)
}

Go Playground

As a command-line interpreter

The Yaegi command can run an interactive Read-Eval-Print-Loop:

$ yaegi
> 1 + 2
3
> import "fmt"
> fmt.Println("Hello World")
Hello World
>

Note that in interactive mode, all stdlib package are pre-imported, you can use them directly:

$ yaegi
> reflect.TypeOf(time.Date)
: func(int, time.Month, int, int, int, int, int, *time.Location) time.Time
>

Or interpret Go packages, directories or files, including itself:

$ yaegi -syscall -unsafe -unrestricted github.com/traefik/yaegi/cmd/yaegi
>

Or for Go scripting in the shebang line:

$ cat /tmp/test
#!/usr/bin/env yaegi
package main

import "fmt"

func main() {
	fmt.Println("test")
}
$ ls -la /tmp/test
-rwxr-xr-x 1 dow184 dow184 93 Jan  6 13:38 /tmp/test
$ /tmp/test
test

Documentation

Documentation about Yaegi commands and libraries can be found at usual godoc.org.

Limitations

Beside the known bugs which are supposed to be fixed in the short term, there are some limitations not planned to be addressed soon:

  • Assembly files (.s) are not supported.
  • Calling C code is not supported (no virtual "C" package).
  • Interfaces to be used from the pre-compiled code can not be added dynamically, as it is required to pre-compile interface wrappers.
  • Representation of types by reflect and printing values using %T may give different results between compiled mode and interpreted mode.
  • Interpreting computation intensive code is likely to remain significantly slower than in compiled mode.

Contributing

Contributing guide.

License

Apache 2.0.

Comments
  • Discussion: Interactive debugger interface

    Discussion: Interactive debugger interface

    Proposal

    Discuss introducing a mechanism for interactively debugging Yaegi scripts. For example, some script.go, I want some interface parallel to interp.EvalPath("script.go") that provides a way to step through the Go statements in script.go.

    I am happy to develop the debugger myself, as part of this repo, or another traefik project, or a completely separate project. I would do this as a Debug Adapter Protocol implementation.

    Background

    I created the Go Notebook Kernel extension for Visual Studio Code. I would like to add the capability to debug Go notebook cells. Since the notebook kernel is driven by Yaegi, this would require interfaces for debugging Interpreter.Eval, so that I could implement a debug adapter for it. I have previously developed the VSCode Byebug Debugger, which relies on my implementation of the debug adapter protocol for Byebug.

    Workarounds

    N/A. Other than debugging Yaegi itself, there's no way to debug a Yaegi script.

  • cmd/yaegi: fails to import

    cmd/yaegi: fails to import "gonum.org/v1/gonum/mat"

    Importing "gonum.org/v1/gonum/mat" fails.

    ~ $ yaegi
    > import "gonum.org/v1/gonum/mat"
    .../src/gonum.org/v1/gonum/internal/math32/math.go:19:21: missing support for type uint: 3
    

    The relevant line in math.go:

    	mask    = 0x7f8 >> 3
    
  • use built in golang tooling to resolve imports

    use built in golang tooling to resolve imports

    fixes #422 but using the golang toolings builtin path resolution instead of the logic in pkgDir. could use a once over in a old style gopath environment not 100% sure on using "." for the srcDir within the yaegi context.

    when yaegi interpreter is running in a go module environment pkgDir cannot locate packages modules because they are not located anywhere in the gopath.

  • interp: produce meaningful output for file statements

    interp: produce meaningful output for file statements

    This MR modifies interp.eval to produce meaningful result values for file statements. Currently, the return value of interp.eval for file-like input is effectively new(interface{}) (as a reflect.Value). With these changes, interp.eval for file-like input will return meaningful values.

    • At the top level, a file-like input will produce a struct with an array of statement results
    • Import specs will produce a struct with the import path, and name if specified
    • Top-level function declarations will produce a struct with the function name
    • Top-level type declarations will produce a struct with the type name

    Variable and constant declarations do not currently produce any result.

    Motivation

    I am working on a notebook extension for Visual Studio Code, powered by Yaegi. This MR is a step towards enabling me to improve the user experience. Specifically, I want to be able to intelligently detect whether the user expects the evaluation result to be displayed. If the input is file-like, I will assume that the user does not wish to inspect/display some value. For my purposes, it doesn't matter exactly what result file-like inputs produce (as long as I can detect it), so I chose to implement something simple that could be extended in the future.

  • `mismatched types gioui.org/ui.pc and .` when importing Gio package

    `mismatched types gioui.org/ui.pc and .` when importing Gio package

    Possibly related to #90 or #308.

    I am trying to run the Gio "hello, world" program from within Yaegi. (Toplevel here.) Yaegi throws an error on this line:

    func (r *OpsReader) Decode() (EncodedOp, bool) {
    	if r.ops == nil {
    		return EncodedOp{}, false
    	}
    	for {
    		if len(r.stack) > 0 {
    			b := r.stack[len(r.stack)-1]
    			if r.pc == b.endPC {
    			// ^^^ error here
    			// /path/to/gioui.org/ui/ops.go:265:7: mismatched types gioui.org/ui.pc and .
    

    As indicated, the error is mismatched types gioui.org/ui.pc and . .

    So first of all, the error message appears to be truncated. And second of all, r.pc and b.endPC are the same type.

    If I try to import the package multiple times, it fails twice, then succeeds, and then panics:

    % rlwrap yaegi
    > import    "gioui.org/ui"
    /Users/lmc/src/gioui.org/ui/ops.go:265:7: mismatched types gioui.org/ui.pc and .
    > import    "gioui.org/ui"
    /Users/lmc/src/gioui.org/ui/ops.go:265:7: mismatched types gioui.org/ui.pc and .
    > import    "gioui.org/ui"
    0xc0000a14e0
    > import    "gioui.org/ui"
    /Users/lmc/src/gioui.org/ui/internal/ops/ops.go:6:7: panic
    panic: reflect.Set: value of type int is not assignable to type uint8 [recovered]
    	panic: reflect.Set: value of type int is not assignable to type uint8
    
  • Review current test cases

    Review current test cases

    https://github.com/containous/dyngo/commit/a316f40ca7b6da6268f1c3c76b4e13097f28f6bc

    • [ ] chan6.go ?
    Got: "123 false\n"
    want: "123 true\n"
    
    • [ ] select1.go ?
    Got:  "start for\nreceived one\nfinish 1\nend for\nstart for\nreceived #2  false\nend for\nBye\n",
    want: "start for\nreceived one\nfinish 1\nend for\nstart for\nreceived #2 two true\nend for\nBye\n"
    
    • [ ] ret1.go
    Got: "<nil>\n"
    want: "5\n"
    
    • [x] run0.go
    # command-line-arguments
    ../_test/run11.go:4:11: multiple-value f() in single-value context
        
    exit status 2
    
    • [x] run11.go
    # command-line-arguments
    ../_test/run11.go:4:11: multiple-value f() in single-value context
        
    exit status 2
    
    • [ ] time3.go
    Got: "20\n"
    want: "20 24 43\n"
    
    • [ ] type5.go
    Got: "int\n"
    want: "main.T\n"
    
    • [ ] type6.go
    Got: "int\n"
    want: "main.T\n"
    
  • PkgPath contains vendor path prefix

    PkgPath contains vendor path prefix

    PkgPath can contain the vendor path prefix when using vendored dependencies in a traefik plugin.

    equals then tries to compare an id with the prefix with one without it:

    "*github.com/rsteube/traefik-plugin-brotli/vendor/github.com/andybalholm/brotli.encoderDictionary" == "`*github.com/andybalholm/brotli.encoderDictionary"
    

    The following triggers an unexpected error:

    # hash.go
    type hasherCommon struct {
    	params           hasherParams
    	is_prepared_     bool
    	dict_num_lookups uint
    	dict_num_matches uint
    }
    
    type hasherHandle interface {
    	Common() *hasherCommon
    	Initialize(params *encoderParams)
    	Prepare(one_shot bool, input_size uint, data []byte)
    	StitchToPreviousBlock(num_bytes uint, position uint, ringbuffer []byte, ringbuffer_mask uint)
    	HashTypeLength() uint
    	StoreLookahead() uint
    	PrepareDistanceCache(distance_cache []int)
    	FindLongestMatch(dictionary *encoderDictionary, data []byte, ring_buffer_mask uint, distance_cache []int, cur_ix uint, max_length uint, max_backward uint, gap uint, max_distance uint, out *hasherSearchResult)
    	StoreRange(data []byte, mask uint, ix_start uint, ix_end uint)
    	Store(data []byte, mask uint, ix uint)
    }
    
    # h10.go
    type h10 struct {
    	hasherCommon
    	window_mask_ uint
    	buckets_     [1 << 17]uint32
    	invalid_pos_ uint32
    	forest       []uint32
    }
    
    func (*h10) FindLongestMatch(dictionary *encoderDictionary, data []byte, ring_buffer_mask uint, distance_cache []int, cur_ix uint, max_length uint, max_backward uint, gap uint, max_distance uint, out *hasherSearchResult) {
    	panic("unimplemented")
    }
    
    
    # encode.go -  where the error occurs
    createZopfliBackwardReferences(uint(bytes), uint(wrapped_last_processed_pos), data, uint(mask), &s.params, s.hasher_.(*h10), s.dist_cache_[:], &s.last_insert_len_, &s.commands, &s.num_literals_)
    
    

    Expected result:

    executes without error
    

    Got:

    /plugins/go/src/github.com/rsteube/traefik-plugin-brotli/vendor/github.com/andybalholm/brotli/encode.go:812:110:
    impossible type assertion: *github.com/andybalholm/brotli.h10
    does not implement github.com/rsteube/traefik-plugin-brotli/vendor/github.com/andybalholm/brotli.hasherHandle
    

    To fix this the prefix might be removed from PkgPath as others have done.

    see also https://github.com/golang/go/issues/12739

  • Failing tests on Arch Linux with yaegi 0.7.6-1

    Failing tests on Arch Linux with yaegi 0.7.6-1

    Hi, I am the yaegi package maintainer for Arch Linux. When I do go test ./... I get failing checks for yaegi 0.7.6-1 on Arch Linux in a systemd-nspawn container:

    ?   	github.com/containous/yaegi	[no test files]
    ?   	github.com/containous/yaegi/cmd/goexports	[no test files]
    --- FAIL: TestYaegiCmdCancel (9.91s)
        yaegi_test.go:67: failed to probe race: write |1: broken pipe
        yaegi_test.go:82: error running yaegi command for "for {}\n": exit status 2
        yaegi_test.go:67: failed to probe race: write |1: broken pipe
        yaegi_test.go:82: error running yaegi command for "select {}\n": exit status 2
    FAIL
    FAIL	github.com/containous/yaegi/cmd/yaegi	9.925s
    ok  	github.com/containous/yaegi/example/closure	0.014s
    ok  	github.com/containous/yaegi/example/getfunc	0.016s
    ok  	github.com/containous/yaegi/example/pkg	0.024s
    ?   	github.com/containous/yaegi/internal/genop	[no test files]
    --- FAIL: TestInterpConsistencyBuild (187.04s)
        --- FAIL: TestInterpConsistencyBuild/composite6.go (0.00s)
            interp_consistent_test.go:92: ../_test/composite6.go:6:2: import "github.com/containous/yaegi/_test/ct1" error: unable to find source related to: "github.com/containous/yaegi/_test/ct1"
        --- FAIL: TestInterpConsistencyBuild/import3.go (0.00s)
            interp_consistent_test.go:92: ../_test/import3.go:3:8: import "github.com/containous/yaegi/_test/foo" error: unable to find source related to: "github.com/containous/yaegi/_test/foo"
        --- FAIL: TestInterpConsistencyBuild/import4.go (0.00s)
            interp_consistent_test.go:92: ../_test/import4.go:3:8: import "github.com/containous/yaegi/_test/p1" error: unable to find source related to: "github.com/containous/yaegi/_test/p1"
        --- FAIL: TestInterpConsistencyBuild/import5.go (0.00s)
            interp_consistent_test.go:92: ../_test/import5.go:3:8: import "github.com/containous/yaegi/_test/foo" error: unable to find source related to: "github.com/containous/yaegi/_test/foo"
        --- FAIL: TestInterpConsistencyBuild/import7.go (0.00s)
            interp_consistent_test.go:92: ../_test/import7.go:3:8: import "github.com/containous/yaegi/_test/foo-bar" error: unable to find source related to: "github.com/containous/yaegi/_test/foo-bar"
        --- FAIL: TestInterpConsistencyBuild/import8.go (0.00s)
            interp_consistent_test.go:92: ../_test/import8.go:3:8: import "github.com/containous/yaegi/_test/b1/foo" error: unable to find source related to: "github.com/containous/yaegi/_test/b1/foo"
        --- FAIL: TestInterpConsistencyBuild/tag0.go (0.00s)
            interp_consistent_test.go:92: ../_test/tag0.go:6:8: import "github.com/containous/yaegi/_test/ct" error: unable to find source related to: "github.com/containous/yaegi/_test/ct"
    --- FAIL: TestInterpErrorConsistency (0.62s)
        --- FAIL: TestInterpErrorConsistency/import6.go (0.03s)
            interp_consistent_test.go:210: got "../_test/import6.go:3:8: import \"github.com/containous/yaegi/_test/c1\" error: unable to find source related to: \"github.com/containous/yaegi/_test/c1\"", want: "import cycle not allowed"
    Hello
    In Hi:
    hello from Myint 4
    --- FAIL: TestFile (2.16s)
        --- FAIL: TestFile/composite6.go (0.00s)
            interp_file_test.go:72: ../_test/composite6.go:6:2: import "github.com/containous/yaegi/_test/ct1" error: unable to find source related to: "github.com/containous/yaegi/_test/ct1"
        --- FAIL: TestFile/import3.go (0.00s)
            interp_file_test.go:72: ../_test/import3.go:3:8: import "github.com/containous/yaegi/_test/foo" error: unable to find source related to: "github.com/containous/yaegi/_test/foo"
        --- FAIL: TestFile/import4.go (0.00s)
            interp_file_test.go:72: ../_test/import4.go:3:8: import "github.com/containous/yaegi/_test/p1" error: unable to find source related to: "github.com/containous/yaegi/_test/p1"
        --- FAIL: TestFile/import5.go (0.00s)
            interp_file_test.go:72: ../_test/import5.go:3:8: import "github.com/containous/yaegi/_test/foo" error: unable to find source related to: "github.com/containous/yaegi/_test/foo"
        --- FAIL: TestFile/import6.go (0.00s)
            interp_file_test.go:66: got "../_test/import6.go:3:8: import \"github.com/containous/yaegi/_test/c1\" error: unable to find source related to: \"github.com/containous/yaegi/_test/c1\"", want: "import cycle not allowed\n\timports github.com/containous/yaegi/_test/c1"
        --- FAIL: TestFile/import7.go (0.00s)
            interp_file_test.go:72: ../_test/import7.go:3:8: import "github.com/containous/yaegi/_test/foo-bar" error: unable to find source related to: "github.com/containous/yaegi/_test/foo-bar"
        --- FAIL: TestFile/import8.go (0.00s)
            interp_file_test.go:72: ../_test/import8.go:3:8: import "github.com/containous/yaegi/_test/b1/foo" error: unable to find source related to: "github.com/containous/yaegi/_test/b1/foo"
        --- FAIL: TestFile/tag0.go (0.00s)
            interp_file_test.go:72: ../_test/tag0.go:6:8: import "github.com/containous/yaegi/_test/ct" error: unable to find source related to: "github.com/containous/yaegi/_test/ct"
    FAIL
    FAIL	github.com/containous/yaegi/interp	190.556s
    ?   	github.com/containous/yaegi/stdlib	[no test files]
    ?   	github.com/containous/yaegi/stdlib/syscall	[no test files]
    ?   	github.com/containous/yaegi/stdlib/unsafe	[no test files]
    FAIL
    

    When I build the binary without the tests, everything works as expected. But it would be nice if we could get this tests working.

  • panic when import `golang.org/x/text`

    panic when import `golang.org/x/text`

    The following program sample.go triggers a panic:

    package main
    
    import (
    	"golang.org/x/text/language"
    	"log"
    )
    
    func main() {
    	log.Println(language.English)
    }
    
    

    Expected result:

    $ go run ./sample.go
    
    2021/06/19 21:42:46 en
    

    Got: panic run: language: tag is not well-formed Links to panic line number

    $ yaegi ./sample.go
    
    
    /Users/wlun/go/src/golang.org/x/text/internal/language/tags.go:12:3: panic
    /Users/wlun/go/src/golang.org/x/text/internal/language/compact/compact.go:59:20: panic
    run: language: tag is not well-formed
    goroutine 1 [running]:
    runtime/debug.Stack(0x1, 0xc0013c6800, 0x40)
    	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/debug/stack.go:24 +0x9f
    github.com/traefik/yaegi/interp.(*Interpreter).eval.func1(0xc00223fc18)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:503 +0xc8
    panic(0x1b3a800, 0xc001a3dc20)
    	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/panic.go:965 +0x1b9
    github.com/traefik/yaegi/interp.runCfg.func1(0xc001bccfd0, 0xc002052100, 0xc00223de68)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:185 +0x253
    panic(0x1b3a800, 0xc001a3dc20)
    	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/panic.go:965 +0x1b9
    github.com/traefik/yaegi/interp.runCfg.func1(0xc001bcd130, 0xc001295900, 0xc00223dc58)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:185 +0x253
    panic(0x1b3a800, 0xc001a3dc20)
    	/usr/local/Cellar/go/1.16.3/libexec/src/runtime/panic.go:965 +0x1b9
    github.com/traefik/yaegi/interp._panic.func1(0xc001bcd130, 0xc0018fa550)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:838 +0x7f
    github.com/traefik/yaegi/interp.runCfg(0xc001295900, 0xc001bcd130)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:191 +0x87
    github.com/traefik/yaegi/interp.call.func7(0xc001bccfd0, 0xc0015b16c0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:1272 +0xd05
    github.com/traefik/yaegi/interp.runCfg(0xc002052100, 0xc001bccfd0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:191 +0x87
    github.com/traefik/yaegi/interp.(*Interpreter).run(0xc000366000, 0xc001ea5800, 0xc000368000)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/run.go:122 +0x2b0
    github.com/traefik/yaegi/interp.(*Interpreter).importSrc(0xc000366000, 0x0, 0x0, 0xc001d0ac91, 0x2b, 0x1078e01, 0xc0004a6158, 0x8, 0x0, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/src.go:156 +0xad9
    github.com/traefik/yaegi/interp.(*Interpreter).gta.func1(0xc0014ffd00, 0xc00223ec48)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/gta.go:222 +0x12e5
    github.com/traefik/yaegi/interp.(*node).Walk(0xc0014ffd00, 0xc00223ec48, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:232 +0xb5
    github.com/traefik/yaegi/interp.(*node).Walk(0xc0014fef00, 0xc00223ec48, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:236 +0x66
    github.com/traefik/yaegi/interp.(*node).Walk(0xc0014fe800, 0xc00223ec48, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:236 +0x66
    github.com/traefik/yaegi/interp.(*Interpreter).gta(0xc000366000, 0xc0014fe800, 0xc00011e580, 0x1a, 0xc000488041, 0x1a, 0xc001bbfc28, 0x8, 0xc0014fe800, 0x0, ...)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/gta.go:20 +0x21f
    github.com/traefik/yaegi/interp.(*Interpreter).importSrc(0xc000366000, 0x0, 0x0, 0xc000488041, 0x1a, 0x1, 0x16, 0xc000211300, 0xca, 0xc000212510)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/src.go:100 +0xf57
    github.com/traefik/yaegi/interp.(*Interpreter).gta.func1(0xc000210400, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/gta.go:222 +0x12e5
    github.com/traefik/yaegi/interp.(*node).Walk(0xc000210400, 0xc00223f9b8, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:232 +0xb5
    github.com/traefik/yaegi/interp.(*node).Walk(0xc000210300, 0xc00223f9b8, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:236 +0x66
    github.com/traefik/yaegi/interp.(*node).Walk(0xc000210100, 0xc00223f9b8, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:236 +0x66
    github.com/traefik/yaegi/interp.(*Interpreter).gta(0xc000366000, 0xc000210100, 0xc00020d9d0, 0x4, 0xc00020d9d0, 0x4, 0x10, 0xc000200240, 0x0, 0xc0002063e0, ...)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/gta.go:20 +0x21f
    github.com/traefik/yaegi/interp.(*Interpreter).gtaRetry(0xc000366000, 0xc00223fb88, 0x1, 0x1, 0xc00020d9d0, 0x4, 0xc00020d9d0, 0x4)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/gta.go:311 +0x12c
    github.com/traefik/yaegi/interp.(*Interpreter).eval(0xc000366000, 0xc0002c6310, 0x6e, 0x7ffeefbffb2e, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:525 +0x2a5
    github.com/traefik/yaegi/interp.(*Interpreter).EvalPath(0xc000366000, 0x7ffeefbffb2e, 0x9, 0xc0002c6200, 0x6e, 0x0, 0x0, 0xc0001b1cb0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/interp/interp.go:417 +0x105
    main.runFile(0xc000366000, 0x7ffeefbffb2e, 0x9, 0x0, 0x1, 0x0)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/cmd/yaegi/run.go:139 +0xb1
    main.run(0xc000032050, 0x1, 0x1, 0xc00000a501, 0xc000001680)
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/cmd/yaegi/run.go:102 +0x99d
    main.main()
    	/Users/wlun/go/pkg/mod/github.com/traefik/[email protected]/cmd/yaegi/yaegi.go:144 +0x415
    
  • Way to check if function is accesible from yaegi?

    Way to check if function is accesible from yaegi?

    Hello, thanks for the nice project. Because the script that yaegi evaluates could be dynamic, how can we check if a function exists before using it in yaegi?

  • Entering 1=1 triggers a panic

    Entering 1=1 triggers a panic

    OS: macOS 10.14.4 Commit: 2dfede3b90b40723cb2c015adb2f2ab5227825cd

    Using the repl:

    ~> yaegi
    > 1=1    
    1:28: panic
    panic: reflect: reflect.Value.Set using unaddressable value [recovered]
    	panic: reflect: reflect.Value.Set using unaddressable value
    
    goroutine 1 [running]:
    github.com/containous/yaegi/interp.runCfg.func1(0xc00018a050, 0xc000112a00)
    	/Users/max/go/src/github.com/containous/yaegi/interp/run.go:107 +0x1e9
    panic(0x17fc540, 0xc000180250)
    	/usr/local/Cellar/go/1.12.1/libexec/src/runtime/panic.go:522 +0x1b5
    reflect.flag.mustBeAssignable(0x82)
    	/usr/local/Cellar/go/1.12.1/libexec/src/reflect/value.go:234 +0x13a
    reflect.Value.Set(0x17fb400, 0xc0000ee650, 0x82, 0x17fb400, 0xc0000ee660, 0x82)
    	/usr/local/Cellar/go/1.12.1/libexec/src/reflect/value.go:1467 +0x2f
    github.com/containous/yaegi/interp.assign.func3(0xc00018a050, 0x1945db0)
    	/Users/max/go/src/github.com/containous/yaegi/interp/run.go:256 +0xb6
    github.com/containous/yaegi/interp.runCfg(0xc000112a00, 0xc00018a050)
    	/Users/max/go/src/github.com/containous/yaegi/interp/run.go:112 +0x70
    github.com/containous/yaegi/interp.(*Interpreter).run(0xc0000ce180, 0xc000112900, 0x0)
    	/Users/max/go/src/github.com/containous/yaegi/interp/run.go:93 +0x126
    github.com/containous/yaegi/interp.(*Interpreter).Eval(0xc0000ce180, 0xc0000ee5f0, 0x6, 0xc0001c9df0, 0x5, 0x19136b5, 0x1, 0xc0000ee5f0)
    	/Users/max/go/src/github.com/containous/yaegi/interp/interp.go:277 +0x2f9
    github.com/containous/yaegi/interp.(*Interpreter).Repl(0xc0000ce180, 0xc000010010, 0xc0000d0000)
    	/Users/max/go/src/github.com/containous/yaegi/interp/interp.go:320 +0x1c3
    main.main()
    	/Users/max/go/src/github.com/containous/yaegi/cmd/yaegi/yaegi.go:85 +0x4e2
    
  • Equality is incorrect when `nil` is used as the left argument of `==`

    Equality is incorrect when `nil` is used as the left argument of `==`

    hi!

    this issue is sorta blocking for me so i thought i would try to fix it.

    im still learning the codebase and understanding how yaegi works, but I thought I would attempt to add a test in the style of other tests as a start.

    please let me know if there is anything i need to change / run, or if anyone knows perhaps a good place to start for tackling this issue

    Fixes #1496

  • (reflect?) issue with github.com/bradfitz/gomemcache/memcache

    (reflect?) issue with github.com/bradfitz/gomemcache/memcache

    The following program sample.go triggers an unexpected result

    package main
    
    import (
    	"log"
    
    	"github.com/bradfitz/gomemcache/memcache"
    )
    
    func main() {
    	mc := memcache.New("localhost:11211", "localhost:11212")
    	mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})
    
    	it, err := mc.Get("foo")
    	if err != nil {
    		log.Fatal(err)
    	}
    	println(it.Key)
    	for _, v := range it.Value {
    		println(string(v))
    	}
    
    }
    

    Expected result

    $ docker run -p 11211:11211 --name my-memcache2 -d memcached -p 11211
    178dd9c73df173a19616bcf3d65538f462fe71d83d03f5c3cc40cd17376f0fbe
    $ docker run -p 11212:11212 --name my-memcache -d memcached -p 11212
    1dd4a193c0ec32c0ef31b04601988ee8ca3d2529806380a69dc025b840a58686
    $
    $ GO111MODULE=off go run ./main.go 
    foo
    m
    y
     
    v
    a
    l
    u
    e
    

    Got

    % yaegi run ./main.go 
    /Users/mpl/src/github.com/bradfitz/gomemcache/memcache/memcache.go:542:9: panic
    ./main.go:10:8: panic
    run: reflect: Call using func(*bufio.ReadWriter, *struct { Key string; Value []uint8; Flags uint32; Expiration int32; Xcasid uint64 }) error as type func(*struct { Timeout time.Duration; MaxIdleConns int; Xselector interp.valueInterface; Xlk sync.Mutex; Xfreeconn map[string][]*struct { Xnc net.Conn; Xrw *bufio.ReadWriter; Xaddr net.Addr; Xc *unsafe2.dummy } }, *bufio.ReadWriter, *struct { Key string; Value []uint8; Flags uint32; Expiration int32; Xcasid uint64 }) error
    goroutine 1 [running]:
    runtime/debug.Stack()
    	/Users/mpl/go1/src/runtime/debug/stack.go:24 +0x65
    github.com/traefik/yaegi/interp.(*Interpreter).Execute.func1()
    	/Users/mpl/src/github.com/traefik/yaegi/interp/program.go:146 +0x94
    panic({0x1978e40, 0xc0004a6270})
    	/Users/mpl/go1/src/runtime/panic.go:884 +0x212
    github.com/traefik/yaegi/interp.runCfg.func1()
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:192 +0x148
    panic({0x1978e40, 0xc0004a6270})
    	/Users/mpl/go1/src/runtime/panic.go:884 +0x212
    github.com/traefik/yaegi/interp.runCfg.func1()
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:192 +0x148
    panic({0x1978e40, 0xc0004a6270})
    	/Users/mpl/go1/src/runtime/panic.go:884 +0x212
    reflect.Value.call({0xc000624de0?, 0xc0003c64b0?, 0xc00004a980?}, {0x1a9e70e, 0x4}, {0xc0003c64e0, 0x2, 0xc0000a0a20?})
    	/Users/mpl/go1/src/reflect/value.go:440 +0x1abf
    reflect.Value.Call({0xc000624de0?, 0xc0003c64b0?, 0x16?}, {0xc0003c64e0?, 0x1a6ef60?, 0xc0002cfad0?})
    	/Users/mpl/go1/src/reflect/value.go:368 +0xbc
    github.com/traefik/yaegi/interp.call.func9.2({0xc0003c64e0?, 0xc0003c64b0?, 0x2?})
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:1288 +0x3c
    github.com/traefik/yaegi/interp.call.func9(0xc0002cfad0)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:1303 +0x122f
    github.com/traefik/yaegi/interp.runCfg(0xc0000a0000, 0xc0002cfad0, 0x1?, 0xc000469ac0?)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:200 +0x29d
    github.com/traefik/yaegi/interp.genFunctionWrapper.func1.1({0xc000012960, 0x1, 0x1?})
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:1002 +0x4a5
    reflect.Value.call({0xc0007ec480?, 0xc0003c6330?, 0x1011c9f?}, {0x1a9e70e, 0x4}, {0xc000012948, 0x1, 0x10d4fde?})
    	/Users/mpl/go1/src/reflect/value.go:584 +0x8c5
    reflect.Value.Call({0xc0007ec480?, 0xc0003c6330?, 0x1055ff2?}, {0xc000012948?, 0x1a9c4c0?, 0x218d801?})
    	/Users/mpl/go1/src/reflect/value.go:368 +0xbc
    github.com/traefik/yaegi/interp.call.func9.2({0xc000012948?, 0xc0003c6330?, 0xc000012588?})
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:1288 +0x3c
    github.com/traefik/yaegi/interp.call.func9(0xc0002cf4a0)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:1303 +0x122f
    github.com/traefik/yaegi/interp.runCfg(0xc0002d7c20, 0xc0002cf4a0, 0xc0002d70e0?, 0x0?)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:200 +0x29d
    github.com/traefik/yaegi/interp.(*Interpreter).run(0xc0002cc000, 0xc0002d70e0, 0xc0002ce000?)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/run.go:119 +0x38e
    github.com/traefik/yaegi/interp.(*Interpreter).Execute(0xc0002cc000, 0xc0007f4d50)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/program.go:172 +0x24b
    github.com/traefik/yaegi/interp.(*Interpreter).eval(0xc0002cc000, {0xc000400300?, 0x16e?}, {0x7ff7bfeff5da?, 0xc000400000?}, 0x6e?)
    	/Users/mpl/src/github.com/traefik/yaegi/interp/interp.go:561 +0x5c
    github.com/traefik/yaegi/interp.(*Interpreter).EvalPath(0xc0002cc000, {0x7ff7bfeff5da, 0x9})
    	/Users/mpl/src/github.com/traefik/yaegi/interp/interp.go:510 +0xab
    main.runFile(0x7ff7bfeff5da?, {0x7ff7bfeff5da, 0x9}, 0x0)
    	/Users/mpl/src/github.com/traefik/yaegi/cmd/yaegi/run.go:153 +0xee
    main.run({0xc0001241a0?, 0x1, 0x1})
    	/Users/mpl/src/github.com/traefik/yaegi/cmd/yaegi/run.go:116 +0xbec
    main.main()
    	/Users/mpl/src/github.com/traefik/yaegi/cmd/yaegi/yaegi.go:133 +0xcf
    

    Yaegi Version

    eee72d1aae664bf6627ef955215d886dc7b105c0

    Additional Notes

    No response

  • go routines not working as expected

    go routines not working as expected

    The following program sample.go triggers an unexpected result

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"strconv"
    	"sync"
    )
    
    func main() {
    	hits := make([]string, 3)
    	for i, _ := range hits {
    		hits[i] = string("hit" + strconv.Itoa(i))
    	}
    
    	bytes, _ := json.Marshal(hits)
    	fmt.Println("hits: " + string(bytes))
    
    	var wg sync.WaitGroup
    	for _, hit := range hits {
    		wg.Add(1)
    		go func(hit string) {
    			fmt.Println("hit = " + hit)
    			defer wg.Done()
    		}(hit)
    	}
    	wg.Wait()
    }
    

    Expected result

    hits: ["hit0","hit1","hit2"]
    hit = hit2
    hit = hit0
    hit = hit1
    

    Got

    hits: ["hit0","hit1","hit2"]
    hit = hit2
    hit = hit2
    hit = hit2
    

    Yaegi Version

    0.14.3

    Additional Notes

    No response

  • double assignment + closure, in a for loop, reuses the same variable, instead of assigning to a new one.

    double assignment + closure, in a for loop, reuses the same variable, instead of assigning to a new one.

    The following program sample.go triggers an unexpected result

    package main
    
    import (
    	"bufio"
    	"flag"
    	"fmt"
    	"strconv"
    	"strings"
    )
    
    var debug = flag.Bool("debug", false, `debug mode`)
    
    type monkey struct {
    	items        []int
    	operation    func(int) int
    	test         func(int) bool
    	monkeyTrue   int
    	monkeyFalse  int
    	inspectCount int
    }
    
    func parseOperation(operation string) func(int) int {
    	parts := strings.Split(operation, " ")
    	self := false
    	if parts[1] == "old" {
    		self = true
    	}
    	operand, _ := strconv.Atoi(parts[1])
    	sign := string(operation[0])
    	if sign == "+" {
    		return func(i int) int {
    			if self {
    				return i + i
    			}
    			return i + operand
    		}
    	}
    	if sign == "*" {
    		return func(i int) int {
    			if self {
    				return i * i
    			}
    			return i * operand
    		}
    	}
    	panic(fmt.Sprintf("unsupported operation: %s", operation))
    }
    
    var input = `Monkey 0:
      Starting items: 79, 98
      Operation: new = old * 19
      Test: divisible by 23
        If true: throw to monkey 2
        If false: throw to monkey 3
    
    Monkey 1:
      Starting items: 54, 65, 75, 74
      Operation: new = old + 6
      Test: divisible by 19
        If true: throw to monkey 2
        If false: throw to monkey 0
    
    Monkey 2:
      Starting items: 79, 60, 97
      Operation: new = old * old
      Test: divisible by 13
        If true: throw to monkey 1
        If false: throw to monkey 3
    
    Monkey 3:
      Starting items: 74
      Operation: new = old + 3
      Test: divisible by 17
        If true: throw to monkey 0
        If false: throw to monkey 1
    `
    
    func main() {
    	flag.Parse()
    	/*
    		f, err := os.Open("./input.txt")
    		if err != nil {
    			log.Fatal(err)
    		}
    		defer f.Close()
    	*/
    
    	f := strings.NewReader(input)
    	sc := bufio.NewScanner(f)
    
    	var monkeys []*monkey
    
    	debugCount := 0
    	for sc.Scan() {
    		if *debug {
    			if debugCount > 34 {
    				break
    			}
    		}
    		debugCount++
    
    		l := sc.Text()
    		if !strings.HasPrefix(l, "Monkey") {
    			panic("BIM")
    		}
    		kong := monkey{}
    
    		sc.Scan()
    		l = sc.Text()
    		items := strings.Split(strings.TrimPrefix(l, "  Starting items: "), ", ")
    		for _, v := range items {
    			item, _ := strconv.Atoi(v)
    			kong.items = append(kong.items, item)
    		}
    
    		sc.Scan()
    		l = sc.Text()
    		operation := strings.TrimPrefix(l, "  Operation: new = old ")
    		kong.operation = parseOperation(operation)
    
    		sc.Scan()
    		l = sc.Text()
    		if !strings.HasPrefix(l, "  Test: divisible by ") {
    			panic(fmt.Sprintf("unsupported test: %s", l))
    		}
    		divisor, _ := strconv.Atoi(strings.TrimPrefix(l, "  Test: divisible by "))
    		if *debug {
    			println(len(monkeys), "DIVISOR: ", divisor)
    		}
    		// TODO: we should defer, and make it directly return the monkey destination
    		kong.test = func(i int) bool {
    			return i%divisor == 0
    		}
    
    		sc.Scan()
    		l = sc.Text()
    		kong.monkeyTrue, _ = strconv.Atoi(strings.TrimPrefix(l, "    If true: throw to monkey "))
    		if *debug {
    			println(len(monkeys), "MONKEYTRUE: ", kong.monkeyTrue)
    		}
    
    		sc.Scan()
    		l = sc.Text()
    		kong.monkeyFalse, _ = strconv.Atoi(strings.TrimPrefix(l, "    If false: throw to monkey "))
    		if *debug {
    			println(len(monkeys), "MONKEYFALSE: ", kong.monkeyFalse)
    		}
    
    		monkeys = append(monkeys, &kong)
    
    		sc.Scan()
    	}
    
    	for i := 0; i < 20; i++ {
    		for idx, mk := range monkeys {
    			if *debug {
    				println("MONKEY ", idx)
    			}
    			for k, item := range mk.items {
    				if *debug {
    					println(k, "OLD WORRY: ", item)
    				}
    				worry := mk.operation(item)
    				worry = worry / 3
    				// mk.items[k] = worry
    				if *debug {
    					println(k, "NEW WORRY: ", worry)
    				}
    
    				monkeyDest := 0
    				if mk.test(worry) {
    					monkeyDest = mk.monkeyTrue
    				} else {
    					monkeyDest = mk.monkeyFalse
    				}
    				if *debug {
    					println(k, worry, "THROWING TO MK ", monkeyDest)
    				}
    				monkeys[monkeyDest].items = append(monkeys[monkeyDest].items, worry)
    				mk.inspectCount++
    			}
    			mk.items = []int{}
    		}
    	}
    
    	for i, mk := range monkeys {
    		println("Monkey", i, "inspected items", mk.inspectCount, "times.")
    	}
    
    	// Answer: 54752
    }
    

    Expected result

    % go run ./main.go
    Monkey 0 inspected items 101 times.
    Monkey 1 inspected items 95 times.
    Monkey 2 inspected items 7 times.
    Monkey 3 inspected items 105 times.
    

    Got

    % yaegi run ./main.go 
    Monkey 0 inspected items 99 times.
    Monkey 1 inspected items 97 times.
    Monkey 2 inspected items 13 times.
    Monkey 3 inspected items 108 times.
    

    Yaegi Version

    eee72d1aae664bf6627ef955215d886dc7b105c0

    Additional Notes

    I was solving the 11th advent of code: https://adventofcode.com/2022/day/11 and I noticed the result given by yaegi is different from the one with the go compiler.

    I haven't had time to narrow it down yet, and to write a small repro. However, I strongly suspect the bug is connected to: monkeys[monkeyDest].items = append(monkeys[monkeyDest].items, worry) as the rest of the code is completely trivial.

    I will update this issue with a better repro and title later.

  • nil and slice equality is incorrect

    nil and slice equality is incorrect

    The following program test.go triggers an unexpected result

    package main
    
    import "fmt"
    
    func main() {
    	a := []byte{} == nil
    	b := nil == []byte{}
    
    	fmt.Printf("ans: %v\n", a)
    	fmt.Printf("ans: %v\n", b)
    }
    

    Expected result

    λ go run ./test.go                                                                                                                             
    ans: false
    ans: false
    

    Got

    λ yaegi ./test.go                                                                                                                              
    ans: false
    ans: true
    

    Yaegi Version

    eee72d1aae664bf6627ef955215d886dc7b105c0

    Additional Notes

    []byte{} == nil and nil == []byte{} should both be false

    however, yaegi thinks that nil == []byte{}

gpython is a python interpreter written in go "batteries not included"

gpython gpython is a part re-implementation / part port of the Python 3.4 interpreter to the Go language, "batteries not included". It includes: runti

Dec 28, 2022
wagon, a WebAssembly-based Go interpreter, for Go.

wagon wagon is a WebAssembly-based interpreter in Go, for Go. As of 2020/05/11 Wagon is in read-only mode, and looking for a maintainer. You may want

Dec 6, 2022
A BASIC interpreter written in golang.
A BASIC interpreter written in golang.

05 PRINT "Index" 10 PRINT "GOBASIC!" 20 PRINT "Limitations" Arrays Line Numbers IF Statement DATA / READ Statements Builtin Functions Types 30 PRINT "

Dec 24, 2022
Lisp Interpreter

golisp Lisp Interpreter Usage $ golisp < foo.lisp Installation $ go get github.com/mattn/golisp/cmd/golisp Features Call Go functions. Print random in

Dec 15, 2022
A simple interpreter

类型: 基础类型: 整形,浮点,字符串,布尔,空值(nil) 符合类型: 数组,只读数组(元组),字典 支持语句: if, elif, else, for, foreach, break, continue, return 支持类型定义: class, func 语法格式: 赋值语句---> ```

Aug 10, 2022
Scriptable interpreter written in golang
Scriptable interpreter written in golang

Anko Anko is a scriptable interpreter written in Go. (Picture licensed under CC BY-SA 3.0, photo by Ocdp) Usage Example - Embedded package main impor

Dec 23, 2022
Yaegi is Another Elegant Go Interpreter
Yaegi is Another Elegant Go Interpreter

Yaegi is Another Elegant Go Interpreter. It powers executable Go scripts and plugins, in embedded interpreters or interactive shells, on top of the Go

Jan 5, 2023
Interpreter - The Official Interpreter for the Infant Lang written in Go

Infant Lang Interpreter Infant Lang Minimalistic Less Esoteric Programming Langu

Jan 10, 2022
The interpreter for qiitan script. Yet another dialect of the Tengo language.

Qiitan は、Qiita ™️ の SNS である「Qiitadonβ」のマスコット・キャラクターです。 キーたん(Qiitan) @ Qiitadon Qiitan-go は Qiitan のファン・アプリであり、Qiita ™️ とは一切関係がありません。 Qiitan-goalpha キー

Feb 6, 2022
Amber is an elegant templating engine for Go Programming Language, inspired from HAML and Jade

amber Notice While Amber is perfectly fine and stable to use, I've been working on a direct Pug.js port for Go. It is somewhat hacky at the moment but

Jan 2, 2023
Elegant Scraper and Crawler Framework for Golang

Colly Lightning Fast and Elegant Scraping Framework for Gophers Colly provides a clean interface to write any kind of crawler/scraper/spider. With Col

Dec 30, 2022
Elegant generics for Go

genny - Generics for Go Install: go get github.com/cheekybits/genny ===== (pron. Jenny) by Mat Ryer (@matryer) and Tyler Bunnell (@TylerJBunnell). Un

Dec 23, 2022
🍐 Elegant Golang REST API Framework
🍐 Elegant Golang REST API Framework

An Elegant Golang Web Framework Goyave is a progressive and accessible web application framework focused on REST APIs, aimed at making backend develop

Jan 4, 2023
A modern, fast and scalable websocket framework with elegant API written in Go
A modern, fast and scalable websocket framework with elegant API written in Go

About neffos Neffos is a cross-platform real-time framework with expressive, elegant API written in Go. Neffos takes the pain out of development by ea

Jan 4, 2023
A modern, fast and scalable websocket framework with elegant API written in Go
A modern, fast and scalable websocket framework with elegant API written in Go

About neffos Neffos is a cross-platform real-time framework with expressive, elegant API written in Go. Neffos takes the pain out of development by ea

Dec 29, 2022
Elegant Scraper and Crawler Framework for Golang

Colly Lightning Fast and Elegant Scraping Framework for Gophers Colly provides a clean interface to write any kind of crawler/scraper/spider. With Col

Jan 9, 2023
🍐 Elegant Golang REST API Framework
🍐 Elegant Golang REST API Framework

An Elegant Golang Web Framework Goyave is a progressive and accessible web application framework focused on REST APIs, aimed at making backend develop

Jan 9, 2023
🍐 Elegant Golang Web Framework

Goyave Template A template project to get started with the Goyave framework. Getting Started Requirements Go 1.13+ Go modules Running the project Firs

Nov 22, 2022
📖 Elegant changelog generator

English | 中文简体 whatchanged An elegant changelog generator. Focus on Elegant/Simple/Efficient/Scalable Feel the magic online Feature: Cross-platform su

Nov 11, 2022
Chore is a elegant and simple tool for executing common tasks on remote servers.
Chore is a elegant and simple tool for executing common tasks on remote servers.

Chore is a tool for executing common tasks you run on your remote servers. You can easily setup tasks for deployment, commands, and more.

May 20, 2022