不肯轻易退出的终端环境

我通常都使用 Windows Terminal (wt) 管理不同的终端环境,比如 MSYS2 的环境。

wt 相比 MSYS2 打包的 mintty 有一个问题:终端里最后一个命令退出代码非零的情况,按下 <kbd>Ctrl+d</kbd> 可能会无法自动关闭标签页,而是显示

注销
终止批处理操作吗(Y/N)?

并等待用户输入。更恼人的是,这个输入必须以 y 或者 n 开头才能关闭标签页,所以不能留空,也不能输入任意字符。

当前配置

查看一下终端环境的 profiles,我的配置如下

{
    "closeOnExit": "always",
    "commandline": "D:\\msys64\\msys2_shell.cmd -defterm -here -no-start -clang64 -shell bash",
    "guid": "{some-random-guid-value}",
    "hidden": false,
    "icon": "D:\\msys64\\clang64.ico",
    "name": "MSYS2: clang64",
    "startingDirectory": "D:\\msys64\\home\\username"
}

可以看出,已经设置了总是在退出时关闭标签页,那么不能关闭的原因就应该是还没真正退出。我们的 commandline 是一个 .cmd 批处理脚本,结合前面的询问信息,确定就是在退出这个批处理时,cmd.exe 想要我们的答复。

思考了一下,我们不通过这个脚本,直接把脚本实质运行的命令放在这里,是不是就不用等待这个退出确认了?修改一下试试看吧。

太长不看点这里

分析命令行作用

defterm

这是我的命令行第一个参数,看看 msys2_shell.cmd,它到底做了什么?

if "x%~1" == "x-defterm" shift& set /a msys2_shiftCounter+=1& set MSYSCON=defterm& goto :checkparams

它将移除已解析的当前参数,设置变量 MSYSCONdefterm 然后返回参数检查的开头。这里的实质作用就是设置了变量,看看这个变量会起到什么作用吧

if "x%MSYSCON%" == "xdefterm" goto startsh

如果变量是 defterm 直接去 startsh 标签,再看看 startsh 的定义

:startsh
set MSYSCON=
if not defined MSYS2_NOSTART (
  start "%CONTITLE%" "%WD%\%LOGINSHELL%" -l !SHELL_ARGS!
) else (
  "%WD%\%LOGINSHELL%" -l !SHELL_ARGS!
)
exit /b %ERRORLEVEL%

清除了这个变量,然后根据 MSYS2_NOSTART 的定义执行不同的命令;不难看出,这里应该就是真正启动终端环境的地方了。那么,MSYS2_NOSTART 是如何定义的呢?我们暂且按下不表。

here

这是第二个参数,看看它是怎么解析的吧

if "x%~1" == "x-here" shift& set /a msys2_shiftCounter+=1& set CHERE_INVOKING=enabled_from_arguments& goto :checkparams

仅仅是设置了 CHERE_INVOKING 变量为 enabled_from_arguments,之后的脚本也没有再使用这个变量,可以猜测,它的值会由最终执行的命令从环境变量中获取,并影响其初始目录。

no-start

if "x%~1" == "x-no-start" shift& set /a msys2_shiftCounter+=1& set MSYS2_NOSTART=yes& goto :checkparams

第三个参数则设置了 MSYS2_NOSTART 变量,我们回到刚才 startsh 的分析,应该是执行 else 的操作 "%WD%\%LOGINSHELL%" -l !SHELL_ARGS!。这里有三个不同的变量,先看看 WD

set "WD=%__CD__%"
if NOT EXIST "%WD%msys-2.0.dll" set "WD=%~dp0usr\bin\"

如果当前目录有 msys-2.0.dllWD 就是当前目录,否则设置为该脚本所在目录的子目录 usr\bin\。行吧,对于我的情况,其实可以写死为 D:\msys64\usr\bin\ 了。其他两个变量再一次按下不表。

clang64

第四个参数代表了终端环境,MSYS2 有几个不同的环境,这里使用了 Clang x64

if "x%~1" == "x-clang64" shift& set /a msys2_shiftCounter+=1& set MSYSTEM=CLANG64& goto :checkparams

它设置了 MSYSTEM 变量为 CLANG64,这个变量会传递给执行的命令,并影响最终启动的环境。除了这一点,其实这个变量在批处理脚本中还影响了 CONTITLECONICON 的设置,不过前文 startsh 的分析中,我们得知,它们俩没有被真正使用。

-shell bash

最后一个参数设置了启动的 shell 为 bash,它就是 startsh 里提到的 LOGINSHELL

还有什么

前面我们还剩下一个变量 SHELL_ARGS 没分析,来看看代码

set msys2_full_cmd=%*
for /f "tokens=%msys2_shiftCounter%,* delims=,;=	 " %%i in ("!msys2_full_cmd!") do set SHELL_ARGS=%%j

把所有命令行参数以逗号、分号、等号、制表符、空格为分隔符拆分,将前文未解析的部分通通塞给 SHELL_ARGS,这将作为 bash 的参数。不过我这里的情况,这个变量为空。

组合为一条命令

将前文分析出的内容结合为一条命令,应该是什么呢?首先,我们希望 bash 退出后可以关闭终端,然后,我们还有环境变量需要传递给 bash,因此最终的配置如下

"commandline": "cmd.exe /c \"set CHERE_INVOKING=enabled_from_arguments && set MSYSTEM=CLANG64 && D:\\msys64\\usr\\bin\\bash.exe -l\""

它使用了 /ccmd 执行之后的命令,结束后退出;在其中设置了我们所需的环境变量,最后启动 bash 登录。