ishell:创建交互式cli应用程序库

ishell是一个用于创建交互式cli应用程序的交互式shell库。

最近在研究supervisor的源码,参考supervisor的架构,做公司的项目。我后面会给出supervisor的开源学习的总结。github上有一个gopher写了一个golang版的supervisor,源码,原理和python版的都类似,但是 ctl是执行命令的方式,不是很优雅。

今天这篇文章介绍一个go的包,实现交互式的CLI工具的包。

常见的cli包有:flag、cli、os...都可以实现

但是上面有一个问题,就是执行完以后,就会给出结果,并退出,不是进入一个shell中,执行所有结果都是不同的。

交互式的cli如下:


今天要介绍的库是 ishell

类似上面的gif图中效果,很容易实现

代码示例

import "strings"
import "github.com/abiosoft/ishell"

func main(){
    // create new shell.
    // by default, new shell includes 'exit', 'help' and 'clear' commands.
    shell := ishell.New()

    // display welcome info.
    shell.Println("Sample Interactive Shell")

    // register a function for "greet" command.
    shell.AddCmd(&ishell.Cmd{
        Name: "greet",
        Help: "greet user",
        Func: func(c *ishell.Context) {
            c.Println("Hello", strings.Join(c.Args, " "))
        },
    })

    // run shell
    shell.Run()
}

上面代码很简单就是先实例化ishell.New()一个 Shell对象,使用方法AddCmd添加命令

看一下源码:

// New creates a new shell with default settings. Uses standard output and default prompt ">> ".
func New() *Shell {
    return NewWithConfig(&readline.Config{Prompt: defaultPrompt})
}

// NewWithConfig creates a new shell with custom readline config.
func NewWithConfig(conf *readline.Config) *Shell {
    rl, err := readline.NewEx(conf)
    if err != nil {
        log.Println("Shell or operating system not supported.")
        log.Fatal(err)
    }

    return NewWithReadline(rl)
}

// NewWithReadline creates a new shell with a custom readline instance.
func NewWithReadline(rl *readline.Instance) *Shell {
    shell := &Shell{
        rootCmd: &Cmd{},
        reader: &shellReader{
            scanner:     rl,
            prompt:      rl.Config.Prompt,
            multiPrompt: defaultMultiPrompt,
            showPrompt:  true,
            buf:         &bytes.Buffer{},
            completer:   readline.NewPrefixCompleter(),
        },
        writer:   rl.Config.Stdout,
        autoHelp: true,
    }
    shell.Actions = &shellActionsImpl{Shell: shell}
    shell.progressBar = newProgressBar(shell)
    addDefaultFuncs(shell)
    return shell
}


func (s *Shell) AddCmd(cmd *Cmd) {
    s.rootCmd.AddCmd(cmd)
}

// AddCmd adds cmd as a subcommand.
func (c *Cmd) AddCmd(cmd *Cmd) {
    if c.children == nil {
        c.children = make(map[string]*Cmd)
    }
    c.children[cmd.Name] = cmd
}

再看一下shell的结构体:

type Shell struct {
    rootCmd           *Cmd
    generic           func(*Context)
    interrupt         func(*Context, int, string)
    interruptCount    int
    eof               func(*Context)
    reader            *shellReader
    writer            io.Writer
    active            bool
    activeMutex       sync.RWMutex
    ignoreCase        bool
    customCompleter   bool
    multiChoiceActive bool
    haltChan          chan struct{}
    historyFile       string
    autoHelp          bool
    rawArgs           []string
    progressBar       ProgressBar
    pager             string
    pagerArgs         []string
    contextValues
    Actions
}

执行的结果:

Sample Interactive Shell
>>> help

Commands:
  clear      clear the screen
  greet      greet user
  exit       exit the program
  help       display help

>>> greet Someone Somewhere
Hello Someone Somewhere
>>> exit
$

常用的属性

1. 输入数据或密码

    shell.AddCmd(&ishell.Cmd{
        Name: "login",
        Func: func(c *ishell.Context) {
            c.ShowPrompt(false)
            defer c.ShowPrompt(true)

            c.Println("Let's simulate login")

            // prompt for input
            c.Print("Username: ")
            username := c.ReadLine()
            c.Print("Password: ")
            password := c.ReadPassword()

            // do something with username and password
            c.Println("Your inputs were", username, "and", password+".")

        },
        Help: "simulate a login",
    })

2. 输入可以换行

    // read multiple lines with "multi" command
    shell.AddCmd(&ishell.Cmd{
        Name: "multi",
        Help: "input in multiple lines",
        Func: func(c *ishell.Context) {
            c.Println("Input multiple lines and end with semicolon ';'.")
            // 设置结束符
            lines := c.ReadMultiLines(";") 
            c.Println("Done reading. You wrote:")
            c.Println(lines)
        },
    })

3. 单选

    // choice
    shell.AddCmd(&ishell.Cmd{
        Name: "choice",
        Help: "multiple choice prompt",
        Func: func(c *ishell.Context) {
            choice := c.MultiChoice([]string{
                "Golangers",
                "Go programmers",
                "Gophers",
                "Goers",
            }, "What are Go programmers called ?")
            if choice == 2 {
                c.Println("You got it!")
            } else {
                c.Println("Sorry, you're wrong.")
            }
        },
    })

4. 多选

    // multiple choice
    shell.AddCmd(&ishell.Cmd{
        Name: "checklist",
        Help: "checklist prompt",
        Func: func(c *ishell.Context) {
            languages := []string{"Python", "Go", "Haskell", "Rust"}
            choices := c.Checklist(languages,
                "What are your favourite programming languages ?",
                nil)
            out := func() (c []string) {
                for _, v := range choices {
                    c = append(c, languages[v])
                }
                return
            }
            c.Println("Your choices are", strings.Join(out(), ", "))
        },
    })

5. 颜色

    cyan := color.New(color.FgCyan).SprintFunc()
    yellow := color.New(color.FgYellow).SprintFunc()
    boldRed := color.New(color.FgRed, color.Bold).SprintFunc()
    shell.AddCmd(&ishell.Cmd{
        Name: "color",
        Help: "color print",
        Func: func(c *ishell.Context) {
            c.Print(cyan("cyan\n"))
            c.Println(yellow("yellow"))
            c.Printf("%s\n", boldRed("bold red"))
        },
    })

6. 进度条

    // progress bars
    {
        // determinate
        shell.AddCmd(&ishell.Cmd{
            Name: "det",
            Help: "determinate progress bar",
            Func: func(c *ishell.Context) {
                c.ProgressBar().Start()
                for i := 0; i < 101; i++ {
                    c.ProgressBar().Suffix(fmt.Sprint(" ", i, "%"))
                    c.ProgressBar().Progress(i)
                    time.Sleep(time.Millisecond * 100)
                }
                c.ProgressBar().Stop()
            },
        })

        // indeterminate
        shell.AddCmd(&ishell.Cmd{
            Name: "ind",
            Help: "indeterminate progress bar",
            Func: func(c *ishell.Context) {
                c.ProgressBar().Indeterminate(true)
                c.ProgressBar().Start()
                time.Sleep(time.Second * 10)
                c.ProgressBar().Stop()
            },
        })
    }

分析一下上面的源码

上面介绍了一些常用的命令,下面我们直接看源码:

        shell.AddCmd(&ishell.Cmd{
            Name: "det",
            Help: "determinate progress bar",
            Func: func(c *ishell.Context) {
                c.ProgressBar().Start()
                for i := 0; i < 101; i++ {
                    c.ProgressBar().Suffix(fmt.Sprint(" ", i, "%"))
                    c.ProgressBar().Progress(i)
                    time.Sleep(time.Millisecond * 100)
                }
                c.ProgressBar().Stop()
            },
        })
        
        

上面很多操作都是在 func(c *ishell.Context)里面操作的

type Context struct {
    contextValues
    progressBar ProgressBar
    err         error

    // Args is command arguments.
    Args []string

    // RawArgs is unprocessed command arguments.
    RawArgs []string

    // Cmd is the currently executing command. This is empty for NotFound and Interrupt.
    Cmd Cmd

    Actions
}

重要 内容都在Actions中

// Actions are actions that can be performed by a shell.
type Actions interface {
    // ReadLine reads a line from standard input.
    ReadLine() string
    // ReadLineErr is ReadLine but returns error as well
    ReadLineErr() (string, error)
    // ReadLineWithDefault reads a line from standard input with default value.
    ReadLineWithDefault(string) string
    // ReadPassword reads password from standard input without echoing the characters.
    // Note that this only works as expected when the standard input is a terminal.
    ReadPassword() string
    // ReadPasswordErr is ReadPassword but returns error as well
    ReadPasswordErr() (string, error)
    // ReadMultiLinesFunc reads multiple lines from standard input. It passes each read line to
    // f and stops reading when f returns false.
    ReadMultiLinesFunc(f func(string) bool) string
    // ReadMultiLines reads multiple lines from standard input. It stops reading when terminator
    // is encountered at the end of the line. It returns the lines read including terminator.
    // For more control, use ReadMultiLinesFunc.
    ReadMultiLines(terminator string) string
    // Println prints to output and ends with newline character.
    Println(val ...interface{})
    // Print prints to output.
    Print(val ...interface{})
    // Printf prints to output using string format.
    Printf(format string, val ...interface{})
    // ShowPaged shows a paged text that is scrollable.
    // This leverages on "less" for unix and "more" for windows.
    ShowPaged(text string) error
    // ShowPagedReader shows a paged text that is scrollable, from a reader source.
    // This leverages on "less" for unix and "more" for windows.
    ShowPagedReader(r io.Reader) error
    // MultiChoice presents options to the user.
    // returns the index of the selection or -1 if nothing is
    // selected.
    // text is displayed before the options.
    MultiChoice(options []string, text string) int
    // Checklist is similar to MultiChoice but user can choose multiple variants using Space.
    // init is initially selected options.
    Checklist(options []string, text string, init []int) []int
    // SetPrompt sets the prompt string. The string to be displayed before the cursor.
    SetPrompt(prompt string)
    // SetMultiPrompt sets the prompt string used for multiple lines. The string to be displayed before
    // the cursor; starting from the second line of input.
    SetMultiPrompt(prompt string)
    // SetMultiChoicePrompt sets the prompt strings used for MultiChoice().
    SetMultiChoicePrompt(prompt, spacer string)
    // SetChecklistOptions sets the strings representing the options of Checklist().
    // The generated string depends on SetMultiChoicePrompt() also.
    SetChecklistOptions(open, selected string)
    // ShowPrompt sets whether prompt should show when requesting input for ReadLine and ReadPassword.
    // Defaults to true.
    ShowPrompt(show bool)
    // Cmds returns all the commands added to the shell.
    Cmds() []*Cmd
    // HelpText returns the computed help of top level commands.
    HelpText() string
    // ClearScreen clears the screen. Same behaviour as running 'clear' in unix terminal or 'cls' in windows cmd.
    ClearScreen() error
    // Stop stops the shell. This will stop the shell from auto reading inputs and calling
    // registered functions. A stopped shell is only inactive but totally functional.
    // Its functions can still be called and can be restarted.
    Stop()
}

具体的用法说明,有注释。
如果需要深入,就自己看吧。有什么问题,可以私信给我。
下面我展示一下demo


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容