0

I'm coding a Bash script to automate tasks across multiple servers.

I am logging to a Centos 7 machine over SSH to run some editor (nano, vi, ...)

ssh -tt centos@... '/bb/Conf edit'

The /bb/Conf edit is basically just vi /bb/conf.yaml.

When I run the SSH command from my shell, it works fine. However, when the same SSH command is ran from a Bash script inside a while read ...; do loop, the editor has wrong size (80x40 I guess) and seems to ignore the keys I press - i.e. in nano, Ctrl+x doesn't do anything. The only key that works is Ctrl+c which closes the connection.

I thought this is something related to the TERM variable, as per this, so I tried to add export TERM=xterm or TERM=rxvt to /bb/Conf or the place calling the SSH. The variable is in fact set in the target environment (I've tried echo $TERM right before vi). But the terminal still misbehaved.

Then I have tried to put just that single command ssh ... to a new script. When running that, the editor worked fine.

After a while I found out that it works outside a while read loop, but not inside. I assume that the editors do some stdin/stdout magic and then read somehow breaks that.

Is there a way to run an editor like vi or nano from within a loop?

(The purpose in my case is to allow the users to edit files on multiple servers.)

1
  • It turns out that somehow it it broken by while read HOST_; do which the ssh is inside. I will rewrite the question. Commented May 1, 2018 at 2:33

1 Answer 1

2

That's because both read and ssh are reading from the same input stream. The solution is to use a different file descriptor for the while read loop:

while IFS= read -r -u3 line; do
    ssh ...
done 3< file

Here, we're using file descriptor 3 instead of stdin.


Lengthy pipelines can be hard to read and maintain, but you can use whitespace constructively: newlines are allowed following | and && and ||. Also, parentheses introduce a subshell which contains an arbitrary script, so indentation helps.

while read -u3 line; do
    : do stuff here that needs to read from stdin
done 3< <(
    command 1 of the pipeline |
    command 2 |
    command 3
)

That's clean and readable. The downside is that it puts the last part of the pipeline (the while loop) first, so the code kind of flows backwards.

Sign up to request clarification or add additional context in comments.

5 Comments

If the loop is reading output from a command (or pipeline), you can use done 3< <(somecmdorpipeline) instead of a regular pipeline. Note that this is a bash-only feature, so use a bash shebang in the script, and don't override it by running it with the sh command.
Yes, it's from a pipeline which is a bit lengthy. I assume there's no way to have this with the | left-to-right syntax?
Also maybe I could use some other loop? The lines it consumes are just hostnames.
What is the IFS= for? I mean, I know it sets the separator but what does it do here?
That's the recipe for reading a line from a file exactly. If you don't use IFS= then any leading/trailing whitespace will be ignored. IFS= read -r line sets the IFS variable to the empty string only for the duration of the read command. The -r option instructs read to handle backslashes as just normal characters instead of the start of an escape sequence.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.