3.6 命令行界面

本节的目标是将目前功能还比较松散简陋的编译器包装为好用一点的ugo命令,这样更方便测试和遇到便于问题时定位。

3.6.1 最小µGo程序再前进一步

我们先看看本节可以编译执行的稍微复杂一点的程序:

package main

func main() {
	println(1)
	println(1000 + 123)
	println(40 + 2)
}

使用ugo程序直接执行:

$ ugo run ./hello.ugo 
1
1123
42

看起来还不错。

3.6.2 ugo的命令行界面

和Go社区习惯一致,使用-h查看帮助:

$ ugo -h
NAME:
   ugo - ugo is a tool for managing µGo source code.

USAGE:
   ugo [global options] command [command options] [arguments...]

VERSION:
   0.0.1

COMMANDS:
   run      compile and run µGo program
   build    compile µGo source code
   lex      lex µGo source code and print token list
   ast      parse µGo source code and print ast
   asm      parse µGo source code and print llvm-ir
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --debug, -d     set debug mode (default: false)
   --help, -h      show help (default: false)
   --version, -v   print the version (default: false)
$

这是基于cli包构建的命令行界面:run用于编译加执行、build构建本地可执行程序、lex输出词法分析结果、ast输出语法树结果、asm输出语法树翻译为LLVM汇编语言的结果。

3.6.3 实现ugo命令 - main函数

main函数通过cli包定义子命令和相关的参数:

package main

import "github.com/urfave/cli/v2"
import "github.com/wa-lang/ugo/build"

func main() {
	app := cli.NewApp()
	app.Name = "ugo"
	app.Usage = "ugo is a tool for managing µGo source code."
	app.Version = "0.0.1"

	app.Flags = []cli.Flag{
		&cli.StringFlag{Name: "goos", Usage: "set GOOS", Value: runtime.GOOS},
		&cli.StringFlag{Name: "goarch", Usage: "set GOARCH", Value: runtime.GOARCH},
		&cli.StringFlag{Name: "clang", Value: "", Usage: "set clang"},
		&cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Usage: "set debug mode"},
	}

首先是cli.NewApp()定义主命令对象,然后设置名字、提示信息、版本和参数。参数中goos和goarch用于指定目标平台(目前还没有用到),clang则用于支持用户自定义的路径,debug表示调试模式。

然后是定义全部的子命令:

	app.Commands = []*cli.Command{
		{
			Name:  "run",
			Usage: "compile and run µGo program",
			Action: func(c *cli.Context) error {
				ctx := build.NewContext(build_Options(c))
				output, _ := ctx.Run(c.Args().First(), nil)
				fmt.Print(string(output))
				return nil
			},
		},
		{
			Name:  "build",
			Usage: "compile µGo source code",
			Action: func(c *cli.Context) error {
				ctx := build.NewContext(build_Options(c))
				ctx.Build(c.Args().First(), nil, "a.out")
				return nil
			},
		},
		{
			Name:  "lex",
			Usage: "lex µGo source code and print token list",
			Action: func(c *cli.Context) error {
				ctx := build.NewContext(build_Options(c))
				tokens, comments, _ := ctx.Lex(c.Args().First(), nil)
				fmt.Println(tokens)
				fmt.Println(comments)
				return nil
			},
		},
		{
			Name:  "ast",
			Usage: "parse µGo source code and print ast",
			Flags: []cli.Flag{
				&cli.BoolFlag{Name: "json", Usage: "output json format"},
			},
			Action: func(c *cli.Context) error {
				ctx := build.NewContext(build_Options(c))
				f, err := ctx.AST(c.Args().First(), nil)
				if c.Bool("json") {
					fmt.Println(f.JSONString())
				} else {
					fmt.Println(f.String())
				}
				return nil
			},
		},
		{
			Name:  "asm",
			Usage: "parse µGo source code and print llvm-ir",
			Action: func(c *cli.Context) error {
				ctx := build.NewContext(build_Options(c))
				ll, _ := ctx.ASM(c.Args().First(), nil)
				fmt.Println(ll)
				return nil
			},
		},
	}

	app.Run(os.Args)
}

对应run、build、lex、ast和asm几个子命令,具体的实现是由build包的Context对象提供的对应方法实现。最后调用app.Run(os.Args)执行命令行。build_Options函数负责从全局参数解析信息,产生执行需要的上下文参数。

3.6.4 实现ugo命令 - build.Context对象

build.Context对象是当前执行命令需要的参数包装:

package build

type Option struct {
	Debug  bool
	GOOS   string
	GOARCH string
	Clang  string
}

type Context struct {
	opt  Option
	path string
	src  string
}

除了目标平台信息,还包含ugo代码的路径。

词法解析方法实现如下:

func (p *Context) Lex(filename string, src interface{}) (tokens, comments []token.Token, err error) {
	code, err := p.readSource(filename, src)
	if err != nil {
		return nil, nil, err
	}

	l := lexer.NewLexer(filename, code)
	tokens = l.Tokens()
	comments = l.Comments()
	return
}

语法树解析方法包装:

func (p *Context) AST(filename string, src interface{}) (f *ast.File, err error) {
	code, err := p.readSource(filename, src)
	if err != nil {
		return nil, err
	}

	f, err = parser.ParseFile(filename, code)
	if err != nil {
		return nil, err
	}

	return f, nil
}

产生LLVM汇编代码的方法包装:

func (p *Context) ASM(filename string, src interface{}) (ll string, err error) {
	code, err := p.readSource(filename, src)
	if err != nil {
		return "", err
	}

	f, err := parser.ParseFile(filename, code)
	if err != nil {
		return "", err
	}

	ll = new(compiler.Compiler).Compile(f)
	return ll, nil
}

Build和Run方法定义如下(实现细节就不展开了,具体方式可以参考代码):

func (p *Context) Build(
	filename string, src interface{}, outfile string,
) (output []byte, err error) {
	// ...
}

func (p *Context) Run(filename string, src interface{}) ([]byte, error) {
	// ...
}

辅助的readSource方法实现如下:

func (p *Context) readSource(filename string, src interface{}) (string, error) {
	if src != nil {
		switch s := src.(type) {
		case string:
			return s, nil
		case []byte:
			return string(s), nil
		case *bytes.Buffer:
			if s != nil {
				return s.String(), nil
			}
		case io.Reader:
			d, err := io.ReadAll(s)
			return string(d), err
		}
		return "", errors.New("invalid source")
	}

	d, err := os.ReadFile(filename)
	return string(d), err
}

这样我们就可以通过指定不同的GOOS和GOARCH实现交叉编译。


© 2021-2022 | 柴树杉 保留所有权利