Jiahonzheng's Blog

Golang CLI 应用 - Selpg

字数统计: 1.8k阅读时长: 7 min
2019/09/25 Share

在本文中,我们参照 开发 Linux 命令行实用程序 的设计,使用 Golang 语言替代 C 语言构建 selpg 应用程序。

项目地址:github.com/Jiahonzheng/Selpg

设计说明

Go Modules

我们使用 Go Modules 作为项目的包管理工具,通过执行 go mod init github.com/Jiahonzheng/Selpg 命令,我们完成项目的初始化工作,此时项目文件夹内新增了 go.mod 文件。

1
2
3
4
5
6
# 初始化项目
go mod init github.com/Jiahonzheng/Selpg
# 构建项目
go build
# 运行项目
./Selpg

参数处理

安装依赖

由于课程作业的要求,我们使用 github.com/spf13/pflag 作为参数标识包,我们通过执行以下命令添加 pflag 包。

1
go get github.com/spf13/pflag

在添加完包依赖后,项目文件夹内会新增 go.sum 依赖版本控制文件。PS:使用 Go Modules 管理包依赖,是非常让人舒心的事情

全局变量

现在,我们使用 pflag 来实现 CLI 应用的参数显示与处理,使其满足 Unix 命令行规范。首先,我们声明了以下全局变量。

1
2
3
4
5
6
7
var (
startPage = pflag.IntP("start_page", "s", -1, "The page to start printing at.")
endPage = pflag.IntP("end_page", "e", -1, "The page to end printing at.")
pageLen = pflag.IntP("page_len", "l", 72, "The number of lines in a page.")
pageType = pflag.BoolP("page_type", "f", false, "If this flag is used, it is delimited by '\\f'.")
printDest = pflag.StringP("print_dest", "d", "", "Use a printer to print the result.")
)

为了使得 pflag 能够解析用户参数,我们需要在 main 函数内添加 pflag.Parse() 调用。

1
2
3
4
5
6
7
8
package main

// ...

func main() {
pflag.Parse()
// ...
}

构建程序,并执行 ./Selpg -h ,我们即可查看到以下的帮助页面。

检验

我们在 validateArgs 函数中,实现了用户输入参数的检验。

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
// It validates the arguments.
func validateArgs() error {
if *startPage == -1 || *endPage == -1 {
return errors.New("arguments are not enough")
}
if *startPage < 1 || *startPage > (MaxInt-1) {
return errors.New("start page is not valid")
}
if *endPage < 1 || *endPage > (MaxInt-1) {
return errors.New("end page is not valid")
}
if *startPage > *endPage {
return errors.New("start page cannot be greater than end page")
}
if !*pageType && (*pageLen < 1 || *pageLen > (MaxInt-1)) {
return errors.New("line number is not valid")
}
if *pageType && *pageLen != 72 {
return errors.New("-f and -lNumber cannot be set at the same time")
}
if pflag.NArg() > 1 {
return errors.New("arguments are too many")
}
return nil
}

我们检验了所有标识的合法性,包括:

  • 必需的 -s-e 参数是否被设置。
  • 标识值是否合法。
  • -l-f 的参数互斥,即通过行数分页和通过分页符分页,是否被同时设置。
  • 参数数量是否过多。

数据分页

在对参数标识进行检验后,我们需要根据标识对数据进行分页,并输出结果。我们在函数 pagination 中,实现了数据分页的功能。

Reader

首先,我们根据输入方式,建立合适的 reader 。对于标准输入的模式,即没有额外参数的情况,我们使用 os.Stdin 构建 bufio.Reader ;对于文件输入的模式,即有额外参数的情况,我们使用 os.File 构建 bufio.Reader

1
2
3
4
5
6
7
8
9
10
11
12
13
var reader *bufio.Reader
if pflag.NArg() == 0 {
// Accept stdin as the input.
reader = bufio.NewReader(os.Stdin)
} else {
// Accept the file as the input.
file, err := os.Open(pflag.Arg(0))
if err != nil {
return err
}
defer file.Close()
reader = bufio.NewReader(file)
}

Delimiter

我们的分页方式有两种:按行数分页按分页符分页。为此,我们有了 delimiter 变量。

1
2
3
4
5
6
7
// Set the delimiter.
var delimiter byte
if *pageType {
delimiter = '\f'
} else {
delimiter = '\n'
}

ReadBytes

我们在迭代中,不断调用 reader.ReadBytes 读取 delimiter 之前的数据,同时更新行计数器和页计数器,实现对数据的分页。

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
// Use strings.Builder for performance reason.
var result strings.Builder
pages := 1
lines := 0
for {
sub, err := reader.ReadBytes(delimiter)
if err == io.EOF {
break
} else if err != nil {
return err
}
if *pageType {
pages++
} else {
lines++
if lines > *pageLen {
pages++
lines = 1
}
}
if pages >= *startPage && pages <= *endPage {
// Here strings.Builder helps improve the performance.
result.Write(sub)
}
}

注意到,我们使用了 strings.Builder 作为字符串的连接函数,这是由于在 Golang 中,字符串是不可变的,如果通过运算符 + 连接两字符串,会出现不必要的内存复制,因此,为了减少字符串拼接的性能开销,我们使用了由 unsafe 实现的 strings.Builder 。有关 Golang 字符串拼接的内容,可在我的另一篇文章中查阅。

我们知道,用户输入的起始页号和终止页号可能超过实际页数。为此,我们在对数据进行分页后,需要对用户输入再次进行判断。

1
2
3
4
5
6
7
// Border detection.
if pages < *startPage {
return fmt.Errorf("start page %d is greater than total pages %d", *startPage, pages)
}
if pages < *endPage {
return fmt.Errorf("end page %d is greater than total pages %d", *endPage, pages)
}

Output

在完成对数据的分页后,我们需要按照用户参数,将分页结果输出至指定位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Set the output destination.
if *printDest == "" {
fmt.Printf("%s", result.String())
} else {
cmd := exec.Command("lp", "-d"+*printDest)
cmd.Stdin = strings.NewReader(result.String())
var b bytes.Buffer
cmd.Stderr = &b
err := cmd.Run()
if err != nil {
return fmt.Errorf("%s %s", err, b.Bytes())
}
}

若用户未设置 -d 参数时,结果将输出至标准输出;否则,结果将被输出至指定打印机。注意到,我们通过 os/exec 下的 Command 方法,执行了 lp 命令,实现与打印机的通讯。

主函数

由于我们使用了两个函数来封装参数处理数据分页的功能,这使得我们的主函数 main 显得十分清爽。

1
2
3
4
5
6
7
8
9
10
func main() {
// Parse the flag arguments.
pflag.Parse()
// Validate the arguments.
err := validateArgs()
handleError(err)
// Make the pagination.
err = pagination()
handleError(err)
}

注意到,我们使用了 handleError 方法,来封装对错误的处理,其具体实现如下。

1
2
3
4
5
6
// It helps to handle the error.
func handleError(err error) {
if err != nil {
log.Fatalf("Error: %s", err)
}
}

使用说明

安装

我们可通过执行以下命令,完成 Selpg 的安装。

1
go get github.com/Jiahonzheng/Selpg

运行

Selpg 的运行参数说明如下。

1
selpg -sNumber -eNumber [-lNumber/-f] [-dDestination] [file_name]
  • -sNumber 表明起始页号(从 1 开始),是必需参数,例如 “-s5” 表示打印将从第 5 页初开始。
  • -eNumber 表明终止页号(从 1 开始),是必需参数,例如 “-e10” 表示打印将在第 10 页末结束。
  • -lNumber 和 -f 为互斥的可选参数。-lNumber 表示按行数分页,是缺省设置。-f 表示按分页符分页
  • -dDestination 表明结果将输出至打印机,是可选参数。
  • file_name 表明输入类型为文件输入,输入类型的缺省值是标准输入

示例

我们使用 test_input_generator.sh 生成测试用的输入文件,其能够生成 1 至 3600 的自然数,并将其写入到 test_input.txt

1
2
3
for i in {1..3600}; do
echo $i >>test_input.txt
done
  • 示例 1

    1
    ./Selpg -s4 -e5 -l2 ./test_input.txt

    运行结果为:

    1
    2
    3
    4
    7
    8
    9
    10
  • 示例 2

    1
    cat test_input.txt | ./Selpg -s4 -e5 -l2

    运行结果为:

    1
    2
    3
    4
    7
    8
    9
    10
  • 示例 3

    1
    ./Selpg -s4 -e5 -l2 ./test_input.txt > test_output.txt

    执行 cat test_output.txt 后,可查看其内容为:

    1
    2
    3
    4
    7
    8
    9
    10
  • 示例 4

    1
    ./Selpg -s4 -e5 -l2 ./test_input.txt | cat

    运行结果为:

    1
    2
    3
    4
    7
    8
    9
    10
  • 示例 5

    1
    selpg -s4 -e5 -dlp1 ./test_input.txt

    运行结果为(电脑未安装打印机):

    1
    Error: exit status 1 lp: No such file or directory
CATALOG
  1. 1. 设计说明
    1. 1.1. Go Modules
    2. 1.2. 参数处理
      1. 1.2.1. 安装依赖
      2. 1.2.2. 全局变量
      3. 1.2.3. 检验
    3. 1.3. 数据分页
      1. 1.3.1. Reader
      2. 1.3.2. Delimiter
      3. 1.3.3. ReadBytes
      4. 1.3.4. Output
    4. 1.4. 主函数
  2. 2. 使用说明
    1. 2.1. 安装
    2. 2.2. 运行
    3. 2.3. 示例