伪终端将不会被分配,因为stdin不是终端

我正在尝试编写一个shell脚本,在远程服务器上创建一些目录,然后使用scp将文件从我的本地机器复制到远程。以下是我目前所了解到的:

ssh -t user@server<<EOT
DEP_ROOT='/home/matthewr/releases'
datestamp=$(date +%Y%m%d%H%M%S)
REL_DIR=$DEP_ROOT"/"$datestamp
if [ ! -d "$DEP_ROOT" ]; then
echo "creating the root directory"
mkdir $DEP_ROOT
fi
mkdir $REL_DIR
exit
EOT


scp ./dir1 user@server:$REL_DIR
scp ./dir2 user@server:$REL_DIR

每当我运行它,我得到这条消息:

Pseudo-terminal will not be allocated because stdin is not a terminal.

剧本永远挂着。

我的公钥在服务器上是可信的,我可以在脚本之外运行所有命令。什么好主意吗?

624035 次浏览

我不知道挂起的原因是什么,但是将命令重定向(或管道)到交互式ssh通常会出现问题。使用command-to-run-as- last-argument样式并在ssh命令行上传递脚本会更健壮:

ssh user@server 'DEP_ROOT="/home/matthewr/releases"
datestamp=$(date +%Y%m%d%H%M%S)
REL_DIR=$DEP_ROOT"/"$datestamp
if [ ! -d "$DEP_ROOT" ]; then
echo "creating the root directory"
mkdir $DEP_ROOT
fi
mkdir $REL_DIR'

(所有这些都在一个巨大的__abc0分隔的多行命令行参数中)。

伪终端消息是因为你的-t要求ssh尝试使它在远程机器上运行的环境看起来像在那里运行的程序的实际终端。你的ssh客户端拒绝这样做,因为它的自己的标准输入不是一个终端,所以它没有办法将特殊的终端api从远程机器传递到你在本地端的实际终端。

你到底想用-t实现什么?

尝试ssh -t -t(或简称ssh -tt)强制伪tty分配,即使stdin不是终端。

参见:终止bash脚本执行的SSH会话

从ssh manpage:

-T      Disable pseudo-tty allocation.


-t      Force pseudo-tty allocation.  This can be used to execute arbitrary
screen-based programs on a remote machine, which can be very useful,
e.g. when implementing menu services.  Multiple -t options force tty
allocation, even if ssh has no local tty.

我添加这个答案是因为它解决了与我遇到的相同错误消息相关的问题。

问题:我已经在Windows下安装了cygwin,并得到这个错误:Pseudo-terminal will not be allocated because stdin is not a terminal

决议:原来我有安装openssh客户端程序和实用程序。正因为如此,cygwin使用的是ssh的Windows实现,而不是cygwin版本。解决方案是安装openssh cygwin包。

还有来自手册的选项-T

禁用伪tty分配

警告消息Pseudo-terminal will not be allocated because stdin is not a terminal.是由于没有为ssh指定命令,而stdin从这里文档重定向。 由于缺少指定的命令作为参数,ssh首先期望一个交互式登录会话(这将需要在远程主机上分配一个pty),但随后必须意识到它的本地stdin不是tty/pty。从here文档重定向ssh的stdin通常需要指定一个命令(例如/bin/sh)作为ssh的参数——在这种情况下,默认情况下不会在远程主机上分配pty 由于没有通过ssh执行的命令需要tty/pty的存在(例如vimtop),所以-t切换到ssh是多余的。 只要使用ssh -T user@server <<EOT ...ssh user@server /bin/bash <<EOT ...,警告就会消失

如果<<EOF没有转义或没有单引号(即<<\EOT<<'EOT'),则在执行ssh ...之前,本地shell将展开这里文档中的变量。结果是,这里文档中的变量将保持为空,因为它们仅在远程shell中定义。

因此,如果$REL_DIR既可以被本地shell访问,又可以在远程shell中定义,则$REL_DIR必须在here文档之外的ssh命令之前定义(下面是版本1);或者,如果使用了<<\EOT<<'EOT',如果ssh命令对stdout的唯一输出是由转义/单引号here文档中的echo "$REL_DIR"生成(下面是$REL_DIR0),则ssh命令的输出可以赋值给REL_DIR

第三种方法是将这里的文档存储在一个变量中,然后将这个变量作为命令参数传递给ssh -t user@server "$heredoc"(下面是版本3)。

最后但并非最不重要的是,检查远程主机上的目录是否成功创建也不是一个坏主意(参见:使用SSH检查远程主机上是否存在文件)。

# version 1


unset DEP_ROOT REL_DIR
DEP_ROOT='/tmp'
datestamp=$(date +%Y%m%d%H%M%S)
REL_DIR="${DEP_ROOT}/${datestamp}"


ssh localhost /bin/bash <<EOF
if [ ! -d "$DEP_ROOT" ] && [ ! -e "$DEP_ROOT" ]; then
echo "creating the root directory" 1>&2
mkdir "$DEP_ROOT"
fi
mkdir "$REL_DIR"
#echo "$REL_DIR"
exit
EOF


scp -r ./dir1 user@server:"$REL_DIR"
scp -r ./dir2 user@server:"$REL_DIR"




# version 2


REL_DIR="$(
ssh localhost /bin/bash <<\EOF
DEP_ROOT='/tmp'
datestamp=$(date +%Y%m%d%H%M%S)
REL_DIR="${DEP_ROOT}/${datestamp}"
if [ ! -d "$DEP_ROOT" ] && [ ! -e "$DEP_ROOT" ]; then
echo "creating the root directory" 1>&2
mkdir "$DEP_ROOT"
fi
mkdir "$REL_DIR"
echo "$REL_DIR"
exit
EOF
)"


scp -r ./dir1 user@server:"$REL_DIR"
scp -r ./dir2 user@server:"$REL_DIR"




# version 3


heredoc="$(cat <<'EOF'
# -onlcr: prevent the terminal from converting bare line feeds to carriage return/line feed pairs
stty -echo -onlcr
DEP_ROOT='/tmp'
datestamp="$(date +%Y%m%d%H%M%S)"
REL_DIR="${DEP_ROOT}/${datestamp}"
if [ ! -d "$DEP_ROOT" ] && [ ! -e "$DEP_ROOT" ]; then
echo "creating the root directory" 1>&2
mkdir "$DEP_ROOT"
fi
mkdir "$REL_DIR"
echo "$REL_DIR"
stty echo onlcr
exit
EOF
)"


REL_DIR="$(ssh -t localhost "$heredoc")"


scp -r ./dir1 user@server:"$REL_DIR"
scp -r ./dir2 user@server:"$REL_DIR"

对于zanco的回答,鉴于shell如何解析命令行,您没有向ssh提供远程命令。要解决此问题,请更改ssh命令调用的语法,以便远程命令由语法正确的多行字符串组成。

可以使用多种语法。例如,由于命令可以通过管道传输到bashsh,也可能是其他shell中,最简单的解决方案是将ssh shell调用与heredocs结合起来:

ssh user@server /bin/bash <<'EOT'
echo "These commands will be run on: $( uname -a )"
echo "They are executed by: $( whoami )"
EOT

注意,执行上面的没有 /bin/bash将导致警告Pseudo-terminal will not be allocated because stdin is not a terminal。还要注意,EOT被单引号括起来,这样bash就可以将这里的文档识别为nowdoc,关闭局部变量插值,这样命令文本就会原样传递给ssh

如果你是管道爱好者,你可以将上面的内容重写如下:

cat <<'EOT' | ssh user@server /bin/bash
echo "These commands will be run on: $( uname -a )"
echo "They are executed by: $( whoami )"
EOT

关于/bin/bash的相同警告也适用于上述情况。

另一种有效的方法是将多行远程命令作为单个字符串传递,使用多层bash变量插值,如下所示:

ssh user@server "$( cat <<'EOT'
echo "These commands will be run on: $( uname -a )"
echo "They are executed by: $( whoami )"
EOT
)"

上述解决方案通过以下方式解决了该问题:

  1. ssh user@server由bash解析,并被解释为ssh命令,后面跟着一个参数user@server,将传递给ssh命令

  2. "开始一个内插的字符串,当它完成时,将包含一个传递给ssh命令的参数,在这种情况下,该参数将被ssh解释为要作为user@server执行的远程命令

  3. $(开始一个要执行的命令,输出被周围的插值字符串捕获

  4. cat是一个命令,输出后面任何文件的内容。cat的输出将被传递回捕获的插值字符串

  5. <<开始一个bash heredoc

  6. 'EOT'指定heredoc的名称为EOT。围绕EOT的单引号'指定heredoc应被解析为nowdoc,这是heredoc的一种特殊形式,其中内容不由bash插入,而是以文字格式传递

  7. <<'EOT'<newline>EOT<newline>之间遇到的任何内容都将被追加到nowdoc输出中

  8. EOT终止nowdoc,导致创建一个nowdoc临时文件并将其传递回调用cat命令。cat输出nowdoc并将输出传递回捕获的插值字符串

  9. )结束要执行的命令

  10. "结束捕获的插值字符串。插值字符串的内容将作为单个命令行参数传递回sshssh将其解释为要作为user@server执行的远程命令

如果你需要避免使用像cat这样的外部工具,并且不介意使用两条语句而不是一条,使用内置的read和heredoc来生成SSH命令:

IFS='' read -r -d '' SSH_COMMAND <<'EOT'
echo "These commands will be run on: $( uname -a )"
echo "They are executed by: $( whoami )"
EOT


ssh user@server "${SSH_COMMAND}"

我在Windows下使用emacs 24.5.1通过/ssh连接到一些公司服务器:user@host时遇到了同样的错误。解决我的问题是将“tramp-default-method”变量设置为“plink”,每当我连接到服务器时,我都会提交ssh协议。你需要安装PuTTY的plink.exe才能工作。

解决方案

  1. M-x自定义变量(然后按Enter)
  2. tramp-default-method(然后再次按Enter键)
  3. 在文本框中输入plink,然后应用并保存缓冲区
  4. 每当我尝试访问远程服务器时,我现在使用C-x-f /user@host:,然后输入密码。现在在Windows上的Emacs下正确地建立了到远程服务器的连接。

在看了很多这样的答案之后,我想分享一下我的解决方案。我只是在heredoc之前添加了/bin/bash,它不再给出错误了。

用这个:

ssh user@machine /bin/bash <<'ENDSSH'
hostname
ENDSSH

而不是这个(给出错误):

ssh user@machine <<'ENDSSH'
hostname
ENDSSH

或者用这个:

ssh user@machine /bin/bash < run-command.sh

而不是这个(给出错误):

ssh user@machine < run-command.sh

额外的:

如果你仍然需要一个远程交互式提示,例如,如果你正在远程运行的脚本提示你输入密码或其他信息,因为前面的解决方案不允许你输入提示。

ssh -t user@machine "$(<run-command.sh)"

如果你还想将整个会话记录在文件logfile.log中:

ssh -t user@machine "$(<run-command.sh)" | tee -a logfile.log

所有相关信息都在现有的答案中,但让我尝试务实的总结:

tl;博士:

    <李> < p > 使用命令行参数:
    ssh jdoe@server '...' < / p >
    • '...'字符串可以跨越多行,这样即使不使用here-document也可以保持代码的可读性:
      ssh jdoe@server ' ... ' < / >
  • 不通过stdin,就像你使用here文档:
    时一样 ssh jdoe@server <<'EOF' # Do NOT do this ... EOF < / p >

传递命令作为参数按原样工作,:

  • 伪终端的问题甚至不会出现。
  • 你不需要exit语句在你的命令结束,因为会话将自动退出后,命令已经处理。
简而言之:通过stdin传递命令是一种与ssh的设计不一致的机制,并且会导致必须解决的问题。

.

.

可选背景信息:

ssh接受命令在目标服务器上执行的机制是命令行参数:最后一个操作数(非选项参数)接受一个包含一个或多个shell命令的字符串。

  • 默认情况下,当最后一个命令完成处理时,这些命令在无人参与的情况下运行,在非交互式 shell中运行,不使用(伪)终端(隐含选项-T),会话自动结束

  • 如果你的命令需要用户交互,比如响应一个交互式提示符,你可以显式地请求创建一个企业(pseudo-tty),一个伪终端,它允许与远程会话交互,使用-t选项;例如:

    • < p > ssh -t jdoe@server 'read -p "Enter something: "; echo "Entered: [$REPLY]"'

    • 注意,交互式read提示符仅与pty一起正确工作,因此需要-t选项。

    • 使用pty有显著的副作用:stdout和stderr是结合,并且都通过stdout报告;换句话说:你失去了常规输出和错误输出之间的区别;例如:

      • < p > ssh jdoe@server 'echo out; echo err >&2' # OK - stdout and stderr separate

      • < p > ssh -t jdoe@server 'echo out; echo err >&2' # !! stdout + stderr -> stdout

在没有此参数的情况下,ssh创建一个交互式 shell -包括当你通过stdin发送命令时,这是问题开始的地方:

  • 对于互动 shell, ssh默认情况下通常分配一个pty(伪终端),如果它的stdin没有连接到(真实)终端,则分配除了

      <李> < p > 通过stdin发送命令意味着ssh的stdin不再连接到终端,因此创建了no pty,并且ssh 相应地警告:
      Pseudo-terminal will not be allocated because stdin is not a terminal. < / p >
    • 即使是-t选项,其明确目的是请求创建pty,在这种情况下not enough :你会得到同样的警告。

      • 有点奇怪的是,你必须double -t选项来强制创建一个pty: ssh -t -t ...ssh -tt ...表明你我是认真的

      • 也许需要这一非常谨慎的步骤的基本原理是事情可能不会像预期的那样发展. b。例如,在macOS 10.12上,与上述命令明显等价的命令,通过stdin提供命令并使用-tt是否正常工作;会话在响应read提示符:
        后卡住 ssh -tt jdoe@server <<<'read -p "Enter something: "; echo "Entered: [$REPLY]"' < / p >

在不太可能发生的情况下,你想作为参数传递的命令使命令行对你的系统来说太长(如果它的长度接近getconf ARG_MAX -参见这篇文章),考虑先以脚本的形式将代码复制到远程系统(例如,使用scp),然后发送一个命令来执行该脚本。

在紧急情况下,使用-T,并通过stdin提供命令,后面有一个exit命令,但请注意,如果你还需要交互功能,使用-tt代替-T可能无法工作。