8 Feb 2012

Running interactive programs uninteractively in Bash scripts

Bash scripts are great for automating tasks. Some challenges arise though when you want to automate a process that involves programs that ask for user input by keyboard. You know, the kind of program that keeps asking things like: "Are you sure you want to do this (y/n)?". Most programs accept arguments that enable some kind of "non-interactive mode" for that. For example,


rm -i somefile
asks for confirmation before deleting somefile, while

rm -f somefile
does not. This of course makes the latter a lot more suitable for use in automated scripts. Unfortunately, not all programs provide these kind of arguments. What to do with them?

One option is to use redirection and echo the input into them, like so:


echo "yes" | rm -i somefile
or, if you want to remove a lot of files (and for some reason don't want to replace the -i argument with -f), with the brilliant command yes:

yes | rm -i somefile*

Things start to look ugly though when you have to provide a lot of different input texts. Say you have a program generate_random_files which, well, generates random files after asking in sequence:

  1. How many files?
  2. Size of files?
  3. Are you sure (y/n)?
Suppose our answer to the first question is 2, to the second question 4, and the third one y. Providing the input could be accomplished by:

/bin/echo -e "2\n4\ny" | generate_random_files
but it doesn't look particularly readable. Another option would be to create a text file input.txt with contents:
2
4
y
and then invoke the program with redirected input from this file:

generate_random_files < input.txt

This gives you one more file to manage though. Also, it makes it clumsy to use Bash variables in the input. For example, if you want a file size not of 4, but of some variable $calculatedSize, then you would have to manipulate the text file from within your script before calling generate_random_files.

An elegant way to do it is to use what is called "here documents", which allows you to basically put the literal contents of input.txt right in your script, eliminating the need for a separate file and allowing use of variables, while keeping things reasonably human-readable. The syntax is: program name followed by << followed by a unique delimiter of your own choosing, then on a new line the text that serves as input for the program, and then on a new line (and not preceded by any spaces or tabs!) the delimiter again. Like in this little script:


#!/bin/bash
echo "Starting generate_random_files..."
generate_random_files <<TextForInput
2
4
y
TextForInput
echo "Finished!"
And here's the same example, but using a variable which holds the size in bytes, calculated based on the argument $1 that provides the size in kilobytes:

#!/bin/bash
sizeInKb=$1
calculatedSize=$((1024*$sizeInKb))
echo "Starting generate_random_files..."
generate_random_files <<TextForInput
2
$calculatedSize
y
TextForInput
echo "Finished!"

2 comments:

  1. Hi Thanks for you post.

    I think in your example:
    echo "2\n4\ny" | generate_random_files

    you have to use `echo -e` to make it work.

    ReplyDelete
  2. Thanks for your comment. You're right, but there's an interesting nuance: some shells have a built-in version of echo. Tcsh's echo for example doesn't recognize the `-e` option. It just prints the `-e` (and it appears to interpret backslashes by default). So I changed the line to explicitly use /bin/echo with -e option, which works from all shells.

    ReplyDelete