62

仿照laravel-artisan实现简易go开发脚手架

 5 years ago
source link: https://studygolang.com/articles/14148?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

像Laravel-Artisan一样执行go命令

前言

作为一个 laravel 重度患者, artisan 是一个不可或缺的功能,可以说这是 laravel 的开发脚手架

可以快速创建需要的文件,加快开发速度

而我目前正在开发的 bingo 框架正是受到 laravel 启发,希望可以快速构建web应用

而一个脚手架是必不可少的东西,所以我实现了一个 bingo sword 工具

laravel-artisan实现思路

我曾经写过artisan的解析,链接在这里 laravel artisan 原理解析

简而言之,就是将 kernel.php 中注册的所有 commands 都实例化一次,然后比对 命令名,对于查找到的命令,调用 handle 方法执行即可

所以思路就有啦~

bingo sword 实现思路

先看图,随便画了画流程

ERzMvaa.png!web

下面直接上代码:

当命令行中输入 bingo sword make:command --name=MakeCommand

那么在 CLIRun() 方法中,获取参数

swordCmd := flag.NewFlagSet("sword", flag.ExitOnError) // bingo sword 命令

err := swordCmd.Parse(os.Args[2:])
swordConfig = os.Args[2:]
		
if swordCmd.Parsed() {
	cli.swordHandle(swordConfig)
}

此时接收到了需要的参数,然后调用 swordHanle 方法:

func (cli *CLI) swordHandle(args []string) {
	// 解析这个参数,将数据传入外部
	//fmt.Println(args)
	//获取env中的kernel路径
	//根据kernel去 go shell执行 go run xxx/kernel.go make:controller AdminController
	consoleKernelPath := bingo.Env.Get("CONSOLE_KERNEL_PATH")
	// 获取命令当前执行目录
	dir, _ := os.Getwd()

    // 拼接Kernel的
	consoleKernelAbsolutePath := dir + "/" + consoleKernelPath + "/Kernel.go"

	// 使用go shell 调用 go run xxx/Kernel.go arg1 arg2 arg3

	var tmpSlice = []string{"go", "run", consoleKernelAbsolutePath}

	args = append(tmpSlice, args...)

	// 先检查这个命令是否属于内部命令
	// arg第一个就是命令
	console := Console{}
	console.Exec(args[2:], InnerCommands)

	//[run /Users/silsuer/go/src/test/app/Console/Kernel.go aaa bbb ccc]
    // 执行 go run Kernel.go command:name
	cmd := exec.Command("go",args[1:]...)
	var out bytes.Buffer
	cmd.Stdout = &out

    // 开始执行命令
	if err := cmd.Start(); err != nil {
		panic(err)
	}

    // 等待命令执行完成
	if err := cmd.Wait(); err != nil {
		log.Fatal(err)
	}

    // 打印输出
	fmt.Println(out.String())
}

所以我们使用 bingo sword command:name --name=CommandName 实际上执行的是 go run app/Console/Kernel.go command:name --name=CommandName

可以查看 Kernel.go 的源码,实例化了一个 console 结构体,并调用了 Exec() 方法

这个方法:

func (console *Console) Exec(args []string, commands []interface{}) {

    // 将参数封装成了input对象
	input := console.initInput(args)
	// 遍历传入的commands数组(这是在kernel里注册的函数)
	for _, command := range commands {

		// 先做检查,查找对应的命令名
		commandValue := reflect.ValueOf(command)
		// 初始化命令结构体
		initCommand(&commandValue)

		// 映射期望参数与实际输入参数(验证参数输入是否正确)
		target := checkParams(command, &input)
		// 不是这个命令,跳过这个命令
		if target == false {
			continue
		}
		// 获得输入和输出并准备作为参数传入Handle方法中
		var params = []reflect.Value{reflect.ValueOf(input), reflect.ValueOf(Output{})}
		commandValue.MethodByName("Handle").Call(params)
	}
}

如果传入的命令名没有对上的话,会跳过这次循环,否则会执行这个命令

值得注意的是,如果命令名一样的话,这些命令都会执行,如果命令中会报错的话,使用 panic()

只会抛出 bing/cli/cli_sword.goswordHandle() 中的那行 panic 错误的代码

目前实现的功能

目前我只是简单的实现了创建命令的命令,安装好bingo后,在控制台输入 bingo sword make:command --name=HelloWorld

会在 app/Console/Commands 目录下生成一个 HelloWorld.go 文件,内容是:

package Commands

import (
	"github.com/silsuer/bingo/cli"
)

type HelloWorld struct {
	cli.Command
	Name        string
	Description string
	Args        map[string]string
}

// 设置命令名
func (c *HelloWorld) SetName() {
	c.Name = "command:name"
}

// 设置命令所需参数
func (c *HelloWorld) SetArgs() {
	c.Args = make(map[string]string)
	c.Args["name"] = ""
}

// 设置命令描述
func (c *HelloWorld) SetDescription() {
	c.Description = "the command description."
}

// 设置命令实现的方法
func (c *HelloWorld) Handle(input cli.Input, output cli.Output) {
	
}

我们只需要在其中修改我们要更改的信息就可以了,例如更新handle方法为 output.Info("Hello,World!") ,更新命令名是 hello:world

然后无需使用 go build 等方式重新编译,直接在命令行使用 bingo sword hello:world ,将在控制台打印输出 Hello,World

当然,这个工具只是为了加速开发,是 bingo 的一个模块,你随时可以把它拆出来作为独立模块使用

知识点

  1. 使用go执行系统命令

cmd := exec.Command("go","run","app/Console/Kernel.go","command:name")

使用 exec.Command 将会生成一个 cmd 对象,执行 cmd.Start() 即可执行命令,这并不会阻塞进程,如果需要获得结果

需要使用 cmd.Wait() 等待执行完成,再获取标准输出,当然也可以直接使用 cmd.Run() ,这行代码会阻塞进程,直到命令完成

如果获取标准输出呢?

var out bytes.Buffer
 
cmd.Stdout = &out

将cmd的标准输出指向我们设定好的一个 buffer 即可

  1. 使用反射调用结构体的方法

使用 commandValue := reflect.ValueOf(command) 获取这个结构体的 Value

使用 commandValue.NumMethod() 可以获取这个结构体的方法数量,如果传入的 command 是一个结构体的指针的话

得到的方法数量包括了针对结构体的方法数量和针对结构体指针的方法数量之和,如果传入的是一个结构体对象,那么得到的

方法数量只是包括了针对结构体的方法数量

使用 commandValue.MethodByName("Handle").Call(params) 调用结构体对应的方法

  1. 在控制台输出带颜色的文字

和shell类似,只需要将控制台输出的信息使用一些颜色字符包裹起来即可

//其中0x1B是标记,[开始定义颜色,1代表高亮,48代表黑色背景,32代表绿色前景,0代表恢复默认颜色。
	fmt.Printf("\n %c[0;48;32m%s%c[0m\n\n", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]"+content, 0x1B)

最后贴一下项目链接 bingo ,欢迎star,更欢迎PR,欢迎提意见~~~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK