golang和C的输出格式化对齐

写代码那么多年,对代码,输出日志有一种近似洁癖的要求。对于不整齐、不整洁、命名混乱的代码,真心看不下去,总会想办法去整顿——或用工具,或人工。曾因这个原因耽误时间,虽然想着改,但一时也改不了多少。今年唯一例外的,应该我手上维护的那套98年开始写的 delphi 工程。

本文从小处着手,单说一些输出日志的对齐方法。

在 golang 中,常用的命令行库为 cobra,内部实现了帮助信息的对齐。以 docker 为例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# docker --help
Usage: docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
--config string Location of client config files (default "/root/.docker")
-D, --debug Enable debug mode

Management Commands:
builder Manage builds
config Manage Docker configs
container Manage containers
context Manage contexts

Commands:
attach Attach local standard input, output, and error streams to a running container
build Build an image from a Dockerfile
commit Create a new image from a container's changes

在自己实现的小型命令终端程序框架时,也使用了 cobra ,但做了一些精简,比如子命令的命令,按官方方法是使用多级子命令形式,但觉得麻烦,将“子命令的命令”作为子命令的参数处理,但如此一来,有些输出信息就不整齐了。
为解决问题,参考了 cobra 源码,提炼出核心代码,最终达到预期目标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ./cmdtool.exe test
test...

Available Commands:
foo just a foo help info
watch watch config file

Usage:
cmdtool.exe test [flags]

Examples:
example comming up...


Flags:
-h, --help help for test
-m, --mode int set the test mode

Global Flags:
-c, --config string config file (config.yaml)
-o, --output string specify the output file name
-p, --print verbose output

其中,Available Commands:为自定义函数中的输出,将参数作为子命令的命令。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// rpad adds padding to the right of a string.
func rpad(s string, padding int) string {
truetemplate := fmt.Sprintf("%%-%ds", padding)
truereturn fmt.Sprintf(template, s)
}

//返回字符串的
func GetHelpInfo(theCmd []conf.UserCmdFunc) (ret string) {
truevar cmdMaxLen int = 0
trueret = fmt.Sprintf("Available Commands:\n");

truefor _, item := range theCmd {
truetruenameLen := len(item.Name)
truetrueif nameLen > cmdMaxLen {
truetruetruecmdMaxLen = nameLen
truetrue}
true}
true
truefor _, item := range theCmd {
truetrueret += fmt.Sprintf(" %v %v\n", rpad(item.Name, cmdMaxLen), item.ShortHelp)
true}
true
truereturn
}

rpad为组装格式化字符串(因此会出现多个%-为左对齐)函数,输入的padding为一列数据的最大值,该值是遍历用户命令列表的名称,获取得到 的最大长度。用户命令列表如下:

1
2
3
4
5
6
7
8
var theCmd = []conf.UserCmdFunc{
conf.UserCmdFunc {
Name: "foo",
ShortHelp: "just a foo help info",
Func: foo,
},
conf.UserCmdFunc {"watch", "watch config file", testWatch,},
}

依据上述思路,在C中也容易实现类似的对齐需求。示例如下:

1
2
3
4
5
char fmt[128] = {0};
sprintf(fmt, "autoTestFee %%s%%-%ds -> %%s%%-%ds vehicleType %%s payFee %%-6d outFee %%-9.2f realFee %%-6d\n",
nameLen1+2, nameLen2+2);
// printf("%s\n", fmt);
printf(fmt, item->en, item->enName, item->ex, item->exName, type, payFee, outFee, realFee);

代码中使用 sprintf 组装格式化字符串,该语句转化后如下:

1
"autoTestFee %s%-10s -> %s%-9s vehicleType %s payFee %-6d outFee %-9.2f realFee %-6d\n"  # 数字为示例

其中 nameLen1 和 nameLen2 分别为 enName 和 exName 列表的最大值(方便对齐)。
结果如下:

1
2
autoTestFee 100(hello)     -> 200(foobarrrrbbbbbbbbbbb) vehicleType 未名 payFee 2018   outFee 20.00     realFee 2003  
autoTestFee 101(hello) -> 201(f) vehicleType 未名 payFee 2019 outFee 20.00 realFee 2003

这样,在搜索日志时就容易分辨了。

经测试,如果字符串含有中文且中文长度不等(或中英混合),对齐还是有问题,待后续解决。