~mna/snow unlisted

b80c93389bef70627bad6a5d2db7398350b0ef65 — Martin Angers 1 year, 1 month ago 21ab2b8
cmd/internal: add packages for command-line
A cmd/internal/flag/flag.go => cmd/internal/flag/flag.go +152 -0
@@ 0,0 1,152 @@
// Package flag implements a command-line flags parser that uses struct tags to
// configure supported flags and returns any error it encounters, without
// printing anything automatically. It can optionally read flag values from
// environment variables first, with the command-line flags used to override
// them.
//
// The struct tag to specify flags is `flag`, while the one to specify
// environment variables is `envconfig`. See the envconfig package for full
// details on struct tags configuration and decoding support:
// https://github.com/kelseyhightower/envconfig.
//
// Flag parsing uses the stdlib's flag package internally, and as such shares
// the same behaviour regarding short and long flags.
package flag

import (
	"flag"
	"fmt"
	"io/ioutil"
	"reflect"
	"strings"
	"time"

	"github.com/kelseyhightower/envconfig"
)

// Parser implements a flag parser.
type Parser struct {
	// EnvVars indicates if environment variables are used to read flag values.
	EnvVars bool

	// EnvPrefix is the prefix to use in front of each flag's environment
	// variable name. If it is empty, the name of the program (as read from the
	// args slice at index 0) is used, with dashes replaced with underscores.
	EnvPrefix string
}

// Parse parses args into v, using struct tags to detect flags.  The tag must
// be named "flag" and multiple flags may be set for the same field using a
// comma-separated list.  v must be a pointer to a struct and the flags must be
// defined on fields with a type of string, int, bool or time.Duration.
// If Parser.EnvVars is true, flag values are initialized from corresponding
// environment variables first.
//
// After parsing, if v implements a Validate method that returns an error, it
// is called and any non-nil error is returned as error.
//
// If v has a SetArgs method, it is called with the list of non-flag arguments.
//
// If v has a SetFlags method, it is called with the set of flags that were set
// by args (a map[string]bool).
//
// It panics if v is not a pointer to a struct or if a flag is defined with an
// unsupported type.
func (p *Parser) Parse(args []string, v interface{}) error {
	if p.EnvVars {
		if err := p.parseEnvVars(args, v); err != nil {
			return err
		}
	}

	if err := p.parseFlags(args, v); err != nil {
		return err
	}

	if val, ok := v.(interface{ Validate() error }); ok {
		return val.Validate()
	}
	return nil
}

func (p *Parser) parseFlags(args []string, v interface{}) error {
	if len(args) == 0 {
		return nil
	}

	// create a FlagSet that is silent and only returns any error
	// it encounters.
	fs := flag.NewFlagSet("", flag.ContinueOnError)
	fs.SetOutput(ioutil.Discard)
	fs.Usage = nil

	durationType := reflect.TypeOf(time.Duration(0))

	// extract the flags from the struct
	val := reflect.ValueOf(v).Elem()
	str := reflect.TypeOf(v).Elem()
	count := val.NumField()
	for i := 0; i < count; i++ {
		fld := val.Field(i)
		typ := str.Field(i)
		names := strings.Split(typ.Tag.Get("flag"), ",")

		for _, nm := range names {
			if nm == "" {
				continue
			}
			switch fld.Kind() {
			case reflect.Bool:
				fs.BoolVar(fld.Addr().Interface().(*bool), nm, fld.Bool(), "")
			case reflect.String:
				fs.StringVar(fld.Addr().Interface().(*string), nm, fld.String(), "")
			case reflect.Int:
				fs.IntVar(fld.Addr().Interface().(*int), nm, int(fld.Int()), "")
			default:
				switch typ.Type {
				case durationType:
					fs.DurationVar(fld.Addr().Interface().(*time.Duration), nm, fld.Interface().(time.Duration), "")
				default:
					panic(fmt.Sprintf("unsupported flag field kind: %s (%s: %s)", fld.Kind(), typ.Name, typ.Type))
				}
			}
		}
	}

	if err := fs.Parse(args[1:]); err != nil {
		return err
	}

	if sa, ok := v.(interface{ SetArgs([]string) }); ok {
		args := fs.Args()
		if len(args) == 0 {
			args = nil
		}
		sa.SetArgs(args)
	}
	if sf, ok := v.(interface{ SetFlags(map[string]bool) }); ok {
		set := make(map[string]bool)
		fs.Visit(func(fl *flag.Flag) {
			set[fl.Name] = true
		})
		if len(set) == 0 {
			set = nil
		}
		sf.SetFlags(set)
	}

	return nil
}

func (p *Parser) parseEnvVars(args []string, v interface{}) error {
	prefix := p.EnvPrefix

	if prefix == "" && len(args) > 0 {
		prefix = prefixFromProgramName(args[0])
	}
	return envconfig.Process(prefix, v)
}

func prefixFromProgramName(name string) string {
	return strings.ReplaceAll(name, "-", "_")
}

A cmd/internal/flag/flag_test.go => cmd/internal/flag/flag_test.go +314 -0
@@ 0,0 1,314 @@
package flag

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

type F struct {
	S     string        `flag:"s,string,long-string"`
	I     int           `flag:"i,int"`
	B     bool          `flag:"b"`
	H     bool          `flag:"h,help"`
	T     time.Duration `flag:"t"`
	N     int
	args  []string
	flags map[string]bool
}

func (f *F) SetArgs(args []string) {
	f.args = args
}

func (f *F) SetFlags(flags map[string]bool) {
	f.flags = flags
}

func TestParseFlags(t *testing.T) {
	cases := []struct {
		args []string
		want *F
		err  string
	}{
		{
			want: &F{},
		},
		{
			args: []string{"toto"},
			want: &F{
				args: []string{"toto"},
			},
		},
		{
			args: []string{"-h"},
			want: &F{
				H:     true,
				flags: map[string]bool{"h": true},
			},
		},
		{
			args: []string{"-i", "10", "--int", "20"},
			want: &F{
				I:     20,
				flags: map[string]bool{"i": true, "int": true},
			},
		},
		{
			args: []string{"-i", "10", "--int", "20"},
			want: &F{
				I:     20,
				flags: map[string]bool{"i": true, "int": true},
			},
		},
		{
			args: []string{"-s", "a", "--string", "b", "-long-string", "c"},
			want: &F{
				S:     "c",
				flags: map[string]bool{"s": true, "string": true, "long-string": true},
			},
		},
		{
			args: []string{"-b", "--b", "-b"},
			want: &F{
				B:     true,
				flags: map[string]bool{"b": true},
			},
		},
		{
			args: []string{"-b", "-int", "1", "-string", "a", "arg1", "arg2"},
			want: &F{
				B:     true,
				I:     1,
				S:     "a",
				args:  []string{"arg1", "arg2"},
				flags: map[string]bool{"b": true, "int": true, "string": true},
			},
		},
		{
			args: []string{"-n", "1"},
			want: &F{},
			err:  "not defined: -n",
		},
		{
			args: []string{"-t", "3s"},
			want: &F{
				T:     3 * time.Second,
				flags: map[string]bool{"t": true},
			},
		},
		{
			args: []string{"-t", "nope"},
			want: &F{},
			err:  "invalid value",
		},
	}

	var p Parser
	for _, c := range cases {
		t.Run(strings.Join(c.args, " "), func(t *testing.T) {
			var f F
			args := append([]string{""}, c.args...)
			err := p.Parse(args, &f)

			if c.err != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), c.err)
				return
			}

			require.NoError(t, err)
			require.Equal(t, c.want, &f)
		})
	}
}

func TestParseNoFlag(t *testing.T) {
	type F struct {
		V int
	}
	var p Parser

	f := F{V: 4}
	err := p.Parse([]string{"", "x"}, &f)
	require.NoError(t, err)
	require.Equal(t, 4, f.V)
}

type noFlagSetArgs struct {
	args []string
}

func (n *noFlagSetArgs) SetArgs(args []string) {
	n.args = args
}

func TestParseNoFlagSetArgs(t *testing.T) {
	var p Parser
	f := noFlagSetArgs{}
	err := p.Parse([]string{"", "x"}, &f)
	require.NoError(t, err)
	require.Equal(t, []string{"x"}, f.args)
}

func TestParseArgsError(t *testing.T) {
	type F struct {
		X bool `flag:"x"`
	}
	var p Parser
	f := F{}
	err := p.Parse([]string{"", "-zz"}, &f)
	require.Error(t, err)
	require.Contains(t, err.Error(), "-zz")
}

func TestParseNotStructPointer(t *testing.T) {
	var (
		i int
		p Parser
	)
	require.Panics(t, func() {
		_ = p.Parse([]string{"-h"}, i)
	})
}

func TestParseUnsupportedFlagType(t *testing.T) {
	type F struct {
		C *bool `flag:"c"`
	}
	var (
		f F
		p Parser
	)
	require.Panics(t, func() {
		_ = p.Parse([]string{"", "-h"}, &f)
	})
}

type E struct {
	Addr    string `flag:"addr"`
	DB      string `flag:"db"`
	Help    bool   `flag:"h,help" ignored:"true"`
	Version bool   `flag:"v,version" ignored:"true"`
}

func (e *E) Validate() error {
	if e.Help || e.Version {
		return nil
	}
	if e.Addr == "" {
		return errors.New("address must be set")
	}
	if e.DB == "" {
		return errors.New("db must be set")
	}
	return nil
}

func TestParseEnvVars(t *testing.T) {
	const progName = "mainer-test"

	p := Parser{
		EnvVars: true,
	}

	cases := []struct {
		env    string // prefix-less Key:val pairs, space-separated
		args   string // space-separated, index 0 added automatically
		want   E
		errMsg string // error must contain that errMsg
	}{
		{
			"",
			"",
			E{},
			"address must be set",
		},
		{
			"ADDR::1234 DB:x",
			"",
			E{Addr: ":1234", DB: "x"},
			"",
		},
		{
			"",
			"-addr :2345 -db v",
			E{Addr: ":2345", DB: "v"},
			"",
		},
		{
			"ADDR::1234",
			"-addr :2345 -db x",
			E{Addr: ":2345", DB: "x"},
			"",
		},
		{
			"HELP:true",
			"-addr :2345",
			E{Addr: ":2345"},
			"db must be set",
		},
		{
			"VERSION:1",
			"-addr :2345 -db x",
			E{Addr: ":2345", DB: "x"},
			"",
		},
		{
			"",
			"-help",
			E{Help: true},
			"",
		},
		{
			"",
			"-v",
			E{Version: true},
			"",
		},
		{
			"",
			"-z",
			E{},
			"flag provided but not defined: -z",
		},
	}
	for _, c := range cases {
		t.Run(fmt.Sprintf("%s|%s", c.env, c.args), func(t *testing.T) {
			// set env vars
			if c.env != "" {
				envPairs := strings.Split(c.env, " ")
				for _, pair := range envPairs {
					ix := strings.Index(pair, ":")
					require.True(t, ix >= 0, "%s: missing colon", pair)
					key, val := pair[:ix], pair[ix+1:]
					key = strings.ToUpper(prefixFromProgramName(progName)) + "_" + key
					os.Setenv(key, val)
					defer os.Unsetenv(key)
				}
			}

			// parse args
			args := []string{progName}
			if c.args != "" {
				args = append(args, strings.Split(c.args, " ")...)
			}

			var e E
			err := p.Parse(args, &e)
			if c.errMsg != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), c.errMsg)
			} else {
				require.NoError(t, err)
			}

			require.Equal(t, c.want, e)
		})
	}
}

A cmd/internal/mainer/mainer.go => cmd/internal/mainer/mainer.go +84 -0
@@ 0,0 1,84 @@
// Package mainer defines types relevant to command entrypoint
// implementation. A typical main entrypoint looks like this,
// assuming cmd is a struct which defines the command's flags
// and implements mainer.Mainer:
//
//   func main() {
//     var c cmd
//     os.Exit(int(c.Main(os.Args, mainer.CurrentStdio())))
//   }
//
package mainer

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/signal"
)

// ExitCode is the type of a process exit code.
type ExitCode int

// List of pre-defined exit codes.
const (
	Success ExitCode = iota
	Failure
	InvalidArgs
)

// CurrentStdio returns the Stdio for the current process. Its Cwd
// field reflects the working directory at the time of the call.
func CurrentStdio() *Stdio {
	cwd, err := os.Getwd()
	if err != nil {
		panic(fmt.Sprintf("failed to get current working directory: %s", err))
	}
	return &Stdio{
		Cwd:    cwd,
		Stdin:  os.Stdin,
		Stdout: os.Stdout,
		Stderr: os.Stderr,
	}
}

// Stdio defines the OS abstraction for standard I/O.
type Stdio struct {
	// Cwd is the current working directory.
	Cwd string

	// Stdin is the standard input reader.
	Stdin io.Reader

	// Stdout is the standard output writer.
	Stdout io.Writer

	// Stderr is the standard error writer.
	Stderr io.Writer
}

// Mainer defines the method to implement for a type that
// implements a Main entrypoint of a command.
type Mainer interface {
	Main([]string, Stdio) ExitCode
}

// CancelOnSignal returns a context that is canceled when the process receives
// one of the specified signals.
func CancelOnSignal(ctx context.Context, signals ...os.Signal) context.Context {
	if len(signals) == 0 {
		return ctx
	}

	ctx, cancel := context.WithCancel(ctx)

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, signals...)
	go func() {
		<-ch
		cancel()
	}()

	return ctx
}

A cmd/internal/mainer/mainer_test.go => cmd/internal/mainer/mainer_test.go +44 -0
@@ 0,0 1,44 @@
package mainer

import (
	"context"
	"os"
	"syscall"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestCurrentStdio(t *testing.T) {
	cwd, err := os.Getwd()
	require.NoError(t, err)
	require.Equal(t, cwd, CurrentStdio().Cwd)
}

func TestCancelOnSignal(t *testing.T) {
	ctx := context.Background()
	ctx = CancelOnSignal(ctx, syscall.SIGUSR1)

	select {
	case <-ctx.Done():
		require.Fail(t, "context should block")
	default:
	}

	proc, err := os.FindProcess(os.Getpid())
	require.NoError(t, err)
	require.NoError(t, proc.Signal(syscall.SIGUSR1))

	select {
	case <-ctx.Done():
	case <-time.After(time.Second):
		require.Fail(t, "context should be done")
	}
}

func TestCancelOnSignal_NoSignal(t *testing.T) {
	ctx := context.Background()
	ctx2 := CancelOnSignal(ctx)
	require.Equal(t, ctx, ctx2)
}

A cmd/snowc/main.go => cmd/snowc/main.go +4 -0
@@ 0,0 1,4 @@
package main

func main() {
}

M go.mod => go.mod +2 -1
@@ 3,7 3,8 @@ module git.sr.ht/~mna/snow
go 1.13

require (
	github.com/kr/pretty v0.1.0
	github.com/kelseyhightower/envconfig v1.4.0
	github.com/kylelemons/godebug v1.1.0
	github.com/stretchr/testify v1.4.0
	golang.org/x/exp v0.0.0-20191129062945-2f5052295587
)

M go.sum => go.sum +13 -5
@@ 1,13 1,17 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=


@@ 26,3 30,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

A pkg/compiler/codegen.go => pkg/compiler/codegen.go +1 -0
@@ 0,0 1,1 @@
package compiler

A pkg/compiler/testdata/codegen/fn_add.snow => pkg/compiler/testdata/codegen/fn_add.snow +15 -0
@@ 0,0 1,15 @@
# This is a minimal version of code that should run. It adds
# two numbers and prints the results using an extern Go
# function.

@extern{import: "fmt", symbol: "Println"}
fn println(v: int)

fn add(x: int, y: int) -> int {
  return x + y
}

fn main() {
  let res: int = add(1, 2)
  println(res)
}