2

I'm writing a script that functions as a "talking terminal" (you type in commands and it says the output) and so far my code is:

#!/bin/bash
while [ 1=1 ]; do
echo -n "~>"
read COMMAND
espeak "$($COMMAND)"
done

and it works for simple commands:

bash$ ./talkingterminal.sh
~> ls
# espeak says "talkingterminal.sh"

but when I use pipes etc:

bash$ ./talkingterminal.sh
~>ip addr | grep inet
Command "|" is unknown, try "ip addr help".
~>

and that command works in bash and gives the expected output. any help? thanks, Martin

MartinUbuntu
  • 347
  • 2
  • 5
  • Not an answer, but for an explanation, I'd say it's because command separation happens in the initial word splitting phase, so expanding the command can't make one command into a pipeline of two anymore. Ref: [How to execute command stored in a variable?](http://stackoverflow.com/a/4668800/12274) Hence `eval`. – JB. Sep 28 '15 at 14:14

3 Answers3

2

This is a rare case where eval is appropriate; you just putting a thin wrapper around what is already unlimited access to the command line. More importantly, you aren't modifying COMMAND in any unexpected ways. What the user types is what will be executed.

#!/bin/bash
while : ; do
    IFS= read -rp "~> " COMMAND
    espeak "$(eval "$COMMAND")"
done

A few notes:

  1. The read builtin in bash can display a custom prompt using the -p option.
  2. Use the : or true commands, which always succeed, to set up an infinite loop.
  3. Use the -r option and an empty value for IFS to ensure that COMMAND is set to the exact line typed by the user.
  4. Quote $COMMAND so that the entire string is passed to eval without any previous shell parsing.
  5. Note that this still cannot handle multi-line input in the same fashion as the shell itself. (For example, a line like for x in 1 2 3; do will not prompt you for the body of the loop.)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    Downvoters: this is what `eval` is actually *intended* for. The OP already wants to execute arbitrary code, rather than assuming `eval` will only execute the intended subset of code it can execute. – chepner Sep 28 '15 at 14:19
0

A simple solution is to use eval:

#!/bin/bash
while [ 1=1 ]; do
echo -n "~>"
read COMMAND
espeak "$(eval $COMMAND)"
done

Then you can print output to the shell as well easily:

#!/bin/bash
while [ 1=1 ]; do
echo -n "~>"
read COMMAND
OUTPUT="$(eval $COMMAND)"
echo $OUTPUT
espeak "$($OUTPUT)"
done
MartinUbuntu
  • 347
  • 2
  • 5
-1

Have you tried to use the "-r" option? This option avoids interpreting special characters. Additionally you have to remove the external $() in the line:

espeak "$($COMMAND)"

It should be:

espeak "$(COMMAND)"
rkachach
  • 16,517
  • 6
  • 42
  • 66
  • `read` is not the problem; the problem is that expanding `$COMMAND` like this passes `|` as an argument to `ipaddr` rather than parsing the entire string as a pipeline. – chepner Sep 28 '15 at 14:53
  • `$(COMMAND)` would attempt to run the literal command "COMMAND", not the value of the `COMMAND` parameter. – chepner Sep 28 '15 at 14:53