feature: command line provide sub-commands

The Yaegi command line has been changed to provide subcommands.

The following sub-commands are provided:
- extract (formerly goexports)
- help
- run
- test

The previous behaviour is now implemented in run command which
is the default, so the change should be transparent.

In run command, prepare the ability to run a package or a directory
in addition to a file. Not implemented yet

The test command is not implemented yet.

The extract command is meant to generate wrappers to non stdlib
packages.

Fixes #639
This commit is contained in:
Marc Vertes
2020-08-10 16:20:05 +02:00
committed by GitHub
parent bd4ce37baa
commit 2ac0c6f70b
4 changed files with 316 additions and 75 deletions

107
cmd/yaegi/extract.go Normal file
View File

@@ -0,0 +1,107 @@
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/containous/yaegi/extract"
)
func extractCmd(arg []string) error {
var licensePath string
var importPath string
eflag := flag.NewFlagSet("run", flag.ContinueOnError)
eflag.StringVar(&licensePath, "license", "", "path to a LICENSE file")
eflag.StringVar(&importPath, "import_path", "", "the namespace for the extracted symbols")
eflag.Usage = func() {
fmt.Println("Usage: yaegi extract [options] packages...")
fmt.Println("Options:")
eflag.PrintDefaults()
}
if err := eflag.Parse(arg); err != nil {
return err
}
args := eflag.Args()
if len(args) == 0 {
return fmt.Errorf("missing package")
}
license, err := genLicense(licensePath)
if err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
ext := extract.Extractor{
Dest: path.Base(wd),
License: license,
}
for _, pkgIdent := range args {
var buf bytes.Buffer
importPath, err := ext.Extract(pkgIdent, importPath, &buf)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
oFile := strings.Replace(importPath, "/", "_", -1) + ".go"
f, err := os.Create(oFile)
if err != nil {
return err
}
if _, err := io.Copy(f, &buf); err != nil {
_ = f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
}
return nil
}
// genLicense generates the correct LICENSE header text from the provided
// path to a LICENSE file.
func genLicense(fname string) (string, error) {
if fname == "" {
return "", nil
}
f, err := os.Open(fname)
if err != nil {
return "", fmt.Errorf("could not open LICENSE file: %v", err)
}
defer func() { _ = f.Close() }()
license := new(strings.Builder)
sc := bufio.NewScanner(f)
for sc.Scan() {
txt := sc.Text()
if txt != "" {
txt = " " + txt
}
license.WriteString("//" + txt + "\n")
}
if sc.Err() != nil {
return "", fmt.Errorf("could not scan LICENSE file: %v", err)
}
return license.String(), nil
}

43
cmd/yaegi/help.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import "fmt"
const usage = `Yaegi is a Go interpreter.
Usage:
yaegi [command] [arguments]
The commands are:
extract generate a wrapper file from a source package
help print usage information
run execute a Go program from source
test execute test functions in a Go package
Use "yaegi help <command>" for more information about a command.
If no command is given or if the first argument is not a command, then
the run command is assumed.
`
func help(arg []string) error {
var cmd string
if len(arg) > 0 {
cmd = arg[0]
}
switch cmd {
case Extract:
return extractCmd([]string{"-h"})
case Help, "", "-h", "--help":
fmt.Print(usage)
return nil
case Run:
return run([]string{"-h"})
case Test:
return fmt.Errorf("help: test not implemented")
default:
return fmt.Errorf("help: invalid yaegi command: %v", cmd)
}
}

132
cmd/yaegi/run.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"flag"
"fmt"
"go/build"
"io/ioutil"
"os"
"strings"
"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
"github.com/containous/yaegi/stdlib/syscall"
"github.com/containous/yaegi/stdlib/unrestricted"
"github.com/containous/yaegi/stdlib/unsafe"
)
func run(arg []string) error {
var interactive bool
var useSyscall bool
var useUnrestricted bool
var useUnsafe bool
var tags string
var cmd string
var err error
rflag := flag.NewFlagSet("run", flag.ContinueOnError)
rflag.BoolVar(&interactive, "i", false, "start an interactive REPL")
rflag.BoolVar(&useSyscall, "syscall", false, "include syscall symbols")
rflag.BoolVar(&useUnrestricted, "unrestricted", false, "include unrestricted symbols")
rflag.StringVar(&tags, "tags", "", "set a list of build tags")
rflag.BoolVar(&useUnsafe, "unsafe", false, "include usafe symbols")
rflag.StringVar(&cmd, "e", "", "set the command to be executed (instead of script or/and shell)")
rflag.Usage = func() {
fmt.Println("Usage: yaegi run [options] [path] [args]")
fmt.Println("Options:")
rflag.PrintDefaults()
}
if err = rflag.Parse(arg); err != nil {
return err
}
args := rflag.Args()
i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")})
i.Use(stdlib.Symbols)
i.Use(interp.Symbols)
if useSyscall {
i.Use(syscall.Symbols)
}
if useUnsafe {
i.Use(unsafe.Symbols)
}
if useUnrestricted {
// Use of unrestricted symbols should always follow use of stdlib symbols, to update them.
i.Use(unrestricted.Symbols)
}
if cmd != "" {
i.REPL(strings.NewReader(cmd), os.Stderr)
}
if len(args) == 0 {
if interactive || cmd == "" {
i.REPL(os.Stdin, os.Stdout)
}
return nil
}
// Skip first os arg to set command line as expected by interpreted main
path := args[0]
os.Args = arg[1:]
flag.CommandLine = flag.NewFlagSet(path, flag.ExitOnError)
if isPackageName(path) {
err = runPackage(i, path)
} else {
if isDir(path) {
err = runDir(i, path)
} else {
err = runFile(i, path)
}
}
if err != nil {
return err
}
if interactive {
i.REPL(os.Stdin, os.Stdout)
}
return nil
}
func isPackageName(path string) bool {
return !strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "./") && !strings.HasPrefix(path, "../")
}
func isDir(path string) bool {
fi, err := os.Lstat(path)
return err == nil && fi.IsDir()
}
func runPackage(i *interp.Interpreter, path string) error {
return fmt.Errorf("runPackage not implemented")
}
func runDir(i *interp.Interpreter, path string) error {
return fmt.Errorf("runDir not implemented")
}
func runFile(i *interp.Interpreter, path string) error {
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if s := string(b); strings.HasPrefix(s, "#!") {
// Allow executable go scripts, Have the same behavior as in interactive mode.
s = strings.Replace(s, "#!", "//", 1)
i.REPL(strings.NewReader(s), os.Stdout)
} else {
// Files not starting with "#!" are supposed to be pure Go, directly Evaled.
i.Name = path
_, err := i.Eval(s)
if err != nil {
fmt.Println(err)
if p, ok := err.(interp.Panic); ok {
fmt.Println(string(p.Stack))
}
}
}
return nil
}

View File

@@ -85,94 +85,53 @@ Debugging support (may be removed at any time):
package main
import (
"errors"
"flag"
"fmt"
"go/build"
"io/ioutil"
"log"
"os"
"strings"
)
"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
"github.com/containous/yaegi/stdlib/syscall"
"github.com/containous/yaegi/stdlib/unrestricted"
"github.com/containous/yaegi/stdlib/unsafe"
const (
Extract = "extract"
Help = "help"
Run = "run"
Test = "test"
)
func main() {
var interactive bool
var useSyscall bool
var useUnrestricted bool
var useUnsafe bool
var tags string
var cmd string
flag.BoolVar(&interactive, "i", false, "start an interactive REPL")
flag.BoolVar(&useSyscall, "syscall", false, "include syscall symbols")
flag.BoolVar(&useUnrestricted, "unrestricted", false, "include unrestricted symbols")
flag.StringVar(&tags, "tags", "", "set a list of build tags")
flag.BoolVar(&useUnsafe, "unsafe", false, "include usafe symbols")
flag.StringVar(&cmd, "e", "", "set the command to be executed (instead of script or/and shell)")
flag.Usage = func() {
fmt.Println("Usage:", os.Args[0], "[options] [script] [args]")
fmt.Println("Options:")
flag.PrintDefaults()
}
flag.Parse()
args := flag.Args()
log.SetFlags(log.Lshortfile)
var err error
var exitCode int
i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")})
i.Use(stdlib.Symbols)
i.Use(interp.Symbols)
if useSyscall {
i.Use(syscall.Symbols)
}
if useUnsafe {
i.Use(unsafe.Symbols)
}
if useUnrestricted {
// Use of unrestricted symbols should always follow use of stdlib symbols, to update them.
i.Use(unrestricted.Symbols)
log.SetFlags(log.Lshortfile) // Ease debugging.
if len(os.Args) > 1 {
cmd = os.Args[1]
}
if cmd != `` {
i.REPL(strings.NewReader(cmd), os.Stderr)
switch cmd {
case Extract:
err = extractCmd(os.Args[2:])
case Help, "-h", "--help":
err = help(os.Args[2:])
case Run:
err = run(os.Args[2:])
case Test:
err = fmt.Errorf("test not implemented")
default:
// If no command is given, fallback to default "run" command.
// This allows scripts starting with "#!/usr/bin/env yaegi",
// as passing more than 1 argument to #! executable may be not supported
// on all platforms.
cmd = Run
err = run(os.Args[1:])
}
if len(args) == 0 {
if interactive || cmd == `` {
i.REPL(os.Stdin, os.Stdout)
}
return
}
// Skip first os arg to set command line as expected by interpreted main
os.Args = os.Args[1:]
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
b, err := ioutil.ReadFile(args[0])
if err != nil {
log.Fatal("Could not read file: ", args[0])
}
if s := string(b); strings.HasPrefix(s, "#!") {
// Allow executable go scripts, Have the same behavior as in interactive mode.
s = strings.Replace(s, "#!", "//", 1)
i.REPL(strings.NewReader(s), os.Stdout)
} else {
// Files not starting with "#!" are supposed to be pure Go, directly Evaled.
i.Name = args[0]
_, err := i.Eval(s)
if err != nil {
fmt.Println(err)
if p, ok := err.(interp.Panic); ok {
fmt.Println(string(p.Stack))
}
}
}
if interactive {
i.REPL(os.Stdin, os.Stdout)
if err != nil && !errors.Is(err, flag.ErrHelp) {
err = fmt.Errorf("%s: %w", cmd, err)
fmt.Fprintln(os.Stderr, err)
exitCode = 1
}
os.Exit(exitCode)
}