关于Linux:不会分配伪终端,因为stdin不是终端

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

每次运行它,我都会收到以下消息:

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

剧本永远挂着。

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


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

另请参见:终止由bash脚本执行的ssh会话

从ssh手册页:

1
2
3
4
5
6
-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.


也可从手动选择-t

Disable pseudo-tty allocation


根据Zanco的回答,考虑到shell如何解析命令行,您不会向ssh提供远程命令。要解决此问题,请更改ssh命令调用的语法,以便远程命令由语法正确的多行字符串组成。

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

1
2
3
4
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将heredoc识别为nowdoc,关闭局部变量插值,以便命令文本按原样传递给ssh

如果您是管道爱好者,可以将上述内容改写为:

1
2
3
4
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变量插值,如下所示:

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

上述解决方案以以下方式解决此问题:

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

  • "开始一个插入字符串,该字符串完成后,将包含一个要传递给ssh命令的参数,在这种情况下,ssh将解释为要作为user@server执行的远程命令。

  • $(开始执行命令,输出被周围的内插字符串捕获。

  • cat是一个命令,用于输出以下任何文件的内容。cat的输出将被传递回捕获的内插字符串。

  • <<开始了一场狂欢

  • 'EOT'规定本文件名称为EOT。围绕eot的单引号'规定,Heredoc应被解析为nowdoc,这是Heredoc的一种特殊形式,其中内容不被bash插入,而是以文本格式传递。

  • <<'EOT'EOT之间遇到的任何内容都将添加到nowdoc输出中。

  • EOT终止nowdoc,从而创建一个nowdoc临时文件并传递回调用的cat命令。cat输出nowdoc并将输出传回捕获的内插字符串

  • )结束要执行的命令

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

  • 如果您需要避免使用诸如cat之类的外部工具,并且不介意使用两个语句而不是一个,那么可以使用read内置的Heredoc来生成ssh命令:

    1
    2
    3
    4
    5
    6
    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下安装了cygwin,并得到了这个错误:Pseudo-terminal will not be allocated because stdin is not a terminal

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


    警告消息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。

    由于没有需要TTY/PTY(如vimtop的命令可通过ssh执行,因此-t切换到ssh是多余的。只要使用ssh -T user@server <ssh user@server /bin/bash <,警告就会消失。

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

    因此,如果$REL_DIR既可以被本地shell访问,又可以在远程shell中定义,那么在使用ssh命令(下面的版本1)之前,$REL_DIR必须在本文档之外定义;或者,如果使用<<\EOT<<'EOT'命令,那么如果仅在将ssh命令放到stdout中,由echo"$REL_DIR"在转义/单引号文档(下面的版本2)中生成。

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

    而且,最后但并非最不重要的是,检查远程主机上的目录是否已成功创建(请参阅:检查具有ssh的远程主机上是否存在文件)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    # 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"


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

    DR:

    • 使用命令行参数传递要运行的命令:ssh jdoe@server '...'

      • '...'字符串可以跨多行,因此即使不使用here文档,也可以保持代码的可读性:ssh jdoe@server '
        ...
        '
    • 不要通过stdin传递命令,就像使用here文档时一样:ssh jdoe@server <<'EOF' # Do NOT do this ... EOF

    将命令作为参数传递可以按原样工作,并且:

    • 伪终端的问题甚至不会出现。
    • 您不需要在命令末尾使用exit语句,因为在处理完命令后,会话将自动退出。

    简而言之:通过stdin传递命令是一种与ssh的设计不一致的机制,并导致必须解决的问题。如果你想了解更多,请继续阅读。

    可选背景信息:

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

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

    • 如果您的命令需要用户交互(如响应交互提示),您可以使用-T选项明确请求创建一个支持与远程会话交互的伪终端pty(pseudo tty);例如:

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

      • 请注意,交互式read提示只适用于一个pty,因此需要-T选项。

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

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

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

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

    • 对于交互shell,默认情况下,ssh通常分配一个pty(伪终端),除非其stdin未连接到(real)终端。

      • 通过stdin发送命令意味着ssh的stdin不再连接到终端,因此不会创建pty,ssh会相应地警告您:Pseudo-terminal will not be allocated because stdin is not a terminal.

      • 在这种情况下,即使是明确目的是请求创建一个PTY的-T选项也不够:您将得到同样的警告。

        • 有点奇怪的是,你必须加倍选择-T强制创建一个私人公司:ssh -t -t ...ssh -tt ...表明你真的,真的是认真的。

        • 也许需要这个深思熟虑的步骤的理由是事情可能不会像预期的那样工作。例如,在MacOS 10.12上,通过stdin和使用-tt提供命令的上述命令的明显等价物不能正常工作;会话在响应read提示后被卡住:ssh -tt jdoe@server <<<'read -p"Enter something:"; echo"Entered: [$REPLY]"'

    在不太可能的情况下,您希望作为参数传递的命令会使命令行对您的系统太长(如果其长度接近getconf ARG_MAX—请参阅本文),请考虑首先以脚本的形式(例如,scp将代码复制到远程系统,然后发送命令来执行该脚本。

    在紧急情况下,使用-T并通过stdin提供命令,附带一个exit命令,但请注意,如果您还需要交互功能,使用-tt代替-T可能不起作用。


    我不知道挂起是从哪里来的,但是将(或管道)命令重定向到交互式ssh通常是解决问题的方法。使用command-to-run-as-a-last参数样式并在ssh命令行上传递脚本更为健壮:

    1
    2
    3
    4
    5
    6
    7
    8
    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'

    (全部在一个巨大的'分隔的多行命令行参数中)。

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

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


    在阅读了大量这些答案之后,我想我会分享我的解决方案。我所添加的是在Heredoc之前的/bin/bash,它不再给出错误。

    使用此:

    1
    2
    3
    ssh user@machine /bin/bash <<'ENDSSH'
       hostname
    ENDSSH

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

    1
    2
    3
    ssh user@machine <<'ENDSSH'
       hostname
    ENDSSH

    或使用此:

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

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

    1
    ssh user@machine < run-command.sh

    额外的:

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

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

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

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

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

    解决方案

  • M-X自定义变量(然后按Enter键)
  • 不定期默认方法(然后再次按Enter键)
  • 在文本字段上放置plink,然后应用并保存缓冲区
  • 每当我试图访问远程服务器时,我现在使用c-x-f/user@host:,然后输入密码。现在已在Windows上的Emacs下正确连接到远程服务器。

  • ssh-t foobar@localhost yourscript.pl