-3

To avoid the XY-problem issue:

X problem: given an interactive TUI program, how can I wrap it into a non-interactive one (a batch script which accepts some inputs in specific format from stdin and output to stdout in specific format) in a suggested way.

Y problem: I'm trying to use expect to wrap passphrase2pgp into a batch script, but met some trouble when dealing with STDIN & STDOUT with pipe.

(PS: I prefer some advice for this kind of problems than a workaround of specific example, since it's not a complicated job, I think there must be some simple and direct way to achieve it, no tricks and workaround. But I'm not sure whether the implementation details of the be wrapped programs will make things very different, anyway, pointing this out -- if it is the fact -- will also be very helpful)

Suppose I want to wrap some interactive program (e.g. passphrase2pgp, or ssh-keygen which is more well-known) into an non-interactive script named my-keygen, so that I can use it like

printf pwd | my-keygen | gpg --import

In this case, my-keygen is expected to receive password from piped STDIN, and output the private key to STDOUT, which will be piped to gpg --import.

But use passphrase2pgp directly like printf pwd | passphrase2pgp --uid alice --repeat 0 | gpg --import doesn't work at all, since it will still prompt for the passphrase interactivly.

This brings me to use expect, following is my-keygen using expect:

(if you have nix installed, change "expect" in shebang to "nix-shell" so you can run it directly without install passphrase2pgp)

#!/usr/bin/env expect
#! nix-shell -p passphrase2pgp -p expect -i expect

spawn passphrase2pgp --uid alice --repeat 0 --armor

expect "passphrase:"

interact

But this script still doesn't work, when used like printf pwd | my-keygen, it exit too early without printing anything out.

And I find it works when I answer questions from tty, it just does not work if the STDIN is from pipe.

I searched a lot and found something useful:

which let me know that there is an EOF in the piped content which will cause the interactive program exit without finishing its job.

A minimal example to reproduce what happened is:

  1. create a file named test with following content

    #!/usr/bin/env expect
    
    spawn bash
    
    interact
    
  2. run chmod +x ./test to make it executable

  3. run echo 'echo hello' | ./test and confirm that hello is not printed as expected

  4. run echo 'echo hello' | bash and confirm that hello is printed as expected this way

So is there any solution to make my-keygen work as expected? And to be more general, what is the suggested way to such a thing?


(edited 2022/10/11)

The exit too early issue of the expect version is fixed (thanks to many helpful answers): just read lines in a while loop and send them one-by-one, and NOT use the interact command of expect at the end of the script.

But in this way, the output of the spawned process is not printed to stdout, if we need to pipe its STDOUT to another program, like gpg --import, then we need to control the STDOUT of my-keygen to only print the armored key, and no extra stuff. It will be better if your solution shows how to achieve that.

(I already received some suggestion that expect is not a good choice for this purpose - thanks @pynexj - so feel free to post your answer if you have better solution which is not based on expect.)

luochen1990
  • 3,689
  • 1
  • 22
  • 37
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/248825/discussion-on-question-by-luochen1990-how-can-i-wrap-an-interactive-program-into). – Samuel Liew Oct 15 '22 at 15:10

2 Answers2

2

Use while read construction like this:

#!/bin/bash

while read -r input; do
     opts+=("$input")
done

echo "${opts[@]}"

Testing:

$ echo -e "myinfo1\nmyinfo2\nmysecret3" | ./test
myinfo1 myinfo2 mysecret3

Using with ssh-keygen:

$ ssh-keygen --help
unknown option -- -
usage: ssh-keygen [-q] [-b bits] [-C comment] [-f output_keyfile] [-m format]
                  [-t dsa | ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa]
                  [-N new_passphrase] [-O option] [-w provider]
       ssh-keygen -p [-f keyfile] [-m format] [-N new_passphrase
...

ssh-keygen -p -f "${opts[0]}" -m "${opts[1]}" -N "${opts[2]}"
Ivan
  • 6,188
  • 1
  • 16
  • 23
1

The way you're echoing your string, the newlines are being sent as literal characters and not being interpreted as newlines so there is only one line of stdin being passed to expect that gets completely consumed the first time you read stdin. There are a couple ways to fix this:

  1. Use echo -e
echo -e "myinfo1\nmyinfo2\nmysecret3" | my-keygen
  1. ANSI-C Quoting
echo "myinfo1"$'\n'"myinfo2"$'\n'"mysecret3" | my-keygen
  1. Using actual newlines
echo "myinfo1
myinfo2
mysecret3" | my-keygen
  1. A here-document
my-keygen <<EOF
myinfo1
myinfo2
mysecret3
EOF
tjm3772
  • 2,346
  • 2
  • 10
  • I added a minimal example to reproduce and explain what happend to my script, could you please have a try? – luochen1990 Oct 09 '22 at 03:27
  • 2
    You are presenting `echo -e` as the first option where really it should be the absolutely last one for completeness' sake if at all; and then you should definitely include `printf`, which is the proper solution to problems where maybe you would have considered `echo -e` – tripleee Oct 12 '22 at 16:22
  • @tripleee Could you have a try about `echo abc | passphrase2pgp --uid bob` or `echo -e abc | passphrase2pgp --uid bob` ? it just doesn't work. – luochen1990 Oct 13 '22 at 05:50
  • Now I find out that the problem is about `EOF` causing `interact` exit. And thank you for pointing out the `EOL` issue of `echo`, but it seems not the key of the problem I met here – luochen1990 Oct 13 '22 at 06:19