我正在尝试编写一个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.

剧本永远挂着。

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


当前回答

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

问题:我在Windows下安装了cygwin,并得到这个错误:伪终端将不会被分配,因为stdin不是终端

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

其他回答

我不知道挂起的原因是什么,但是将命令重定向(或管道)到交互式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'

(所有这些都包含在一个以'分隔的多行命令行参数中)。

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

你想用-t实现什么呢?

在看了很多这样的答案之后,我想分享一下我的解决方案。我只是在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

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

可以使用多种语法。例如,由于命令可以通过管道传输到bash和sh,也可能是其他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的情况下执行上述操作将导致警告伪终端将不会被分配,因为stdin不是终端。还要注意,EOT由单引号包围,这样bash就可以将heredoc识别为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
)"

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

ssh user@server is parsed by bash, and is interpreted to be the ssh command, followed by an argument user@server to be passed to the ssh command " begins an interpolated string, which when completed, will comprise an argument to be passed to the ssh command, which in this case will be interpreted by ssh to be the remote command to execute as user@server $( begins a command to be executed, with the output being captured by the surrounding interpolated string cat is a command to output the contents of whatever file follows. The output of cat will be passed back into the capturing interpolated string << begins a bash heredoc 'EOT' specifies that the name of the heredoc is EOT. The single quotes ' surrounding EOT specifies that the heredoc should be parsed as a nowdoc, which is a special form of heredoc in which the contents do not get interpolated by bash, but rather passed on in literal format Any content that is encountered between <<'EOT' and <newline>EOT<newline> will be appended to the nowdoc output EOT terminates the nowdoc, resulting in a nowdoc temporary file being created and passed back to the calling cat command. cat outputs the nowdoc and passes the output back to the capturing interpolated string ) concludes the command to be executed " concludes the capturing interpolated string. The contents of the interpolated string will be passed back to ssh as a single command line argument, which ssh will interpret as the remote command to execute as 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}"

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

tl; diana:

使用命令行参数传递命令: SSH jdoe@server“…” “…'字符串可以跨越多行,所以即使不使用here-document,你也可以保持代码的可读性: SSH jdoe@server“…” 不要通过stdin传递命令,就像你使用here-document一样: ssh jdoe@server <<'EOF' #不这样做…EOF

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

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

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


可选背景信息:

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

By default, these commands run unattended, in a non-interactive shell, without the use of a (pseudo) terminal (option -T is implied), and the session automatically ends when the last command finishes processing. In the event that your commands require user interaction, such as responding to an interactive prompt, you can explicitly request the creation of a pty (pseudo-tty), a pseudo terminal, that enables interacting with the remote session, using the -t option; e.g.: ssh -t jdoe@server 'read -p "Enter something: "; echo "Entered: [$REPLY]"' Note that the interactive read prompt only works correctly with a pty, so the -t option is needed. Using a pty has a notable side effect: stdout and stderr are combined and both reported via stdout; in other words: you lose the distinction between regular and error output; e.g.: 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发送命令时,这就是问题开始的地方:

For an interactive shell, ssh normally allocates a pty (pseudo-terminal) by default, except if its stdin is not connected to a (real) terminal. Sending commands via stdin means that ssh's stdin is no longer connected to a terminal, so no pty is created, and ssh warns you accordingly: Pseudo-terminal will not be allocated because stdin is not a terminal. Even the -t option, whose express purpose is to request creation of a pty, is not enough in this case: you'll get the same warning. Somewhat curiously, you must then double the -t option to force creation of a pty: ssh -t -t ... or ssh -tt ... shows that you really, really mean it. Perhaps the rationale for requiring this very deliberate step is that things may not work as expected. For instance, on macOS 10.12, the apparent equivalent of the above command, providing the commands via stdin and using -tt, does not work properly; the session gets stuck after responding to the read prompt: ssh -tt jdoe@server <<<'read -p "Enter something: "; echo "Entered: [$REPLY]"'


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

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

The warning message Pseudo-terminal will not be allocated because stdin is not a terminal. is due to the fact that no command is specified for ssh while stdin is redirected from a here document. Due to the lack of a specified command as an argument ssh first expects an interactive login session (which would require the allocation of a pty on the remote host) but then has to realize that its local stdin is no tty/pty. Redirecting ssh's stdin from a here document normally requires a command (such as /bin/sh) to be specified as an argument to ssh - and in such a case no pty will be allocated on the remote host by default.

由于通过ssh执行的命令不需要tty/pty(如vim或top),所以切换到ssh的-t是多余的。 只需使用ssh -T user@server <<EOT…或ssh user@server /bin/bash <<EOT…警告就会消失。

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

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

第三种方法是将here文档存储在一个变量中,然后将该变量作为命令参数传递给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"