精华内容
下载资源
问答
  • 一种编程语言是否易用,很大程度上,取决于开发命令行程序的能力。本篇文章主要介绍了Node.js 命令行程序开发教程,有兴趣的可以了解一下
  • 基于Golang的CLI 命令行程序开发

    千次阅读 2020-10-11 17:17:57
    基于Golang的CLI 命令行程序开发 【阅读时间:约15分钟】一. CLI 命令行程序概述二. 系统环境&项目介绍&开发准备1.系统环境2.项目介绍3.开发准备三.具体程序设计及Golang代码实现1.selpg的程序结构2.导入的...




    一. CLI 命令行程序概述

    CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:

    Linux提供了cat、ls、copy等命令与操作系统交互;
    go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
    容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
    git、npm等也是大家比较熟悉的工具。

    尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。


    二. 系统环境&项目介绍&开发准备

    1.系统环境

    操作系统:CentOS7
    硬件信息:使用virtual box配置虚拟机(内存3G、磁盘30G)
    编程语言:GO 1.15.2

    2.项目介绍

    本项目的开发主要基于IBM Developer社区的C语言程序(https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html),出于熟悉golang语言的目的,笔者主要的工作只是将其翻译为golang格式,其中还使用了部分库,如os和pflag,再次感谢原作者及开源代码工作者。

    项目完成后的运行效果与CLI 命令行程序一致,一个简单的输出文本第一页20行的内容的例子如下:
    在这里插入图片描述

    3.开发准备

    ①首先下载上文的C语言源码(点击下载
    ②安装并使用 pflag 替代 goflag 以满足 Unix 命令行规范,此处出于篇幅考虑,只在后面的函数介绍时给出部分使用教程,详细的pflag 使用教程可见【六. References. 1. Golang之使用Flag和Pflag】
    ③将C语言源码翻译为golang语言


    三.具体程序设计及Golang代码实现

    1.selpg的程序结构

    selpg的程序结构非常简单,主要有以下组成:
    ①sp_args结构
    ②main函数
    ③process_args函数
    ④process_input函数
    ⑤usage函数

    2.导入的库

    主要要导入的库有:
    ①bufio:用于文件的读写
    ②io:用于文件读写、读环境变量
    ③pflag:用于解释命令行参数,替代 goflag 以满足 Unix 命令行规范

    /*================================= includes ======================*/
    
    package main
    
    import (
    	"bufio"
    	"fmt"
    	"io"
    	"os"
    	"os/exec"
    
    	"github.com/spf13/pflag"
    )
    

    3.sp_args结构体

    sp_args结构体是用于记录数据的结构体,分别记录着开始页码,结束页码,文件名,每页大小,页的类型和打印输出位置等信息。

    /*================================= types =========================*/
    
    type sp_args struct {
    	start_page  int
    	end_page    int
    	in_filename string
    	page_len    int  /* default value, can be overriden by "-l number" on command line */
    	page_type   bool /* 'l' for lines-delimited, 'f' for form-feed-delimited */
    	/* default is 'l' */
    	print_dest string
    }
    

    4.全局变量

    全局变量共有两个:
    ①progname是程序名,在输出错误信息时有用;
    ②用 INT_MAX 检查一个数是否为有效整数,由于golang没有预定义的INT_MAX,此处用别的方式来手动实现

    /*================================= globals =======================*/
    
    var progname string                /* program name, for error messages */
    const INT_MAX = int(^uint(0) >> 1) //golang需要手动声明INT_MAX
    
    

    4.main函数

    main函数作为程序的入口,给出了整个程序的大概运行过程。
    ①首先进行sp_args变量和progname的初始化,其中主要的默认属性为开始页码和结束页码均为1,每页长度为20行,不可用用换页符换页
    ②然后调用process_args函数来处理输入时的各种参数错误
    ③最后才调用process_input函数来执行输入的参数。

    /*================================= main()=== =====================*/
    
    func main() {
    
    	var sa sp_args
    	sa.start_page = 1
    	sa.end_page = 1
    	sa.in_filename = ""
    	sa.page_len = 20 //默认20行一页
    	sa.page_type = false
    	sa.print_dest = ""
    
    	/* save name by which program is invoked, for error messages */
    	progname = os.Args[0]
    
    	process_args(len(os.Args), &sa)
    	process_input(sa)
    }
    

    5.process_args函数

    process_args函数用于处理输入时的各种参数错误。
    ①首先通过pflag绑定各参数和usage函数
    ②然后判断各种参数的错误即可,比如起始页码是负数,终止页码小于起始页码等情况,具体的错误情况在代码中已给出注释
    ③当发生错误,首先通过pflag.usage函数输出正确的指令参数格式来提醒用户,并通过os.Exit函数退出程序

    /*================================= process_args() ================*/
    
    func process_args(ac int, psa *sp_args) {
    	//指令格式:selpg -sstart_page -eend_page [-lline | -f ] [-d dstFile] filename
    	//使用pflag绑定各参数, psa初始化
    	pflag.Usage = usage
    	pflag.IntVarP(&psa.start_page, "start_page", "s", 1, "Start page")
    	pflag.IntVarP(&psa.end_page, "end_page", "e", 1, "End page")
    	pflag.IntVarP(&psa.page_len, "page_len", "l", 20, "Lines per page")
    	pflag.BoolVarP(&psa.page_type, "page_type", "f", false, "Page type")
    	pflag.StringVarP(&psa.print_dest, "dest", "d", "", "Destination")
    	pflag.Parse()
    
    	/* check the command-line arguments for validity */
    	if ac < 3 { /* Not enough args, minimum command is "selpg -sstartpage -eend_page"  */
    		fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
    		pflag.Usage()
    		os.Exit(1)
    	}
    
    	/* handle 1st arg - start page */
    	temp := os.Args[1]
    	if temp[0:2] != "-s" { 
    		fmt.Fprintf(os.Stderr, "%s: 1st arg should be -sstart_page\n", progname)
    		pflag.Usage()
    		os.Exit(2)
    	}
    
    	if psa.start_page < 1 || psa.start_page > (INT_MAX-1) {
    		fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, psa.start_page)
    		pflag.Usage()
    		os.Exit(3)
    	}
    
    	/* handle 2nd arg - end page */
    	temp = os.Args[2]
    	if temp[0:2] != "-e" {
    		fmt.Fprintf(os.Stderr, "%s: 2nd arg should be -eend_page\n", progname)
    		pflag.Usage()
    		os.Exit(4)
    	}
    
    	if psa.end_page < 1 || psa.end_page > (INT_MAX-1) || psa.end_page < psa.start_page {
    		fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname, psa.end_page)
    		pflag.Usage()
    		os.Exit(5)
    	}
    
    	/* now handle optional args */
    	//使用pflag,selpg.c的while+switch可去掉
    	if psa.page_len != 5 {
    		if psa.page_len < 1 {
    			fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, psa.page_len)
    			pflag.Usage()
    			os.Exit(6)
    		}
    	}
    
    	if pflag.NArg() > 0 { /* there is one more arg */
    		psa.in_filename = pflag.Arg(0)
    		/* check if file exists */
    		file, err := os.Open(psa.in_filename)
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname, psa.in_filename)
    			os.Exit(7)
    		}
    		/* check if file is readable */
    		file, err = os.OpenFile(psa.in_filename, os.O_RDONLY, 0666)
    		if err != nil {
    			if os.IsPermission(err) {
    				fmt.Fprintf(os.Stderr, "%s: input file \"%s\" exists but cannot be read\n", progname, psa.in_filename)
    				os.Exit(8)
    			}
    		}
    		file.Close()
    	}
    
    }
    

    6.process_args函数

    process_input函数用于执行输入的参数,执行文件读写和输出到屏幕等操作。其中由于没有打印机,转而使用cat命令测试。

    /*================================= process_input() ===============*/
    
    func process_input(sa sp_args) {
    	var fin *os.File        /* input stream */
    	var fout io.WriteCloser /* output stream */
    	var c byte              /* to read 1 char */
    	var line string
    	var line_ctr int /* line counter */
    	var page_ctr int /* page counter */
    	var err error
    	cmd := &exec.Cmd{}
    
    	/* set the input source */
    	if sa.in_filename == "" {
    		fin = os.Stdin
    	} else {
    		fin, err = os.Open(sa.in_filename)
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "%s: could not open input file \"%s\"\n", progname, sa.in_filename)
    			os.Exit(9)
    		}
    	}
    
    	/* set the output destination */
    	if sa.print_dest == "" {
    		fout = os.Stdout
    	} else {
    		cmd = exec.Command("cat") //由于没有打印机,使用cat命令测试
    		cmd.Stdout, err = os.OpenFile(sa.print_dest, os.O_WRONLY|os.O_TRUNC, 0600)
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "%s: could not open output file \"%s\"\n", progname, sa.print_dest)
    			os.Exit(10)
    		}
    
    		fout, err = cmd.StdinPipe()
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "%s: could not open pipe to \"%s\"\n", progname, sa.print_dest)
    			os.Exit(11)
    		}
    		cmd.Start()
    	}
    
    	/* begin one of two main loops based on page type */
    	rd := bufio.NewReader(fin)
    	if sa.page_type == false {
    		line_ctr = 0
    		page_ctr = 1
    		for true {
    			line, err = rd.ReadString('\n')
    			if err != nil { /* error or EOF */
    				break
    			}
    			line_ctr++
    			if line_ctr > sa.page_len {
    				page_ctr++
    				line_ctr = 1
    			}
    			if page_ctr >= sa.start_page && page_ctr <= sa.end_page {
    				fmt.Fprintf(fout, "%s", line)
    			}
    		}
    	} else {
    		page_ctr = 1
    		for true {
    			c, err = rd.ReadByte()
    			if err != nil { /* error or EOF */
    				break
    			}
    			if c == '\f' {
    				page_ctr++
    			}
    			if page_ctr >= sa.start_page && page_ctr <= sa.end_page {
    				fmt.Fprintf(fout, "%c", c)
    			}
    		}
    		fmt.Print("\n") 
    	}
    
    	/* end main loop */
    	if page_ctr < sa.start_page {
    		fmt.Fprintf(os.Stderr, "%s: start_page (%d) greater than total pages (%d), no output written\n", progname, sa.start_page, page_ctr)
    	} else if page_ctr < sa.end_page {
    		fmt.Fprintf(os.Stderr, "%s: end_page (%d) greater than total pages (%d), less output than expected\n", progname, sa.end_page, page_ctr)
    	}
    
    	fin.Close()
    	fout.Close()
    	fmt.Fprintf(os.Stderr, "%s: done\n", progname)
    }
    

    7.usage函数

    usage函数用于输出正确的指令参数格式。

    /*================================= usage() =======================*/
    
    func usage() {
    	fmt.Fprintf(os.Stderr, "\nUSAGE: %s -sstart_page -eend_page [ -f | -llines_per_page ] [ -ddest ] [ in_filename ]\n", progname)
    }
    



    四.程序测试

    1.功能测试

    此处按照IBM的c语言程序的使用实例来进行功能测试。
    首先在selpg目录下建立三个txt文件,分别为:
    ①in.txt, 用于输入的文本,内容如下(为方便演示,只有20行):
    在这里插入图片描述

    ②out.txt, 保存输出的文本,内容初始为空
    ③error.txt,保存错误信息,内容初始为空

    (1)selpg -s1 -e1 in.txt

    该命令将把“in.txt”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。

    [henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt
    Hello world!
    I am HenryHZY.
    line 1
    iine 2
    line 3
    line 4
    line 5
    line 6
    iine 7
    line 8
    line 9
    line 10
    line 11
    iine 12
    line 13
    line 14
    line 15
    line 16
    iine 17
    line 18
    selpg: done
    

    (2)selpg -s1 -e1 < in.txt

    该命令与示例 1 所做的工作相同,但在本例中,selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“in.txt”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。

    [henryhzy@localhost selpg]$ selpg -s1 -e1 < in.txt
    Hello world!
    I am HenryHZY.
    line 1
    iine 2
    line 3
    line 4
    line 5
    line 6
    iine 7
    line 8
    line 9
    line 10
    line 11
    iine 12
    line 13
    line 14
    line 15
    line 16
    iine 17
    line 18
    selpg: done
    

    (3)other_command | selpg -s1 -e1

    “other_command”的标准输出被 shell/内核重定向至 selpg 的标准输入。将第 1页写至 selpg 的标准输出(屏幕)。

    [henryhzy@localhost selpg]$ ls | selpg -s1 -e1
    error.txt
    in.txt
    out.txt
    selpg.go
    selpg: done
    

    (4)selpg -s1 -e1 in.txt >out.txt

    selpg 将第 1 页写至标准输出;标准输出被 shell/内核重定向至out.txt“”。
    在这里插入图片描述

    (5)selpg -s20 -e20 in.txt 2>error.txt

    selpg 将第 20 页至标准输出(屏幕);所有的错误消息被 shell/内核重定向至“error.txt”。请注意:在“2”和“>”之间不能有空格;这是 shell 语法的一部分(请参阅“man bash”或“man sh”)。
    在这里插入图片描述

    (6)selpg -s1 -e1 in.txt >out.txt 2>error.txt

    selpg 将第 1页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至“error_file”。当“input_file”很大时可使用这种调用;您不会想坐在那里等着 selpg 完成工作,并且您希望对输出和错误都进行保存。
    在这里插入图片描述

    (7)selpg -s20 -e20 in.txt >out.txt 2>/dev/null

    selpg 将第 20 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至 /dev/null(空设备),这意味着错误消息被丢弃了。设备文件 /dev/null 废弃所有写至它的输出,当从该设备文件读取时,会立即返回 EOF。
    此处本应有的的error信息被丢弃了。
    在这里插入图片描述

    (8)selpg -s10 -e20 in.txt >/dev/null

    selpg 将第 10 页到第 20 页写至标准输出,标准输出被丢弃;错误消息在屏幕出现。这可作为测试 selpg 的用途,此时您也许只想(对一些测试情况)检查错误消息,而不想看到正常输出。

    [henryhzy@localhost selpg]$ selpg -s10 -e20 in.txt >/dev/null
    selpg: start_page (10) greater than total pages (1), no output written
    selpg: done
    

    (9)selpg -s10 -e20 input_file 2>error_file | other_command

    selpg 的标准输出透明地被 shell/内核重定向,成为“other_command”的标准输入,第 1页被写至该标准输入。“other_command”的示例可以是 lp,它使输出在系统缺省打印机上打印。“other_command”的示例也可以 wc,它会显示选定范围的页中包含的行数、字数和字符数。“other_command”可以是任何其它能从其标准输入读取的命令。错误消息仍在屏幕显示。

    [henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt | ps
    selpg: done
      PID TTY          TIME CMD
    10209 pts/0    00:00:00 bash
    10528 pts/0    00:00:00 ps
    

    (10)selpg -s10 -e20 input_file 2>error_file | other_command

    与上面的示例 9 相似,只有一点不同:错误消息被写至“error_file”。
    在这里插入图片描述

    (11)selpg -s1 -e1 -l10 in.txt

    该命令将页长设置为 10 行,这样 selpg 就可以把输入当作被定界为该长度的页那样处理。文本的前10行被写至 selpg 的标准输出(屏幕)。

    [henryhzy@localhost selpg]$ selpg -s1 -e1 -l10 in.txt
    Hello world!
    I am HenryHZY.
    line 1
    iine 2
    line 3
    line 4
    line 5
    line 6
    iine 7
    line 8
    selpg: done
    

    (12)selpg -s1 -e1 -f in.txt

    假定页由换页符定界。第 10页被写至 selpg 的标准输出(屏幕)。

    [henryhzy@localhost selpg]$ selpg -s1 -e1 -f in.txt
    Hello world!
    I am HenryHZY.
    line 1
    iine 2
    line 3
    line 4
    line 5
    line 6
    iine 7
    line 8
    line 9
    line 10
    line 11
    iine 12
    line 13
    line 14
    line 15
    line 16
    iine 17
    line 18
    
    selpg: done
    
    

    (13)selpg -s1 -e1 in.txt | cat -n

    由于没有打印机,原测试的打印机输出改为cat输出。

    [henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt | cat -n
    selpg: done
         1	Hello world!
         2	I am HenryHZY.
         3	line 1
         4	iine 2
         5	line 3
         6	line 4
         7	line 5
         8	line 6
         9	iine 7
        10	line 8
        11	line 9
        12	line 10
        13	line 11
        14	iine 12
        15	line 13
        16	line 14
        17	line 15
        18	line 16
        19	iine 17
        20	line 18
    
    

    (14)selpg -s10 -e20 in.txt > out.txt 2>error.txt &

    该命令利用了 Linux 的一个强大特性,即:在“后台”运行进程的能力。在这个例子中发生的情况是:“进程标识”(pid)如 1234 将被显示,然后 shell 提示符几乎立刻会出现,使得您能向 shell 输入更多命令。同时,selpg 进程在后台运行,并且标准输出和标准错误都被重定向至文件。这样做的好处是您可以在 selpg 运行时继续做其它工作。
    在这里插入图片描述

    2.单元测试

    根据查找的单元测试与功能测试的区别:

    功能测试是站在用户的角度从外部测试应用查看效果是否达到
    单元测试是站在程序员的角度从内部测试应用

    既然已经测试过功能测试,那么在进行时单元测试,简单地以函数为单元测试调用情况即可。若是在结合输入输出正确与否,感觉有些多余了。:)

    测试代码:

    package main
    
    import "testing"
    
    func Test_usage(t *testing.T) {
    	tes := []struct {
    		name string
    	}{
    		{name: "Test_usage1"},
    		{name: "Test_usage2"},
    	}
    	for _, tt := range tes {
    		t.Run(tt.name, func(t *testing.T) {
    			usage()
    		})
    	}
    }
    
    func Test_process_args(t *testing.T) {
    
    	tests := []struct {
    		name string
    		len  int
    		sa   sp_args
    	}{}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			process_args(tt.len, &tt.sa)
    		})
    	}
    }
    
    func Test_process_input(t *testing.T) {
    	tests := []struct {
    		name string
    		sa   sp_args
    	}{}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			process_input(tt.sa)
    		})
    	}
    }
    
    func Test_main(t *testing.T) {
    	tests := []struct {
    		name string
    	}{}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			main()
    		})
    	}
    }
    
    

    为了方便进行单元测试,此处使用vscode进行测试,直接点击相应函数上面的run test即可,非常方便~~
    各个函数的测试截图如下:

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述



    五.完整代码

    具体代码可见gitee仓库:gitee仓库



    六. References

    1. Golang之使用Flag和Pflag
    2. 开发 Linux 命令行实用程序
    3. Package os
    展开全文
  • Node.js 命令行程序开发教程一种编程语言是否易用,很大程度上,取决于开发命令行程序的能力。 Node.js 作为目前最热门的开发工具之一,怎样使用它开发命令行程序,是 Web 开发者应该掌握的技能。 下面我们从开发...

    Node.js 命令行程序开发教程

    一种编程语言是否易用,很大程度上,取决于开发命令行程序的能力。
    Node.js 作为目前最热门的开发工具之一,怎样使用它开发命令行程序,是 Web 开发者应该掌握的技能。
    下面我们从开发一个简单的hello命令开始:

    1.可执行脚本

    首先,使用 JavaScript 语言,写一个可执行脚本 hello 。

    #!/usr/bin/env node
    console.log('hello world');

    然后,修改 hello 的权限。

    $ chmod 755 hello

    现在,hello 就可以执行了。

    $ ./hello
    hello world

    如果想把 hello 前面的路径去除,可以将 hello 的路径加入环境变量 PATH。但是,另一种更好的做法,是在当前目录下新建 package.json ,写入下面的内容。

    
    {
      "name": "hello",
      "bin": {
        "hello": "hello"
      }
    }

    然后执行 npm link 命令。

    $ npm link

    现在再执行 hello ,就不用输入路径了。

    $ hello
    hello world

    2.命令行参数的原始写法

    命令行参数可以用系统变量 process.argv 获取。
    还是以hello脚本为例:

    #!/usr/bin/env node
    console.log('hello ', process.argv[2]);

    执行时,直接在脚本文件后面,加上参数即可。

    $ ./hello tom
    hello tom

    上面代码中,实际上执行的是 node ./hello tom ,对应的 process.argv 是 [‘node’, ‘/path/to/hello’, ‘tom’] 。

    3.新建进程

    脚本可以通过 child_process 模块新建子进程,从而执行 Unix 系统命令。

    #!/usr/bin/env node
    var name = process.argv[2];
    var exec = require('child_process').exec;
    
    var child = exec('echo hello ' + name, function(err, stdout, stderr) {
      if (err) throw err;
      console.log(stdout);
    });

    用法如下。

    $ ./hello tom
    hello tom

    4.shelljs 模块

    shelljs 模块重新包装了 child_process,调用系统命令更加方便。它需要安装后使用。

    npm install --save shelljs

    然后,改写脚本。

    #!/usr/bin/env node
    var name = process.argv[2];
    var shell = require("shelljs");
    
    shell.exec("echo hello " + name);

    上面代码是 shelljs 的本地模式,即通过 exec 方法执行 shell 命令。此外还有全局模式,允许直接在脚本中写 shell 命令。

    require('shelljs/global');
    
    if (!which('git')) {
      echo('Sorry, this script requires git');
      exit(1);
    }
    
    mkdir('-p', 'out/Release');
    cp('-R', 'stuff/*', 'out/Release');
    
    cd('lib');
    ls('*.js').forEach(function(file) {
      sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
      sed('-i', /.*REMOVE_THIS_LINE.*\n/, '', file);
      sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat('macro.js'), file);
    });
    cd('..');
    
    if (exec('git commit -am "Auto-commit"').code !== 0) {
      echo('Error: Git commit failed');
      exit(1);
    }

    5.yargs 模块

    shelljs 只解决了如何调用 shell 命令,而 yargs 模块能够解决如何处理命令行参数。它也需要安装。

    $ npm install --save yargs

    yargs 模块提供 argv 对象,用来读取命令行参数。请看改写后的 hello 。

    #!/usr/bin/env node
    var argv = require('yargs').argv;
    console.log('hello ', argv.name);

    使用时,下面两种用法都可以。

    $ hello --name=tom
    hello tom
    
    $ hello --name tom
    hello tom

    也就是说,process.argv 的原始返回值如下。

    $ node hello --name=tom
    [ 'node',
      '/path/to/myscript.js',
      '--name=tom' ]

    yargs 可以上面的结果改为一个对象,每个参数项就是一个键值对。

    var argv = require('yargs').argv;
    
    // $ node hello --name=tom
    // argv = {
    //   name: tom
    // };

    如果将 argv.name 改成 argv.n,就可以使用一个字母的短参数形式了。

    $ hello -n tom
    hello tom

    可以使用 alias 方法,指定 name 是 n 的别名。

    #!/usr/bin/env node
    var argv = require('yargs')
      .alias('n', 'name')
      .argv;
    
    console.log('hello ', argv.n);

    这样一来,短参数和长参数就都可以使用了。

    $ hello -n tom
    hello tom
    $ hello --name tom
    hello tom

    argv 对象有一个下划线(_)属性,可以获取非连词线开头的参数。

    #!/usr/bin/env node
    var argv = require('yargs').argv;
    
    console.log('hello ', argv.n);
    console.log(argv._);

    用法如下。

    $ hello A -n tom B C
    hello  tom
    [ 'A', 'B', 'C' ]

    6.命令行参数的配置

    yargs 模块还提供3个方法,用来配置命令行参数。
    * demand:是否必选
    * default:默认值
    * describe:提示

    #!/usr/bin/env node
    var argv = require('yargs')
      .demand(['n'])
      .default({n: 'tom'})
      .describe({n: 'your name'})
      .argv;
    
    console.log('hello ', argv.n);

    上面代码指定 n 参数不可省略,默认值为 tom,并给出一行提示。
    options 方法允许将所有这些配置写进一个对象。

    #!/usr/bin/env node
    var argv = require('yargs')
      .option('n', {
        alias : 'name',
        demand: true,
        default: 'tom',
        describe: 'your name',
        type: 'string'
      })
      .argv;
    
    console.log('hello ', argv.n);

    有时,某些参数不需要值,只起到一个开关作用,这时可以用 boolean 方法指定这些参数返回布尔值。

    #!/usr/bin/env node
    var argv = require('yargs')
      .boolean(['n'])
      .argv;
    
    console.log('hello ', argv.n);

    上面代码中,参数 n 总是返回一个布尔值,用法如下。

    $ hello
    hello  false
    $ hello -n
    hello  true
    $ hello -n tom
    hello  true

    boolean 方法也可以作为属性,写入 option 对象。

    #!/usr/bin/env node
    var argv = require('yargs')
      .option('n', {
        boolean: true
      })
      .argv;
    
    console.log('hello ', argv.n);

    7.帮助信息

    yargs 模块提供以下方法,生成帮助信息。
    * usage:用法格式
    * example:提供例子
    * help:显示帮助信息
    * epilog:出现在帮助信息的结尾

    #!/usr/bin/env node
    var argv = require('yargs')
      .option('f', {
        alias : 'name',
        demand: true,
        default: 'tom',
        describe: 'your name',
        type: 'string'
      })
      .usage('Usage: hello [options]')
      .example('hello -n tom', 'say hello to Tom')
      .help('h')
      .alias('h', 'help')
      .epilog('copyright 2015')
      .argv;
    
    console.log('hello ', argv.n);

    执行结果如下。

    $ hello -h
    
    Usage: hello [options]
    
    Options:
      -f, --name  your name [string] [required] [default: "tom"]
      -h, --help  Show help [boolean]
    
    Examples:
      hello -n tom  say hello to Tom
    
    copyright 2015

    8.子命令

    yargs 模块还允许通过 command 方法,设置 Git 风格的子命令。

    #!/usr/bin/env node
    var argv = require('yargs')
      .command("morning", "good morning", function (yargs) {
        console.log("Good Morning");
      })
      .command("evening", "good evening", function (yargs) {
        console.log("Good Evening");
      })
      .argv;
    
    console.log('hello ', argv.n);

    用法如下。

    $ hello morning -n tom
    Good Morning
    hello tom

    可以将这个功能与 shellojs 模块结合起来。

    #!/usr/bin/env node
    require('shelljs/global');
    var argv = require('yargs')
      .command("morning", "good morning", function (yargs) {
        echo("Good Morning");
      })
      .command("evening", "good evening", function (yargs) {
        echo("Good Evening");
      })
      .argv;
    
    console.log('hello ', argv.n);

    每个子命令往往有自己的参数,这时就需要在回调函数中单独指定。回调函数中,要先用 reset 方法重置 yargs 对象。

    #!/usr/bin/env node
    require('shelljs/global');
    var argv = require('yargs')
      .command("morning", "good morning", function (yargs) {
        echo("Good Morning");
        var argv = yargs.reset()
          .option("m", {
            alias: "message",
            description: "provide any sentence"
          })
          .help("h")
          .alias("h", "help")
          .argv;
    
        echo(argv.m);
      })
      .argv;

    用法如下。

    $ hello morning -m "Are you hungry?"
    Good Morning
    Are you hungry?

    9.其他事项

    (1)返回值

    根据 Unix 传统,程序执行成功返回 0,否则返回 1 。

    (2)重定向

    Unix 允许程序之间使用管道重定向数据。

    $ ps aux | grep 'node'

    脚本可以通过监听标准输入的data 事件,获取重定向的数据。

    process.stdin.resume();
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', function(data) {
      process.stdout.write(data);
    });

    下面是用法。

    $ echo 'foo' | ./hello
    hello foo

    (3)系统信号

    操作系统可以向执行中的进程发送信号,process 对象能够监听信号事件。

    process.on('SIGINT', function () {
      console.log('Got a SIGINT');
      process.exit(0);
    });

    发送信号的方法如下。

    $ kill -s SIGINT [process_id]
    展开全文
  • Node.js 命令行程序开发教程http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html用Node.js创建命令行工具...

    Node.js 命令行程序开发教程
    http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html
    用Node.js创建命令行工具
    http://www.html-js.com/article/A-day-to-learn-JavaScript-create-commandline-tools-with-Nodejs
    Commander写自己的Nodejs命令
    http://blog.fens.me/nodejs-commander/
    node.js 如何完美的从命令行接收参数所传递进来的值
    http://segmentfault.com/q/1010000000367285
    用node.js开发命令行工具
    http://binbinliao.com/programming/commandline-nodejs.html

     

    注意点:

      在windows和mac直接开发node命令行一定要注意,文件的权限问题(往往window开发完毕,在mac下面npm link会报错),这里建议通过touch 生成文本问题,例touch index.js。

    转载于:https://www.cnblogs.com/pingfan1990/p/4909660.html

    展开全文
  • golang的selpg命令行程序开发

    千次阅读 2020-10-12 17:51:35
    CLI 命令行实用程序开发基础 文章目录CLI 命令行实用程序开发基础实验环境概述开发过程开发实践要求总体结构安装pflag和使用安装import使用参考C源码定义结构体以存储参数测试型开发绑定flag到变量上并初始化提供...

    CLI 命令行实用程序开发基础

    实验环境

    操作系统:Ubuntu18.04.5LTS-amd64
    编辑器:VScode、Typora

    概述

    CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。

    开发过程

    详细代码见这

    开发实践要求

    使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg

    提示:

    1. 请按文档 使用 selpg 章节要求测试你的程序
    2. 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
    3. golang 文件读写、读环境变量,请自己查 os 包
    4. “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
    5. 请自带测试程序,确保函数等功能正确

    总体结构

    • selpg.go包含selpg的相关程序
    • selpg_test.go包含相关测试函数
    • selpg为可执行二进制文件
    • test、out、error为测试输入输出所用的文件

    安装pflag和使用

    安装

    go get github.com/spf13/pflag
    

    import

    import flag "github.com/spf13/pflag"
    

    代码可以使用flag.xxx来调用函数了;

    使用

    pflag包在此处的优势是它对参数的读取与unix标准相同;

    pflag包基本用法和flag包类似,由于flag包文档多,基本是参照flag包的文档来使用;

    更多相关用法可见github文档

    参考C源码

    开发 Linux 命令行实用程序 中提供了 selpg.c 可进行参考;

    事实上,go与c之间的良好的移植性可以简化代码开发;

    而且在许多方面,go的实现能更简单快捷,使得整体程序不像c那样繁琐,就是可能效率慢点;

    下面的代码中有许多是移植C源码的,但在pflag、输入输出相关的方面需要做大的改动。

    以下代码如无特别说明,均位于selpg.go中

    定义结构体以存储参数

    type selpgArgs struct {
    	startPage  int
    	endPage    int
    	inFilename string
    	pageLen    int
    	pageType   bool
    	printDest  string
    }
    
    • startPage:开始页码
    • endPage:结束页码
    • inFilename:输入的文件名
    • printDest:输出的文件名
    • pageLen:每页的行数
    • pageType:打印的模式,"-l"按行打印,"-f"按换页符打印

    测试型开发

    程序需要进行参数的初始化,在Initflag函数中完成,对其的测试函数:

    函数位于selpg_test.go

    func TestInitflag(t *testing.T) {
    	want := selpgArgs{0, 0, "", 72, false, ""}
    	var got selpgArgs
    
    	Initflag(&got)
    
    	if got != want {
    		t.Errorf("Initflag got %v, want %v\n", got, want)
    	}
    }
    

    程序需要检查参数格式,在ProcessArgs函数中完成,对其的测试函数:

    函数位于selpg_test.go

    func TestProcessArgs(t *testing.T) {
    	cases := []struct {
    		insel selpgArgs
    		inos  []string
    		want  string
    	}{
    		{selpgArgs{1, 1, "test", 72, false, ""}, []string{"./selpg", "-s1", "-e1", "test"}, ""},
    		{selpgArgs{0, 1, "test", 72, false, ""}, []string{"./selpg", "-s0", "-e1", "test"}, "./selpg: invalid start page 0\n"},
    		{selpgArgs{2, 1, "test", 72, false, ""}, []string{"./selpg", "-s2", "-e1", "test"}, "./selpg: invalid end page 1\n"},
    		{selpgArgs{1, 0, "", 72, false, ""}, []string{"./selpg", "-s1"}, "./selpg: not enough arguments\n"},
    		{selpgArgs{0, 1, "test", 72, false, ""}, []string{"./selpg", "-e1", "-e1", "test"}, "./selpg: 1st arg should be -sstartPage\n"},
    		{selpgArgs{1, 0, "test", 72, false, ""}, []string{"./selpg", "-s1", "-s1", "test"}, "./selpg: 2nd arg should be -eendPage\n"},
    	}
    
    	for _, c := range cases {
    		got := ProcessArgs(&c.insel, c.inos)
    		if got != c.want {
    			t.Errorf("ProcessArgs(%v, %v) == %s, want %s\n", c.insel, c.inos, got, c.want)
    		}
    	}
    }
    

    绑定flag到变量上并初始化

    pflag包中处理参数的关键;

    func Initflag(sa *selpgArgs) {
    	flag.IntVarP(&sa.startPage, "startPage", "s", 0, "Start page number")
    	flag.IntVarP(&sa.endPage, "endPage", "e", 0, "End page number")
    	flag.IntVarP(&sa.pageLen, "pageLen", "l", 72, "Line number for a page")
    	flag.BoolVarP(&sa.pageType, "pageType", "f", false, "Determine form-feed-delimited")
    	flag.StringVarP(&sa.printDest, "dest", "d", "", "Set printer")
    	flag.Usage = usage
    
    	flag.Parse()
    }
    

    提供使用说明

    func usage() {
    	fmt.Printf("\nUSAGE: %s -sstartPage -eendPage [ -f | -llines_per_page ] [ -ddest ] [ inFilename ]\n", progname)
    }
    

    检验参数

    检验必要的必要参数是否有、参数形式是否正确、参数内容是否合理(如页码不能少于1、起始页码不能大于终止页码等);

    // ProcessArgs check args format
    func ProcessArgs(sa *selpgArgs, args []string) string {
    	progname = args[0]
    
    	/* check the command-line arguments for validity */
    	if len(args) < 3 {
    		return fmt.Sprintf("%s: not enough arguments\n", progname)
    	}
    
    	/* handle 1st arg - start page */
    	if args[1][1] != 's' {
    		return fmt.Sprintf("%s: 1st arg should be -sstartPage\n", progname)
    	}
    
    	if sa.startPage < 1 || sa.startPage > (math.MaxInt32-1) {
    		return fmt.Sprintf("%s: invalid start page %d\n", progname, sa.startPage)
    	}
    
    	/* handle 2nd arg - end page */
    	index := 2
    	if len(args[1]) == 2 {
    		index = 3
    	}
    	if args[index][1] != 'e' {
    		return fmt.Sprintf("%s: 2nd arg should be -eendPage\n", progname)
    	}
    
    	if sa.endPage < 1 || sa.endPage > (math.MaxInt32-1) || sa.startPage > sa.endPage {
    		return fmt.Sprintf("%s: invalid end page %d\n", progname, sa.endPage)
    	}
    
    	var noerr string
    	return noerr
    }
    

    执行命令

    根据参数执行相应的命令;

    func processInput(sa *selpg_args) {...}
    

    使用的部分标准库中的函数

    关于io.Writecloser :WriteCloser 接口组合了基本的 Write 和 Close 方法。

    Write 将 len§ 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len§)以及任何遇到的引起写入提前停止的错误。

    关于exec.Cmd :Cmd表示正在准备或运行的外部命令。

    关于Cmd.Runfunc (c *Cmd) Run() error

    Run starts the specified command and waits for it to complete.

    关于StdinPipefunc (c *Cmd) StdinPipe() (io.WriteCloser, error),StdinPipe返回一个管道,该管道将在命令启动时连接到命令的标准输入。

    关于NewReaderfunc NewReader(rd io.Reader) *Reader,返回一个新的Reader;

    关于ReadStringfunc (b *Reader) ReadString(delim byte) (line string, err error),ReadString读取输入到第一次终止符发生的时候,返回的string包含从当前到终止符的内容(包括终止符)。

    关于ReadLine:ReadLine尝试返回单个行,不包括行尾的最后一个分隔符。

    关于Scanner:Scanner类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行。

    • exec包的使用是为了获得外部的程序,得以调用cat命令来启用打印设备;
    • 程序中采用三种读取方式ReadStringReadLineScanner 是为了更方便的应对三种不同的读取场景,ReadString是为了方便读取到换页符\fReadLine是为了按行打印的模式运行,Scanner则是适应最参数缺省下的场景;

    部分代码展示与解释

    利用os/exec包和io包管理进程的输入与输出,将cat命令放在管道中,在相应时间且需要打印时调用Run函数运行。

    cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上。

    var stdin io.WriteCloser
    var cmd *exec.Cmd
    ...
    if sa.printDest != "" {
    	var err1 error
    	var err2 error
    	cmd = exec.Command("cat")
    	cmd.Stdout, err1 = os.OpenFile(sa.printDest, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
    
    	if err1 != nil {
    		fmt.Fprintf(os.Stderr, "\n%s: fail to open file %s\n", progname, sa.printDest)
    		os.Exit(4)
    	}
    
    	stdin, err2 = cmd.StdinPipe()
    	if err2 != nil {
    		fmt.Fprintf(os.Stderr, "\n%s: fail to open pipe to %s\n", progname, sa.printDest)
    		os.Exit(5)
    	}
    } else {
    	stdin = nil
    }
    ...
    if sa.printDest != "" {
    	stdin.Close()
    	err := cmd.Run()
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "\n%s: fail to connect to device\n", progname)
    		os.Exit(9)
    	}
    }
    

    按行打印模式下的程序读写,通过ReadLine函数来计算读取的行,并以此确定页数;

    count := 0
    for {
    	line, _, err := reader.ReadLine()
    	if err != io.EOF && err != nil {
    		return "", fmt.Sprintf("\n%s: fail to read line\n", progname)
    	}
    	if err == io.EOF {
    		break
    	}
    	if count/sa.pageLen+1 >= sa.startPage {
    		if count/sa.pageLen+1 <= sa.endPage {
    			// printAns(sa, string(line), stdin)
    			ref += string(line)
    			ref += "\n"
    		} else {
    			break
    		}
    	}
    
    	count++
    }
    

    打印输出

    打印输出时需要考虑是输出到打印设备还是直接输出到终端;

    • 注意:这里输出到打印设备只是通过Write函数将信息放在管道中,直到main函数中cmd.Run()时才输出到打印设备。
    func printAns(sa *selpg_args, line string, stdin io.WriteCloser) {
    	if sa.print_dest != "" {
    		stdin.Write([]byte(line + "\n"))
    	} else {
    		fmt.Println(line)
    	}
    }
    

    main函数

    调用各函数,并处理错误的输出、打印设备的输出;

    func main() {
    	var sa selpgArgs
    
    	Initflag(&sa)
    	err := ProcessArgs(&sa, os.Args)
    	if err != "" {
    		fmt.Fprintf(os.Stderr, err)
    		flag.Usage()
    		os.Exit(1)
    	}
    
    	ans, err := ProcessInput(&sa)
    	if err != "" {
    		fmt.Fprintf(os.Stderr, err)
    		os.Exit(1)
    	} else {
    		printAns(&sa, ans, stdin)
    	}
    
    	if sa.printDest != "" {
    		stdin.Close()
    		err := cmd.Run()
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "\n%s: fail to connect to device\n", progname)
    		}
    	}
    }
    

    测试

    • 关于运行程序,文件夹中已有selpg二进制文件,可以通过./selpg xxxx的方式运行,也可以使用go install命令,之后用selpg xxxx的方式运行。

    单元测试

    运行单元测试函数

    27

    28

    功能测试

    • 实验中的test文档在Gitee上也包含,文档中包含一系列test Line帮助检验输出行数,而输出的空行是换行符换行符以空行显示但它并不占据实际的输出行数,这点在1中可以明显看出,说明程序运行正确。

    pfalg中支持多种参数输入形式,如-s1-s 1-s=1均可以,根据题目下面测试均采用unix格式,即-s1形式,但其余形式均也可以正常运行。

    wc利用wc指令我们可以计算文件的Byte数、字数、或是列数

    测试均参考开发 Linux 命令行实用程序使用selpg,但部分演示为了能演示功能,修改了起始终止页码等部分参数;

    1. selpg -s1 -e1 test

      输出较长,截图只截了前后两部分

      1

      ……

      2

    2. selpg -s1 -e1 < test

      输出较长,截图只截了前后两部分

      3

      ……

      4

    3. cat test | selpg -s10 -e20

      输出较长,截图只截了前后两部分

      5

      ……

      6

    4. selpg -s3 -e5 -l10 test >out

      7

      out文件:

      8

    5. selpg -s20 -e10 test 2>error

      9

      error文件:

      10

    6. selpg -s1 -e2 -l10 test >out 2>error

      11

      out文件:

      12

      error文件:为空,因为无错误

    7. selpg -s2 -e1 test >out 2>/dev/null

      13

      out文件:输入提示被输入到此处

      14

      error文件:为空,报错信息被丢弃

    8. selpg -s10 -e20 test >/dev/null

      15

      out文件:为空

    9. selpg -s1 -e1 test | wc

      16

    10. selpg -s2 -e1 test 2>error | wc

      17

      error文件:

      18

    11. selpg -s2 -e2 -l 16 test

      19

    12. selpg -s1 -e1 -f test

      输出较长,截图只截了前后两部分

      20

      ……

      21

    13. selpg -s2 -e3 -l10 -dout test

      22

      out文件:

      23

    部分报错情况测试

    1. selpg -s0 -e1 test

      24

    2. selpg -s1

      25

    3. selpg -s1 -e1 tes

      26

    参考

    开发 Linux 命令行实用程序

    Golang之使用Flag和Pflag

    额外的博客

    godoc与go doc相关

    展开全文
  • 命令行单执行文件 Phar 开发程序骨架,Mix 封装的命令行基础设施在骨架中都可使用。 我们提供了打包工具 ,可以将本项目打包为 Phar 文件(就像 golang 编译成执行文件一样)。 PHP 原本就是一个动态版本的 C 库集合...
  • http://www.runoob.com/w3cnote/node-js-command-develop.html
  • 目的-用C开发查看HEX字节码的工具 在windows下,要看一个文本文件的内容,可用”记事本“即可,用type file.txt 也可在命令窗显示文件内容。但是– 如果要看一个二进制文件的内部字节码,用什么工具? 你会说,有...
  • cobra包1、支持子命令命令行程序支持包开发1.1 概述1.2 课程任务2、cobra介绍3、简化版cobra3.1 程序说明3.2 程序主要函数3.3 单元测试3.4 功能测试 1、支持子命令命令行程序支持包开发 1.1 概述 命令行实用程序并...
  • dti 命令行Electron开发工具安装程序
  • begins, 面向繁忙开发人员的命令行 程序 开始 概述用于 lazy的命令行 程序。将一个函数装饰为你的程序起始点。基于函数签名的命令行 解析器生成。用于选项默认值的搜索系统环境。 为什么开始?我在 python 编写了很...
  • CLI 命令行实用程序开发基础CLI 命令行实用程序开发基础命令行准则输入输出参数程序说明定义结构体读取解析参数拟定提示信息从标准输入或文件输出至标准输出或文件程序测试输入测试输出测试错误输出测试其他参数测试...
  • 支持子命令命令行程序支持包开发任务文件结构实现过程实现一个简单的子命令安装cobra包实现根命令实现子命令 任务 了解Cobra包,使用 cobra 命令行生成一个简单的带子命令的命令行程序 模仿 cobra.Command 编写一个...
  • MixCLI让PHP像Golang一样开发命令行程序单执行文件
  • 文章目录任务先行知识flag与pflag异常...用 Go 语言实现一个类 selpg 程序,关于该程序的详细介绍,可见:开发Linux命令行实用程序程序的 C 语言源码实现:selpg.c selpg 的用法如下: selpg --s startPage --e e...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,718
精华内容 5,087
关键字:

命令行程序开发