ishell:創(chuàng)建交互式cli應(yīng)用程序庫

ishell是一個用于創(chuàng)建交互式cli應(yīng)用程序的交互式shell庫。

最近在研究supervisor的源碼,參考supervisor的架構(gòu),做公司的項目。我后面會給出supervisor的開源學(xué)習(xí)的總結(jié)绊袋。github上有一個gopher寫了一個golang版的supervisor,源碼,原理和python版的都類似铸鹰,但是 ctl是執(zhí)行命令的方式癌别,不是很優(yōu)雅。

今天這篇文章介紹一個go的包蹋笼,實現(xiàn)交互式的CLI工具的包展姐。

常見的cli包有:flag、cli剖毯、os...都可以實現(xiàn)

但是上面有一個問題圾笨,就是執(zhí)行完以后,就會給出結(jié)果逊谋,并退出擂达,不是進(jìn)入一個shell中,執(zhí)行所有結(jié)果都是不同的胶滋。

交互式的cli如下:


今天要介紹的庫是 ishell

類似上面的gif圖中效果板鬓,很容易實現(xiàn)

代碼示例

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的結(jié)構(gòu)體:

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
}

執(zhí)行的結(jié)果:

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. 輸入數(shù)據(jù)或密碼

    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 ';'.")
            // 設(shè)置結(jié)束符
            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. 進(jìn)度條

    // 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
}

重要 內(nèi)容都在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


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妓柜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涯穷,更是在濱河造成了極大的恐慌棍掐,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拷况,死亡現(xiàn)場離奇詭異作煌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)赚瘦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門粟誓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人起意,你說我怎么就攤上這事鹰服。” “怎么了揽咕?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵悲酷,是天一觀的道長。 經(jīng)常有香客問我亲善,道長设易,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任蛹头,我火速辦了婚禮顿肺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘渣蜗。我一直安慰自己屠尊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布袍睡。 她就那樣靜靜地躺著知染,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斑胜。 梳的紋絲不亂的頭發(fā)上控淡,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機(jī)與錄音止潘,去河邊找鬼掺炭。 笑死,一個胖子當(dāng)著我的面吹牛凭戴,可吹牛的內(nèi)容都是我干的涧狮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼者冤!你這毒婦竟也來了肤视?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤涉枫,失蹤者是張志新(化名)和其女友劉穎邢滑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愿汰,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡困后,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了衬廷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摇予。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吗跋,靈堂內(nèi)的尸體忽然破棺而出侧戴,到底是詐尸還是另有隱情,我是刑警寧澤小腊,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布救鲤,位于F島的核電站,受9級特大地震影響秩冈,放射性物質(zhì)發(fā)生泄漏本缠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一入问、第九天 我趴在偏房一處隱蔽的房頂上張望丹锹。 院中可真熱鬧,春花似錦芬失、人聲如沸楣黍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽租漂。三九已至,卻和暖如春颊糜,著一層夾襖步出監(jiān)牢的瞬間哩治,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工衬鱼, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留业筏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓鸟赫,卻偏偏與公主長得像蒜胖,于是被迫代替她去往敵國和親消别。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容