前言

好久没写东西了,所以翻出老东西继续写点水水的玩意儿QAQ
核心原理这个包github.com/howeyc/gopass的代码实现已经写的很清晰了,其实就是正确使用golang.org/x/crypto/ssh/terminal中MakeRaw功能即可。

实现

“请按任意键继续”是windows bat中很常见的一句话。
这里关键点在于任意键,Go中用fmt.Scan等函数程序处理输入都是在按下回车键后才进入我们的处理逻辑的,所以这里就要用到terminal.MakeRaw函数,通过传入stdin的文件描述符,亲自处理每个传进来的字节。
同样的,因为用MakeRaw能够处理每个传进来的字节,所以我们不仅能实现按任意键继续,也能实现按指定的字符后继续

直接贴代码吧,加上注释,代码扔在了https://github.com/Starainrt/stario


// StopUntil 按下指定的字符函数才会返回,如果不是指定的字符,返回error(repear=false)
// 或阻塞住直到按下指定的字符(repeat=true)
// hint提示文本,trigger指定的字符,repeat是否允许多次重试
func StopUntil(hint string, trigger string, repeat bool) error {
    pressLen := len([]rune(trigger))
    if trigger == "" {
        pressLen = 1
    }
        //这里获取stdin标准输入的文件描述符
    fd := int(os.Stdin.Fd())
    if hint != "" {
                //这里打印自定义提示,如:“请按任意键继续”
        fmt.Print(hint)
    }
        // 进入Raw模式
    state, err := terminal.MakeRaw(fd)
    if err != nil {
        return err
    }
       //defer退出Raw模式
    defer terminal.Restore(fd, state)
    inputReader := bufio.NewReader(os.Stdin)
    //ioBuf := make([]byte, pressLen)
    i := 0
    for {
                //这里用了Rune,是考虑到中文字符
        b, _, err := inputReader.ReadRune()
        if err != nil {
            return err
        }
                //指定的字符为空,按下任意键直接返回
        if trigger == "" {
            break
        }
        if b == []rune(trigger)[i] {
            i++
            if i == pressLen {
                break
            }
            continue
        }
        i = 0
        if hint != "" && repeat {
            fmt.Print("\r\n")
            fmt.Print(hint)
        }
    }
    return nil
}

测试代码:

package main

import (
    "b612.me/stario"
    "fmt"
)

func main() {
    if err := stario.StopUntil("请按任意键继续", "", false); err != nil {
        panic(err)
    }
    fmt.Println("\ncontinued")
    if err := stario.StopUntil("请按b612继续", "b612", true); err != nil {
        panic(err)
    }
    fmt.Println("\nbye bye")
}

效果:

屏幕截图 2021-08-14 105238.png

密码掩码的实现原理一样,差别就是读取到输入的时候屏幕上打印*或者其他自定义字符,实现可参考gopass包
直接的简单的实现可参考下面的代码,代码同样扔在了https://github.com/Starainrt/stario

type InputMsg struct {
    msg string
    err error
}

func passwd(hint string, defaultVal string, mask string) InputMsg {
    var ioBuf []rune
    if hint != "" {
        fmt.Print(hint)
    }
    if strings.Index(hint, "\n") >= 0 {
        hint = strings.TrimSpace(hint[strings.LastIndex(hint, "\n"):])
    }
    fd := int(os.Stdin.Fd())
    state, err := terminal.MakeRaw(fd)
    if err != nil {
        return InputMsg{"", err}
    }
    defer fmt.Println()
    defer terminal.Restore(fd, state)
    inputReader := bufio.NewReader(os.Stdin)
    for {
        b, _, err := inputReader.ReadRune()
        if err != nil {
            return InputMsg{"", err}
        }
        if b == 0x0d {
            strValue := strings.TrimSpace(string(ioBuf))
            if len(strValue) == 0 {
                strValue = defaultVal
            }
            return InputMsg{strValue, nil}
        }
        if b == 0x08 || b == 0x7F {
            if len(ioBuf) > 0 {
                ioBuf = ioBuf[:len(ioBuf)-1]
            }
            fmt.Print("\r")
            for i := 0; i < len(ioBuf)+2+len(hint); i++ {
                fmt.Print(" ")
            }
        } else {
            ioBuf = append(ioBuf, b)
        }
        fmt.Print("\r")
        if hint != "" {
            fmt.Print(hint)
        }
        for i := 0; i < len(ioBuf); i++ {
            fmt.Print(mask)
        }
    }
}

测试代码:

package main

import (
    "b612.me/stario"
    "fmt"
)

func main() {
    //设置掩码为*
    fmt.Println("输入的密码为:",
        stario.PasswdWithMask("请输入密码:", "", "*").MustString())
    //不设置掩码,输入密码时不显示
    fmt.Println("输入的密码为:",
        stario.PasswdWithMask("请输入密码:", "", "").MustString())
    //设置掩码为@
    fmt.Println("输入的密码为:",
        stario.PasswdWithMask("请输入密码:", "", "@").MustString())
}

效果:
屏幕截图 2021-08-14 110128.png