├── .gitignore ├── main.go ├── cli ├── run.go └── parse.go ├── run ├── paths.go ├── template.go └── command.go ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // gorram is a command line helper for interfacing with go packages. 2 | // Automagically understands how to produce an interface from the command line 3 | // into a Go function. 4 | // 5 | // For example: 6 | // 7 | // cat ugly.json | gorram json.Indent "" "\t" 8 | // 9 | // The above will run encoding/json.Indent, with src = stdin and dst being a 10 | // bytes.Buffer that gets written to stdout if json.Indent returns without an 11 | // error. 12 | package main 13 | 14 | import ( 15 | "os" 16 | 17 | "npf.io/gorram/cli" 18 | ) 19 | 20 | func main() { 21 | os.Exit(cli.Run()) 22 | } 23 | -------------------------------------------------------------------------------- /cli/run.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | // Run parses the gorram command line args and runs the gorram command. 9 | func Run() int { 10 | env := OSEnv{ 11 | Args: make([]string, len(os.Args)), 12 | Stderr: os.Stderr, 13 | Stdout: os.Stdout, 14 | Stdin: os.Stdin, 15 | Env: getenv(os.Environ()), 16 | } 17 | copy(env.Args, os.Args) 18 | return ParseAndRun(env) 19 | } 20 | 21 | func getenv(env []string) map[string]string { 22 | ret := make(map[string]string, len(env)) 23 | for _, s := range env { 24 | parts := strings.SplitN(s, "=", 2) 25 | if len(parts) != 2 { 26 | panic("invalid environment variable set: " + s) 27 | } 28 | ret[parts[0]] = parts[1] 29 | } 30 | return ret 31 | } 32 | -------------------------------------------------------------------------------- /run/paths.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | func confDir() string { 10 | switch runtime.GOOS { 11 | case "windows": 12 | return filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"), "gorram") 13 | default: 14 | return filepath.Join(os.Getenv("HOME"), ".gorram") 15 | } 16 | } 17 | 18 | func createFile(path string) (f *os.File, err error) { 19 | if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { 20 | return nil, err 21 | } 22 | return os.Create(path) 23 | } 24 | 25 | func dir(cmd Command) string { 26 | base := cmd.Cache 27 | if cmd.Cache == "" { 28 | base = confDir() 29 | } 30 | return filepath.Join(base, filepath.FromSlash(cmd.Package)) 31 | } 32 | 33 | func script(cmd Command) string { 34 | name := cmd.Function 35 | if cmd.GlobalVar != "" { 36 | name = cmd.GlobalVar + "." + cmd.Function 37 | } 38 | return filepath.Join(dir(cmd), name+".go") 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nate Finch 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 | -------------------------------------------------------------------------------- /run/template.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import "text/template" 4 | 5 | var templ = template.Must(template.New("").Parse(` 6 | package main 7 | 8 | import ( 9 | {{range $import, $ignored := .Imports -}} 10 | "{{$import}}" 11 | {{end}} 12 | ) 13 | 14 | func main() { 15 | log.SetFlags(0) 16 | 17 | {{.SrcInit}} 18 | 19 | {{if gt .NumCLIArgs 0}} 20 | // strip off the executable name and the -- that we put in so that go run 21 | // won't treat arguments to the script as files to run. 22 | if len(os.Args) < 3 { 23 | log.Fatal("Not enough arguments.\n\n") 24 | } 25 | args := os.Args[2:] 26 | {{end}} 27 | {{if ne .SrcIdx -1}} 28 | expectedCLIArgs := {{.NumCLIArgs}} 29 | switch len(args) { 30 | case expectedCLIArgs - 1: 31 | src = stdinToSrc() 32 | case expectedCLIArgs: 33 | src, args = argsToSrc(args) 34 | default: 35 | log.Fatalf("Expected %d or %d arguments, but got %d args.\n\n", expectedCLIArgs-1, expectedCLIArgs, len(args)) 36 | } 37 | {{end}} 38 | {{range .ArgInits}} 39 | {{.}} 40 | {{end}} 41 | {{.DstInit}} 42 | 43 | 44 | {{.Results}}{{.PkgName}}.{{if .GlobalVar}}{{.GlobalVar}}.{{end}}{{.Func}}({{.Args}}) 45 | {{.ErrCheck}} 46 | {{if ne .DstIdx -1}} 47 | {{.DstToStdout}} 48 | {{else}} 49 | {{.PrintVal}} 50 | {{end}} 51 | } 52 | {{.ArgsToSrc}} 53 | {{.StdinToSrc}} 54 | {{range .ArgConvFuncs}} 55 | {{.}} 56 | {{end}} 57 | `)) 58 | -------------------------------------------------------------------------------- /cli/parse.go: -------------------------------------------------------------------------------- 1 | package cli // import "npf.io/gorram/cli" 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "npf.io/gorram/run" 10 | ) 11 | 12 | // CacheEnv is the environment variable that users may set to change the 13 | // location where gorram stores its script files. 14 | const CacheEnv = "GORRAM_CACHE" 15 | 16 | // OSEnv encapsulates the gorram environment. 17 | type OSEnv struct { 18 | Args []string 19 | Stderr io.Writer 20 | Stdout io.Writer 21 | Stdin io.Reader 22 | Env map[string]string 23 | } 24 | 25 | // ParseAndRun parses the environment to create a run.Command and runs it. It 26 | // returns the code that should be used for os.Exit. 27 | func ParseAndRun(env OSEnv) int { 28 | cmd, err := Parse(env) 29 | if err != nil { 30 | fmt.Fprintln(env.Stderr, err.Error()) 31 | return code(err) 32 | } 33 | renv := run.Env{ 34 | Stderr: env.Stderr, 35 | Stdout: env.Stdout, 36 | Stdin: env.Stdin, 37 | } 38 | if err := run.Run(cmd, renv); err != nil { 39 | fmt.Fprintln(env.Stderr, err.Error()) 40 | return 1 41 | } 42 | return 0 43 | } 44 | 45 | func code(err error) int { 46 | type coded interface { 47 | Code() int 48 | } 49 | if c, ok := err.(coded); ok { 50 | return c.Code() 51 | } 52 | return 1 53 | } 54 | 55 | // Parse converts the gorram command line. If an error is returned, the program 56 | // should exit with the code specified by the error's Code() int function. 57 | func Parse(env OSEnv) (run.Command, error) { 58 | fs := flag.NewFlagSet("", flag.ContinueOnError) 59 | regen := fs.Bool("r", false, "") 60 | err := fs.Parse(env.Args[1:]) 61 | switch { 62 | case err == flag.ErrHelp: 63 | return run.Command{}, codeError{code: 0, msg: usage} 64 | case err != nil: 65 | return run.Command{}, codeError{code: 2, msg: err.Error()} 66 | } 67 | args := fs.Args() 68 | if len(args) == 0 { 69 | return run.Command{}, codeError{code: 0, msg: usage} 70 | } 71 | if len(args) == 1 { 72 | return run.Command{}, codeError{code: 2, msg: usage} 73 | } 74 | cmd := run.Command{ 75 | Args: args[2:], 76 | Regen: regen != nil && *regen, 77 | Package: args[0], 78 | } 79 | parts := strings.Split(args[1], ".") 80 | switch len(parts) { 81 | case 1: 82 | cmd.Function = parts[0] 83 | case 2: 84 | cmd.GlobalVar = parts[0] 85 | cmd.Function = parts[1] 86 | default: 87 | return run.Command{}, codeError{code: 2, msg: fmt.Sprintf(`Command %q invalid. Expected "importpath Function" or "importpath Varable.Method".`, args[0])} 88 | } 89 | if d := env.Env[CacheEnv]; d != "" { 90 | cmd.Cache = d 91 | } 92 | return cmd, nil 93 | } 94 | 95 | type codeError struct { 96 | code int 97 | msg string 98 | } 99 | 100 | func (c codeError) Error() string { 101 | return c.msg 102 | } 103 | 104 | func (c codeError) Code() int { 105 | return c.code 106 | } 107 | 108 | const usage = `Usage: 109 | gorram [OPTION] [args...] 110 | 111 | Options: 112 | -r regenerate the script generated for the given function 113 | -h, --help display this help 114 | 115 | Executes a go function or an method on a global variable defined in a package in 116 | the stdlib or a package in your GOPATH. Package must be the full package import 117 | path, e.g. encoding/json. Only exported functions, methods, and variables may 118 | be called. 119 | 120 | Most builtin types are supported, and streams of input (via io.Reader or []byte 121 | for example) may be read from stdin. If specified as an argument, the argument 122 | to a stream input is expected to be a filename. 123 | 124 | Return values are printed to stdout. If the function has an output argument, 125 | like io.Reader or *bytes.Buffer, it is automatically passed in and then written 126 | to stdout. 127 | 128 | Example: 129 | 130 | $ echo '{"a":"b"}' | gorram encoding/json Indent "" " " 131 | { 132 | "a" : "b" 133 | } 134 | ` 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gorram [![Build Status](https://drone.io/github.com/natefinch/gorram/status.png)](https://drone.io/github.com/natefinch/gorram/latest) 2 | ![river](https://cloud.githubusercontent.com/assets/3185864/18798443/97829e60-81a0-11e6-99a2-d8a788dd9279.jpg) 3 | 4 | image: © [SubSuid](http://subsuid.deviantart.com/art/River-Tam-Speed-Drawing-282223915) 5 | 6 | It's like go run for any go function. 7 | 8 | Automagically understands how to produce an interface from the command line into 9 | a Go function. 10 | 11 | *Sometimes, magic is just someone spending more time on something than anyone else might reasonably expect.* -Teller 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get npf.io/gorram 17 | ``` 18 | 19 | Note: gorram depends on having a working go environment to function, since it 20 | dynamically analyzes go code in the stdlib and in your GOPATH. 21 | 22 | ## Examples 23 | 24 | Pretty print JSON: 25 | 26 | ``` 27 | $ echo '{ "foo" : "bar" }' | gorram encoding/json Indent "" $'\t' 28 | { 29 | "foo" : "bar" 30 | } 31 | ``` 32 | 33 | Calculate a sha256 sum: 34 | 35 | ``` 36 | $ gorram crypto/sha256 Sum256 foo.gz 37 | abcdef012345678 38 | ``` 39 | 40 | 41 | ## How it works 42 | 43 | The first time you run Gorram with a specific function name, Gorram analyzes the 44 | package function and generates a file for use with `go run`. Gorram 45 | intelligently converts stdin and/or cli arguments into arguments for the 46 | function. Output is converted similarly to stdout. The code is cached in a 47 | local directory so that later runs don't incur the generation overhead. 48 | 49 | ## Heuristics 50 | 51 | By default, Gorram just turns CLI args into function args and prints out the 52 | return value of a function using fmt's %v. However, there are some special 53 | heuristics that it uses to be smarter about inputs and outputs, based on common 54 | go idioms. 55 | 56 | For example: 57 | 58 | ``` 59 | usage: 60 | $ cat foo.zip | gorram crypto/sha1 Sum 61 | or 62 | $ gorram crypto/sha1 Sum foo.zip 63 | 64 | function: 65 | // crypto/sha1 66 | func Sum(data []byte) [Size]byte 67 | ``` 68 | 69 | Gorram understands functions that take a single slice of bytes (or an io.Reader) 70 | should read from stdin, or if an argument is specified, the argument is treated 71 | as a filename to be read. 72 | 73 | Return values that are an array of bytes are understood to be intended to be 74 | printed with fmt's %x, so that you get `2c37424d58` instead of `[44 55 66 77 75 | 88]`. 76 | 77 | ``` 78 | usage: 79 | $ gorram encoding/json Indent foo.json "" $'\t' 80 | or 81 | $ cat foo.json | gorram encoding/json Indent "" $'\t' 82 | 83 | function: 84 | // encoding/json 85 | func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error 86 | ``` 87 | 88 | Gorram understands that functions with a src argument that is an io.Reader or 89 | []bytes and a dst argument that is a []byte, *bytes.Buffer, or io.Writer will 90 | read from stdin (or use an argument as a file to open), and write what is 91 | written to dst to stdout. 92 | 93 | Gorram understands that if the function returns a non-nil error, the error 94 | should be written to stderr, the program exits with a non-zero exit status, and 95 | nothing is written to stdout. 96 | 97 | Gorram understands that prefix and indent are arguments that need to be 98 | specified in the command line. 99 | 100 | 101 | ``` 102 | usage: 103 | $ gorram math Cos 25 104 | 105 | function: 106 | // math 107 | func Cos(x float64) float64 108 | ``` 109 | 110 | Gorram understands how to convert CLI arguments using the stringconv.Parse* 111 | functions, and will print outputs with `fmt.Printf("%v\n", val)`. 112 | 113 | 114 | ``` 115 | usage: 116 | $ echo 12345 | gorram encoding/base64 StdEncoding.EncodeToString 117 | MTIzNDU2Cg== 118 | 119 | function: 120 | // base64 121 | func (e *Encoding) EncodeToString(b []byte]) string 122 | ``` 123 | Gorram understands that packages have global variables that have methods you can 124 | call. 125 | 126 | ## Development 127 | 128 | See the [project page](https://github.com/natefinch/gorram/projects/1) for what's 129 | being worked on now. -------------------------------------------------------------------------------- /run/command.go: -------------------------------------------------------------------------------- 1 | package run // import "npf.io/gorram/run" 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/types" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "sort" 11 | "strings" 12 | 13 | "golang.org/x/tools/go/loader" 14 | ) 15 | 16 | func initTypes(prog *loader.Program) { 17 | buf := prog.Package("bytes").Pkg.Scope().Lookup("Buffer").Type() 18 | ptrBytesBufferType = types.NewPointer(buf) 19 | 20 | errorType = types.Universe.Lookup("error").Type() 21 | ioReaderType = prog.Package("io").Pkg.Scope().Lookup("Reader").Type() 22 | ioWriterType = prog.Package("io").Pkg.Scope().Lookup("Writer").Type() 23 | ioReader = ioReaderType.Underlying().(*types.Interface) 24 | ioWriter = ioWriterType.Underlying().(*types.Interface) 25 | 26 | // we do these here so they are definitely performed after we initialize 27 | // some of the types they depend on. 28 | setDstHandlers() 29 | setSrcHandlers() 30 | setArgConverters() 31 | } 32 | 33 | // Used for type comparison. 34 | var ptrBytesBufferType types.Type 35 | var errorType types.Type 36 | var ioReaderType types.Type 37 | var ioWriterType types.Type 38 | var byteSliceType = types.NewSlice(types.Typ[types.Byte]) 39 | var stringType = types.Typ[types.String] 40 | 41 | // Used for types.Implements. 42 | var ioReader *types.Interface 43 | var ioWriter *types.Interface 44 | 45 | // Command contains the definition of a command that gorram can execute. It 46 | // represents a package and either global function or a method call on a global 47 | // variable. If GlobalVar is non-empty, it's the latter. 48 | type Command struct { 49 | // Args contains the arguments to the function. 50 | Args []string 51 | // Package the function exists in. 52 | Package string 53 | // Function (or method) to call. 54 | Function string 55 | // GlobalVar, if not empty, indicates a global variable to call, and means 56 | // Function is a method on that variable. 57 | GlobalVar string 58 | // Regen, if true, indicates we should create a new script file even if the 59 | // old one exists. 60 | Regen bool 61 | // Cache, if non-empty, indicates the user has specified the non-default 62 | // location for their gorram scripts to be located. 63 | Cache string 64 | } 65 | 66 | // Env encapsulates the externalities of the environment in which a command is 67 | // running. 68 | type Env struct { 69 | Stderr io.Writer 70 | Stdout io.Writer 71 | Stdin io.Reader 72 | } 73 | 74 | // Run generates the gorram .go file if it doesn't already exist and then runs 75 | // it with the given args. 76 | func Run(cmd Command, env Env) error { 77 | path, err := Generate(cmd, env) 78 | if err != nil { 79 | return err 80 | } 81 | return run(path, cmd.Args, env) 82 | } 83 | 84 | func run(path string, args []string, env Env) error { 85 | // put a -- between the filename and the args so we don't confuse go run 86 | // into thinking the first arg is another file to run. 87 | realArgs := append([]string{"run", path, "--"}, args...) 88 | cmd := exec.Command("go", realArgs...) 89 | cmd.Stdin = env.Stdin 90 | cmd.Stderr = env.Stderr 91 | cmd.Stdout = env.Stdout 92 | return cmd.Run() 93 | } 94 | 95 | // Generate creates the gorram .go file for the given command. 96 | func Generate(cmd Command, env Env) (path string, err error) { 97 | path = script(cmd) 98 | if !cmd.Regen { 99 | if _, err := os.Stat(script(cmd)); err == nil { 100 | return path, nil 101 | } 102 | } 103 | // let's see if this is even a valid package 104 | imports := map[string]bool{ 105 | "io": false, 106 | "bytes": false, 107 | } 108 | imports[cmd.Package] = false 109 | conf := loader.Config{ 110 | ImportPkgs: imports, 111 | } 112 | lprog, err := conf.Load() 113 | if err != nil { 114 | return "", err 115 | } 116 | initTypes(lprog) 117 | 118 | // Find the package and package-level object. 119 | pkg := lprog.Package(cmd.Package).Pkg 120 | 121 | f, err := getFunc(cmd, pkg) 122 | if err != nil { 123 | return "", err 124 | } 125 | // guaranteed to work per types.Cloud docs. 126 | sig := f.Type().(*types.Signature) 127 | data, err := compileData(cmd, pkg.Name(), sig) 128 | if err != nil { 129 | return "", err 130 | } 131 | if err := gen(cmd, path, data); err != nil { 132 | return "", err 133 | } 134 | if err := goFmt(path, env); err != nil { 135 | return "", err 136 | } 137 | return path, nil 138 | } 139 | 140 | func goFmt(path string, env Env) error { 141 | // put a -- between the filename and the args so we don't confuse go run 142 | // into thinking the first arg is another file to run. 143 | cmd := exec.Command("gofmt", "-s", "-w", path) 144 | cmd.Stderr = env.Stderr 145 | cmd.Stdout = env.Stdout 146 | return cmd.Run() 147 | } 148 | 149 | func getFunc(cmd Command, pkg *types.Package) (*types.Func, error) { 150 | if cmd.GlobalVar == "" { 151 | obj := pkg.Scope().Lookup(cmd.Function) 152 | if obj == nil { 153 | return nil, fmt.Errorf("%s.%s not found", cmd.Package, cmd.Function) 154 | } 155 | f, ok := obj.(*types.Func) 156 | if !ok { 157 | return nil, fmt.Errorf("%s.%s is not a function", cmd.Package, cmd.Function) 158 | } 159 | if !f.Exported() { 160 | return nil, fmt.Errorf("%s.%s is not exported", cmd.Package, cmd.Function) 161 | } 162 | return f, nil 163 | } 164 | obj := pkg.Scope().Lookup(cmd.GlobalVar) 165 | if obj == nil { 166 | return nil, fmt.Errorf("%s.%s not found", cmd.Package, cmd.GlobalVar) 167 | } 168 | v, ok := obj.(*types.Var) 169 | if !ok { 170 | return nil, fmt.Errorf("%s.%s is not a global variable", cmd.Package, cmd.GlobalVar) 171 | } 172 | if !v.Exported() { 173 | return nil, fmt.Errorf("%s.%s is not exported", cmd.Package, cmd.GlobalVar) 174 | } 175 | methods := types.NewMethodSet(v.Type()) 176 | sel := methods.Lookup(pkg, cmd.Function) 177 | if sel == nil { 178 | return nil, fmt.Errorf("%s.%s.%s not found", cmd.Package, cmd.GlobalVar, cmd.Function) 179 | } 180 | f, ok := sel.Obj().(*types.Func) 181 | if !ok { 182 | return nil, fmt.Errorf("%s.%s.%s is not a method", cmd.Package, cmd.GlobalVar, cmd.Function) 183 | } 184 | if !f.Exported() { 185 | return nil, fmt.Errorf("%s.%s.%s is not exported", cmd.Package, cmd.GlobalVar, cmd.Function) 186 | } 187 | return f, nil 188 | } 189 | 190 | func gen(cmd Command, path string, data templateData) error { 191 | f, err := createFile(path) 192 | if err != nil { 193 | return err 194 | } 195 | defer f.Close() 196 | return templ.Execute(f, data) 197 | } 198 | 199 | type templateData struct { 200 | Results string 201 | Args string 202 | NumCLIArgs int 203 | PkgName string 204 | GlobalVar string 205 | Func string 206 | SrcIdx int 207 | DstIdx int 208 | ErrCheck string 209 | HasLen bool 210 | SrcInit string 211 | ArgsToSrc string 212 | StdinToSrc string 213 | DstInit string 214 | DstToStdout string 215 | PrintVal string 216 | ParamTypes map[types.Type]struct{} 217 | Imports map[string]struct{} 218 | ArgConvFuncs []string 219 | ArgInits []string 220 | } 221 | 222 | func compileData(cmd Command, pkgName string, sig *types.Signature) (templateData, error) { 223 | data := templateData{ 224 | PkgName: pkgName, 225 | Func: cmd.Function, 226 | GlobalVar: cmd.GlobalVar, 227 | HasLen: hasLen(sig.Results()), 228 | SrcIdx: -1, 229 | DstIdx: -1, 230 | ParamTypes: map[types.Type]struct{}{}, 231 | Imports: map[string]struct{}{ 232 | cmd.Package: struct{}{}, 233 | "log": struct{}{}, 234 | "os": struct{}{}, 235 | }, 236 | } 237 | if err := data.parseResults(sig.Results()); err != nil { 238 | return templateData{}, err 239 | } 240 | if dst, src, ok := checkSrcDst(sig.Params()); ok { 241 | if err := data.setSrcDst(dst, src, sig.Params()); err != nil { 242 | return templateData{}, err 243 | } 244 | } else { 245 | 246 | } 247 | if err := data.parseParams(sig.Params()); err != nil { 248 | return templateData{}, err 249 | } 250 | data.NumCLIArgs = sig.Params().Len() 251 | if data.DstIdx != -1 { 252 | data.NumCLIArgs-- 253 | } 254 | return data, nil 255 | } 256 | 257 | func (data *templateData) setSrcDst(dst, src int, params *types.Tuple) error { 258 | data.SrcIdx = src 259 | data.DstIdx = dst 260 | srcType := params.At(src).Type() 261 | srcH, ok := getSrcHandler(srcType) 262 | if !ok { 263 | return fmt.Errorf("should be impossible: src type %q has no handler", srcType) 264 | } 265 | 266 | // if src position is, as per usual, after dst, e.g. func f(dst, src []byte) 267 | // then the cli arg position will actually be one less than that, since we 268 | // don't pass in the dst arg from the CLI. e.g. in the above, dst is 0, src 269 | // is 1, but for the cli args, src would be arg 0. 270 | srcArg := src 271 | if dst != -1 && src > dst { 272 | srcArg-- 273 | } 274 | data.ArgsToSrc = fmt.Sprintf(srcH.ArgToSrc, srcArg) 275 | data.StdinToSrc = srcH.StdinToSrc 276 | for _, imp := range srcH.Imports { 277 | data.Imports[imp] = struct{}{} 278 | } 279 | data.SrcInit = srcH.Init 280 | 281 | if dst == -1 { 282 | return nil 283 | } 284 | dstType := params.At(dst).Type() 285 | dstH, ok := getDstHandler(dstType) 286 | if !ok { 287 | return fmt.Errorf("should be impossible: dst type %q has no handler", dstType) 288 | } 289 | data.DstInit = dstH.Init 290 | data.DstToStdout = dstH.ToStdout 291 | for _, imp := range dstH.Imports { 292 | data.Imports[imp] = struct{}{} 293 | } 294 | return nil 295 | } 296 | 297 | func (data *templateData) parseParams(params *types.Tuple) error { 298 | pos := 0 299 | var args []string 300 | for x := 0; x < params.Len(); x++ { 301 | if x == data.SrcIdx { 302 | args = append(args, "src") 303 | continue 304 | } 305 | if x == data.DstIdx { 306 | args = append(args, "dst") 307 | continue 308 | } 309 | p := params.At(x) 310 | t := p.Type() 311 | conv, ok := getArgConverter(t) 312 | if !ok { 313 | return fmt.Errorf("don't understand how to convert arg %q from CLI", p.Name()) 314 | } 315 | args = append(args, fmt.Sprintf("arg%d", pos+1)) 316 | data.ParamTypes[t] = struct{}{} 317 | data.ArgInits = append(data.ArgInits, fmt.Sprintf(conv.Assign, pos+1, pos)) 318 | pos++ 319 | } 320 | data.Args = strings.Join(args, ", ") 321 | for t := range data.ParamTypes { 322 | converter, _ := getArgConverter(t) 323 | data.ArgConvFuncs = append(data.ArgConvFuncs, converter.Func) 324 | for _, imp := range converter.Imports { 325 | data.Imports[imp] = struct{}{} 326 | } 327 | } 328 | 329 | // sort so we have consistent output. 330 | sort.Strings(data.ArgConvFuncs) 331 | return nil 332 | } 333 | 334 | func checkSrcDst(params *types.Tuple) (dst, src int, ok bool) { 335 | dst, src = -1, -1 336 | for x := 0; x < params.Len(); x++ { 337 | p := params.At(x) 338 | switch p.Name() { 339 | case "dst": 340 | if isDstType(p.Type()) { 341 | dst = x 342 | } 343 | default: 344 | if src == -1 && isSrcType(p.Type()) { 345 | src = x 346 | } 347 | } 348 | } 349 | if src != -1 { 350 | return dst, src, true 351 | } 352 | return -1, -1, false 353 | } 354 | 355 | func isDstType(t types.Type) bool { 356 | _, ok := getDstHandler(t) 357 | return ok 358 | } 359 | 360 | func isSrcType(t types.Type) bool { 361 | _, ok := getSrcHandler(t) 362 | return ok 363 | } 364 | 365 | func isByteArray(t types.Type) bool { 366 | arr, ok := t.(*types.Array) 367 | if !ok { 368 | return false 369 | } 370 | return types.Identical(arr.Elem(), types.Typ[types.Byte]) 371 | } 372 | 373 | type retHandler struct { 374 | Filter func(types.Type) bool 375 | Imports []string 376 | Code string 377 | } 378 | 379 | var defaultRetHandler = retHandler{ 380 | Imports: []string{"os", "fmt", "log"}, 381 | Code: ` 382 | if _, err := fmt.Fprintf(os.Stdout, "%v\n", val); err != nil { 383 | log.Fatal(err) 384 | } 385 | `} 386 | 387 | func getRetHandler(t types.Type) retHandler { 388 | for _, h := range retHandlers { 389 | if h.Filter(t) { 390 | return h 391 | } 392 | } 393 | return defaultRetHandler 394 | } 395 | 396 | var retHandlers = []retHandler{ 397 | { 398 | Filter: isByteArray, 399 | Imports: []string{"fmt", "os", "log"}, 400 | Code: ` 401 | if _, err := fmt.Fprintf(os.Stdout, "%x\n", val); err != nil { 402 | log.Fatal(err) 403 | } 404 | `, 405 | }, 406 | } 407 | 408 | // yay go! (no, really, I actually do like go's error handling) 409 | const errCheck = ` 410 | if err != nil { 411 | log.Fatal(err) 412 | } 413 | ` 414 | 415 | // parseResults ensures that the return value on the signature is one that we 416 | // can support, and creates the data to output in the template data. 417 | func (data *templateData) parseResults(results *types.Tuple) error { 418 | switch results.Len() { 419 | case 0: 420 | return nil 421 | case 1: 422 | if types.Identical(results.At(0).Type(), errorType) { 423 | data.Results = "err := " 424 | data.ErrCheck = errCheck 425 | data.Imports["log"] = struct{}{} 426 | return nil 427 | } 428 | if hasLen(results) { 429 | data.Results = "_ = " 430 | return nil 431 | } 432 | data.Results = "val := " 433 | data.setReturnType(results.At(0).Type()) 434 | return nil 435 | case 2: 436 | // val, err is ok. 437 | if types.Identical(results.At(1).Type(), errorType) { 438 | if hasLen(results) { 439 | data.Results = "_, err := " 440 | data.ErrCheck = errCheck 441 | data.Imports["log"] = struct{}{} 442 | return nil 443 | } 444 | data.Results = "val, err := " 445 | data.ErrCheck = errCheck 446 | data.setReturnType(results.At(0).Type()) 447 | return nil 448 | } 449 | return errors.New("can't understand function with multiple non-error return values") 450 | default: 451 | return errors.New("can't understand functions with more than two return values") 452 | } 453 | } 454 | 455 | func (data *templateData) setReturnType(t types.Type) { 456 | h := getRetHandler(t) 457 | data.PrintVal = h.Code 458 | for _, imp := range h.Imports { 459 | data.Imports[imp] = struct{}{} 460 | } 461 | } 462 | 463 | // hasError determines if the function can fail. For this, we assume the last 464 | // value returned is the one that determines whether or not the function may 465 | // fail. We also assume that only the builtin error interface indicates an 466 | // error. 467 | func hasError(sig *types.Signature) bool { 468 | if len := sig.Results().Len(); len > 0 { 469 | // We only care about the last value. 470 | return types.Identical(sig.Results().At(len-1).Type().Underlying(), errorType) 471 | } 472 | return false 473 | } 474 | 475 | // hasLen determines if the function returns a value indicating a number of 476 | // bytes written. This is a common go idiom, and is usually the first value 477 | // returned, with a variable name called n of type int. 478 | func hasLen(results *types.Tuple) bool { 479 | if results.Len() > 0 { 480 | // We only care about the last value. 481 | val := results.At(0) 482 | return val.Name() == "n" && types.Identical(val.Type().Underlying(), types.Typ[types.Int]) 483 | } 484 | return false 485 | } 486 | 487 | type srcHandler struct { 488 | // Type is the type, duh. 489 | Type types.Type 490 | // Imports holds the packages needed for the functions this handler uses. 491 | Imports []string 492 | // Init holds the line that initializes the src variable. 493 | Init string 494 | // ArgToSrc holds the definition of a function that is put at the bottom of the 495 | // file to convert the src CLI arg into the proper format for the function. It 496 | // is expected to contain a %d format directive taking the index of the src arg 497 | // from the cli args. 498 | ArgToSrc string 499 | // StdInToSrc holds the definition of a function that is put at the bottom 500 | // of the file to convert data sent to stdin into a format suitable to pass 501 | // to the function. 502 | StdinToSrc string 503 | } 504 | 505 | var srcHandlers []srcHandler 506 | 507 | // have to do it this way since some types won't work in maps. 508 | func getSrcHandler(t types.Type) (srcHandler, bool) { 509 | for _, h := range srcHandlers { 510 | if types.Identical(t, h.Type) { 511 | return h, true 512 | } 513 | } 514 | return srcHandler{}, false 515 | } 516 | 517 | func setSrcHandlers() { 518 | srcHandlers = []srcHandler{ 519 | { 520 | Type: byteSliceType, 521 | Imports: []string{"io/ioutil", "log"}, 522 | Init: "var src []byte", 523 | ArgToSrc: ` 524 | func argsToSrc(args []string) ([]byte, []string) { 525 | srcIdx := %d 526 | src, err := ioutil.ReadFile(args[srcIdx]) 527 | if err != nil { 528 | log.Fatal(err) 529 | } 530 | // Take out the src arg. 531 | args = append(args[:srcIdx], args[srcIdx+1:]...) 532 | return src, args 533 | } 534 | `, 535 | StdinToSrc: ` 536 | func stdinToSrc() []byte { 537 | src, err := ioutil.ReadAll(os.Stdin) 538 | if err != nil { 539 | log.Fatal(err) 540 | } 541 | return src 542 | } 543 | `}, 544 | { 545 | Type: ioReaderType, 546 | Imports: []string{"io", "os"}, 547 | Init: "var src io.Reader", 548 | ArgToSrc: ` 549 | func argsToSrc(args []string) (io.Reader, []string) { 550 | srcIdx := %d 551 | // yes, I know I never close this. It gets closed when the process exits. 552 | // It's ugly, but it works and it simplifies the code. Sorry. 553 | src, err = os.Open(args[srcIdx]) 554 | if err != nil { 555 | log.Fatal(err) 556 | } 557 | // Take out the src arg. 558 | args = append(args[:srcIdx], args[srcIdx+1]...) 559 | return src, args 560 | } 561 | `, 562 | StdinToSrc: ` 563 | func stdinToSrc() io.Reader { 564 | return os.Stdin 565 | } 566 | `}, 567 | } 568 | } 569 | 570 | // dstHandler contains the code to handle destination arguments in a function. 571 | type dstHandler struct { 572 | // Type is the types.Type this handler handles. 573 | Type types.Type 574 | // Imports holds the packages needed for the functions this handler uses. 575 | Imports []string 576 | // Init contains the initialization line necessary for creating a variable 577 | // called dst that is used in functions that have a source and destination 578 | // arguments. 579 | Init string 580 | // ToStdout contains the code that handles writing the data written to dst 581 | // to stdout. 582 | ToStdout string 583 | } 584 | 585 | var dstHandlers []dstHandler 586 | 587 | func getDstHandler(t types.Type) (dstHandler, bool) { 588 | for _, h := range dstHandlers { 589 | if types.Identical(t, h.Type) { 590 | return h, true 591 | } 592 | } 593 | return dstHandler{}, false 594 | } 595 | 596 | func setDstHandlers() { 597 | dstHandlers = []dstHandler{ 598 | { 599 | Type: ptrBytesBufferType, 600 | Imports: []string{"bytes", "io"}, 601 | Init: "dst := &bytes.Buffer{}", 602 | ToStdout: ` 603 | if _, err := io.Copy(os.Stdout, dst); err != nil { 604 | log.Fatal(err) 605 | } 606 | `}, 607 | { 608 | Type: ioWriterType, 609 | Imports: []string{"os"}, 610 | Init: "dst := os.Stdout", 611 | // no ToStdout needed, since dst *is* stdout. 612 | }, 613 | } 614 | } 615 | 616 | // converter is a type that holds information about argument conversions from 617 | // CLI strings to function arguments of various types. If a function takes an 618 | // argument that is not declared here, and is not a destination or source 619 | // argument, we can't handle it. 620 | type converter struct { 621 | // Type is the types.Type this converter converts. 622 | Type types.Type 623 | // Assign is a format string that is used to make the conversion between CLI 624 | // arg x and function arg y in the body of the main function. It should 625 | // take the cli arg index and the function arg index as %d format values. 626 | // Ideally, it is a single line of code, which may call a helper function. 627 | // If it calls a helper function, that function must be listed in Func. 628 | Assign string 629 | // Imports is the list of imports that Func uses, so we can make sure 630 | // they're added to the list of imports. 631 | Imports []string 632 | // Func is the declaration of the conversion function between a string (the 633 | // CLI arg) and a given type. It must only return a single value of the 634 | // appropriate type. Errors should be handled with log.Fatal(err). It 635 | // should be named argTo to avoid collision with other conversion 636 | // function. 637 | Func string 638 | } 639 | 640 | // argConverters is a map of types to helper functions that we dump at the 641 | // end of the file to make the rest of the file easier to construct and read. The values 642 | var argConverters []converter 643 | 644 | func getArgConverter(t types.Type) (converter, bool) { 645 | for _, c := range argConverters { 646 | if types.Identical(t, c.Type) { 647 | return c, true 648 | } 649 | } 650 | return converter{}, false 651 | } 652 | 653 | func setArgConverters() { 654 | argConverters = []converter{ 655 | { 656 | // string is a special flower because it doesn't need a converter, but we 657 | // keep an empty converter here so that we don't need to special case it 658 | // elsewhere. 659 | Type: types.Typ[types.String], 660 | Assign: "arg%d := args[%d]", 661 | }, 662 | { 663 | Type: types.Typ[types.Int], 664 | Assign: "arg%d := argToInt(args[%d])", 665 | Imports: []string{"strconv", "log"}, 666 | Func: ` 667 | func argToInt(s string) int { 668 | i, err := strconv.ParseInt(s, 0, 0) 669 | if err != nil { 670 | log.Fatal(err) 671 | } 672 | return int(i) 673 | } 674 | `}, 675 | { 676 | Type: types.Typ[types.Uint], 677 | Assign: "arg%d := argToUint(args[%d])", 678 | Imports: []string{"strconv", "log"}, 679 | Func: ` 680 | func argToUint(s string) int { 681 | u, err := strconv.ParseUint(s, 0, 0) 682 | if err != nil { 683 | log.Fatal(err) 684 | } 685 | return uint(u) 686 | } 687 | `}, 688 | { 689 | Type: types.Typ[types.Float64], 690 | Assign: "arg%d := argToFloat64(args[%d])", 691 | Imports: []string{"strconv", "log"}, 692 | Func: ` 693 | func argToFloat64(s string) float64 { 694 | f, err := strconv.ParseFloat(s, 64) 695 | if err != nil { 696 | log.Fatal(err) 697 | } 698 | return f 699 | } 700 | `}, 701 | { 702 | Type: types.Typ[types.Bool], 703 | Assign: "arg%d := argToBool(args[%d])", 704 | Imports: []string{"strconv", "log"}, 705 | Func: ` 706 | func argToBool(s string) bool { 707 | b, err := strconv.ParseBool(s) 708 | if err != nil { 709 | log.Fatal(err) 710 | } 711 | return b 712 | } 713 | `}, 714 | { 715 | Type: types.Typ[types.Int64], 716 | Assign: "arg%d := argToInt64(args[%d])", 717 | Imports: []string{"strconv", "log"}, 718 | Func: ` 719 | func argToInt64(s string) int64 { 720 | i, err := strconv.ParseInt(s, 0, 64) 721 | if err != nil { 722 | log.Fatal(err) 723 | } 724 | return i 725 | } 726 | `}, 727 | { 728 | Type: types.Typ[types.Uint64], 729 | Assign: "arg%d := argToUint64(args[%d])", 730 | Imports: []string{"strconv", "log"}, 731 | Func: ` 732 | func argToUint(s string) uint64 { 733 | u, err := strconv.ParseUint(s, 0, 64) 734 | if err != nil { 735 | log.Fatal(err) 736 | } 737 | return u 738 | } 739 | `}, 740 | } 741 | } 742 | --------------------------------------------------------------------------------