1、命令行工具概述
日常命令行操作,相对应的众多命令行工具是提高生产力的必备工具,我在之前的文章我的生产力工具推荐-终端01篇中有推荐过一些我常用的基于terminal
终端的命令行cli
工具
鼠标能够让用户更容易上手,降低用户学习成本。 而对于开发者,键盘操作模式能显著提升生产力,还有在一些专业工具中, 大量使用快捷键代替繁琐的鼠标操作,能够使开发人员更加专注于工作,提高效率,因为键盘操作模式更容易产生肌肉记忆
举个栗子。我司业务研发,前些年在我们的强力推动下(被迫)转向使用了git
作为版本控制,开始使用的是图形化“小乌龟”工具。后续出现几次问题解决起来较麻烦后,推荐其使用原生的git
命令行。如今,使用git
命令行操作版本控制可谓 “一顿操作猛如虎......”
命令行(键盘)操作在很大程度上可以提高工作效率,与之相对应的是鼠标(触屏等)操作,这两种模式是目前的主流人机交互方式
设计一款命令行工具的开发语言可以选择原始的shell
、甚至是更原始的语言C
,更为容易上手且功能更多的有node
、python
、golang
本文是基于golang
开发命令行工具的开篇,主要是基于golang
原生内置的、轻量的flag
包实现,用golang
设计命令行工具而不用shell
、python
的原因这里就不做论述了
2、flag包介绍
flag
包用来解析命令行参数
相比简单的使用os.Args
来获取命令行参数,flag
可以实现按照更为通用的命令行用法,例如mysql -u root -p 123456
。其中mysql
是命令行的名称即这个命令,-u
和-p
分别是这个命令的两个参数:用户名和密码,后面接着的是对应的参数值,有了参数的声明之后,两个参数可以互换位置,参数值也可以选填或按照缺省(默认)值进行指定
flag
的详细用法可参考flag包文档
flag
包支持的命令行参数的类型有bool
、int
、int64
、uint
、uint64
、float
float64
、string
、duration
即布尔值、整型、浮点型、字符串、时间段类型
3、flag包命令行参数的定义
定义flag
命令行参数,用来接收命令行输入的参数值,一般有以下两种方法
- flag.TypeVar():先定义参数(实际上是指针),再定义
flag.TypeVar
将命令行参数存储(绑定)到前面参数的值的指针(地址)
var name string | |
var age int | |
var height float64 | |
var graduated bool | |
// &name 就是接收用户命令行中输入的-n后面的参数值 | |
// 返回值是一个用来存储name参数的值的指针/地址 | |
// 定义string类型命令行参数name,括号中依次是变量名、flag参数名、默认值、参数说明 | |
flag.StringVar(&name, "n", "", "name参数,默认为空") | |
// 定义整型命令行参数age | |
flag.IntVar(&age,"a", 0, "age参数,默认为0") | |
// 定义浮点型命令行参数height | |
flag.Float64Var(&height,"h", 0, "height参数,默认为0") | |
// 定义布尔型命令行参数graduated | |
flag.BoolVar(&graduated,"g", false, "graduated参数,默认为false") |
- flag.Type():用短变量声明的方式定义参数类型及变量名
// 定义string类型命令行参数name,括号中依次是flag参数名、默认值、参数说明 | |
namePtr := flag.String("n", "", "name参数,默认为空") | |
// 定义整型命令行参数age | |
age := flag.Int("a", 0, "age参数,默认为0") | |
// 定义浮点型命令行参数height | |
height := flag.Float64("h", 0, "height参数,默认为0") | |
// 定义布尔型命令行参数graduated | |
graduated:= flag.Bool("g", false, "graduated参数,默认为false") |
4、flag包命令行参数解析
固定用法,定义好参数后,通过调用flag.Parse()
来对命令行参数进行解析写入注册的flag
里,进而解析获取参数值,通过查看源码中也是调用的os.Args
源码路径go/src/flag/flag.go
// Parse parses the command-line flags from os.Args[1:]. Must be called | |
// after all flags are defined and before flags are accessed by the program. | |
func Parse() { | |
// Ignore errors; CommandLine is set for ExitOnError. | |
CommandLine.Parse(os.Args[1:]) | |
} |
进而查看Parse
方法的源码
func (f *FlagSet) Parse(arguments []string) error { | |
f.parsed = true | |
f.args = arguments | |
for { | |
seen, err := f.parseOne() | |
if seen { | |
continue | |
} | |
if err == nil { | |
break | |
} | |
switch f.errorHandling { | |
case ContinueOnError: | |
return err | |
case ExitOnError: | |
if err == ErrHelp { | |
os.Exit(0) | |
} | |
os.Exit(2) | |
case PanicOnError: | |
panic(err) | |
} | |
} | |
return nil | |
} |
真正解析参数的是parseOne
方法(这里省略源码),结论是
- 当遇到单独的一个 "-" 或不是 "-" 开始时,会停止解析
- 遇到连续的两个 "-" 时,解析停止
- 在终止符"-"之后停止
解析参数时,对于参数的指定方式一般有"-"、"--"、以及是否空格等方式,组合下来有如下几种方式
-flag xxx
空格和一个-符号
--flag xxx
空格和两个-符号
-flag=xxx
等号和一个-符号
--flag=xxx
等号和两个-符号
其中,-flag xxx
方式最为常用,如果参数是布尔型,只能用等号方式指定
5、flag包命令行帮助
flag
包默认会根据定义的命令行参数,在使用时如果不输入参数就打印对应的帮助信息
这样的帮助信息我们可以对其进行覆盖去改变默认的Usage
package main | |
import ( | |
"flag" | |
"fmt" | |
) | |
func main() { | |
var host string | |
var port int | |
var verbor bool | |
var help bool | |
// 绑定命令行参数与变量关系 | |
flag.StringVar(&host, "H", "127.0.0.1", "ssh host") | |
flag.IntVar(&port, "P", 22, "ssh port") | |
flag.BoolVar(&verbor, "v", false, "detail log") | |
flag.BoolVar(&help, "h", false, "help") | |
// 自定义-h | |
flag.Usage = func() { | |
fmt.Println(` | |
Usage: flag [-H addr] [-p port] [-v] | |
Options: | |
`) | |
flag.PrintDefaults() | |
} | |
// 解析命令行参数 | |
flag.Parse() | |
if help { | |
flag.Usage() | |
} else { | |
fmt.Println(host, port, verbor) | |
} | |
} | |
/* | |
➜ go run flag_args.go -h | |
Usage: flag [-H addr] [-p port] [-v] | |
Options: | |
-H string | |
ssh host (default "127.0.0.1") | |
-P int | |
ssh port (default 22) | |
-h help | |
-v detail log | |
*/ |
6、flag定义短参数和长参数
简单来说,短参数和长参数,就是例如我们在使用某些命令时,查看命令版本可以输入-V
,也可以输入--version
。这种情况下,flag
并没有默认支持,但是可以通过可以两个选项共享同一个变量来实现,即通过给某个相同的变量设置不同的选项,参数在初始化的时候其顺序是不固定的,因此还需要保证其拥有相同的默认值
package main | |
import ( | |
"fmt" | |
"flag" | |
) | |
var logLevel string | |
func init() { | |
const ( | |
defaultLogLevel = "DEBUG" | |
usage = "set log level" | |
) | |
flag.StringVar(&logLevel, "log_level", defaultLogLevel, usage) | |
flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)") | |
} | |
func main() { | |
flag.Parse() | |
fmt.Println("log level:", logLevel) | |
} |
通过const
声明公共的常量,并在默认值以及帮助信息中去使用,这样就可以实现了
7、示例
实现计算字符串或目录下递归计算文件md5
的命令,类似linux
的md5sum
命令
其中利用bufio
分批次读取文件,防止文件过大时造成资源占用高
package main | |
import ( | |
"bufio" | |
"crypto/md5" | |
"flag" | |
"fmt" | |
"io" | |
"os" | |
"strings" | |
) | |
func md5reader(reader *bufio.Reader) string { // | |
hasher := md5.New() // 定义MD5 hash计算器 | |
bytes := make([]byte, 1024*1024*10) // 分批次读取文件 | |
for { | |
n, err := reader.Read(bytes) | |
if err != nil { | |
if err != io.EOF { | |
return "" | |
} | |
break | |
} else { | |
hasher.Write(bytes[:n]) | |
} | |
} | |
return fmt.Sprintf("%x", hasher.Sum(nil)) | |
} | |
func md5file(path string) (string, error) { | |
file, err := os.Open(path) | |
if err != nil { | |
return "", err | |
} else { | |
defer file.Close() | |
return md5reader(bufio.NewReader(file)), nil | |
} | |
} | |
func md5str(txt string) (string, error) { | |
return md5reader(bufio.NewReader(strings.NewReader(txt))), nil | |
//return fmt.Sprintf("%x", md5.Sum([]byte(txt))) | |
} | |
func main() { | |
txt := flag.String("s", "", "md5 txt") | |
path := flag.String("f", "", "file path") | |
help := flag.Bool("h", false, "help") | |
flag.Usage = func() { | |
fmt.Println(` | |
Usage: md5 [-s 123abc] [-f path] | |
Options: | |
`) | |
flag.PrintDefaults() | |
} | |
flag.Parse() | |
if *help || *txt == "" && *path == "" { | |
flag.Usage() | |
} else { | |
var md5 string | |
var err error | |
if *path != "" { | |
md5, err = md5file(*path) | |
} else { | |
md5, err = md5str(*txt) | |
} | |
if err != nil { | |
fmt.Println(err) | |
} else { | |
fmt.Println(md5) | |
} | |
} | |
} |
编译生成二进制文件
➜ go build -o md5go -x md5_bufio.go | |
➜ ll md5go | |
-rwxr-xr-x 1 ssgeek staff 1.9M Oct 2 00:54 md5go |
测试使用
➜ ./md5go -h | |
Usage: md5 [-s 123abc] [-f path] | |
Options: | |
-f string | |
file path | |
-h help | |
-s string | |
md5 txt | |
➜ ./md5go -s 123456 | |
e10adc3949ba59abbe56e057f20f883e | |
➜ ./md5go -f md5_bufio.go | |
8607a07cbb98cec0e9abe14b0db0bee6 |
Just here,see you ~