..

Interactive与Non-interactive Shell

interactive 与 non-interactive shell

我们日常用到最多的,是交互式 shell。没有输入任何命令之前,命令行会等待输入命令。输入了命令,在命令执行过程也可以进行输入,如 sudo apt install vim ,会交互式的让你输入密码、安装前会让你输入 Y 进行确认。

而所谓无交互式 “shell”,就是没有办法进行交互式输入的程序。要特别对 “shell” 强调 “无交互” ,只不过提一到 shell,我们预想到的是终端及其交互式的界面,预期的都是有交互的。很多程序都是以无交互的形式在运行的,各类 server、cronjob、甚至连交互式shell中跟在管道后面的命令都是以无交互式的形式运行的。

在 shell 中,我们可以通过 tty 命令来查询当前 shell 所使用的虚拟 tty,如果当前有所使用的 tty,那基本就说明当前是以交互式的方式运行的 shell。其实这很像一句废话,都能输入 tty 这三个字符了,当然是交互式的。

$ tty
/dev/ttys011

# 运行在管道里命令,那就没有 tty了,也就不是交互式 shell。 因为它是没有继承父进程(交互式 shell)的 stdio 的子进程
$ echo "hell" | tty
not a tty

# 还可以通过 echo $- 来查看是不是 interactive,输出带有 `i` 就表示 interactive
$ echo $-
himBHs

如果你在实现一个交互式命令,其调用子命令(指子进程)的时候你又希望其子命令也能与用户有交互,那么你就需要让这个子进程继承父进程的 stdio。比如,nodejs 的 child_process 就有 stdio: 'inherit' 这样的选项。上面的示例 go 程序可以这样改:

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	cmd := exec.Command("vim", ".git/COMMIT_EDITMSG")
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()

	if err != nil {
		fmt.Printf("%v\n", err.Error())
		os.Exit(-1)
	}
}
# 执行这个程序,它就会打开一个 vim 。像不像 git commit 提交的过程?
go run main.go

假如,注释掉 cmd.Stdin = os.Stdin ,那么在 go 语言里 cmd 的 stdio 就是 /dev/null,运行之后程序会非正常退出, exit code 是 1。我之前试过 nodejs 的脚本,子进程等待用户输入,但是却没有继承交互式父进程的 stdio,那么程序就会卡在那里。

所以,自己写脚本、写命令行工具的时候,如果遇到类似的“卡死”问题或者报错应该知道如何处理。总结起来就是不要让需要 interactive 的程序运行在 non-interactive 的环境下。

  • 那些 service,cronjob 是万万不能依赖 interactive 的,它们运行在 non-interactive 环境下,依赖用户输入会导致程序“卡死”;
  • 那些希望与用户进行交互的子进程,要处理好 stdio 继承,免得让它们运行在一个 non-interactive 环境下;

另外 interactive shell (或者说 tty,这里不细分)本身也意味着是一种“常驻进程”,因为它“时刻”在等待着用户进行输入操作。所以一个 docker container 想要一直存活,要嘛以 -d 即守护进程的形式运行,要嘛以 -ttty: true 的形式运行。之前部署 jenkins in k8s 的时候,发现 runner 容器一启动就退出,导致构建中止。我是通过让 runner 容器的启动命令变成 sleep 99d 来保持其常驻的。 而今天才明白,XX的 runner container 是通过 tty: true 来保活的。