0

This is almost the exact same question as in this post, except that I do not want to use eval.

Quick question short, I want to execute the command echo aaa | grep a by first storing it in a string variable Command='echo aaa | grep a', and then running it without using eval.

In the post above, the selected answer used eval. That works for me too. What concerns me a lot is that there are plenty of warnings about eval below, followed by some attempts to circumvent it. However, none of them are able to solve my problem (essentially the OP's). I have commented below their attempts, but since it has been there for a long time, I suppose it is better to post the question again with the restriction of not using eval.

Concrete Example

What I want is a shell script that runs my command when I am happy:

#!/bin/bash
# This script run-this-if.sh runs the commands when I am happy
# Warning: the following script does not work (on nose)
if [ "$1" == "I-am-happy" ]; then
    "$2"
fi

$ run-if.sh I-am-happy [insert-any-command]

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Student
  • 400
  • 3
  • 10
  • 5
    Short answer is: You don't. This seems like an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – melpomene Jun 24 '19 at 21:37
  • 1
    http://mywiki.wooledge.org/BashFAQ/050 – melpomene Jun 24 '19 at 21:38
  • 2
    Why "by first storing it in a string variable"? Once you make that your first step, you force the next step to be just as hazardous as `eval` is. The right answer is to select an answer that doesn't ever require conflation of data and code. – Charles Duffy Jun 24 '19 at 22:14
  • ...if you want to store a command in a way that doesn't require later re-parsing, *don't* store it in a string; store it in a function, or an array. – Charles Duffy Jun 24 '19 at 22:22
  • Reasons to do this: 1) out of curiosity. 2) I want to run commands under certain conditions.. for example `$ run-this-if.sh "i'm happy" "echo aaa | grep a"`. – Student Jun 24 '19 at 22:23
  • @melpomene I read it when I went through the original post, not knowing which part it solves my problem. – Student Jun 24 '19 at 22:25
  • @Student, sounds like you have a use case for exported functions. `foo() { echo aaa | grep a; }; export -f foo; run-this-if "i'm happy" foo`, ensuring that `run-this-if` uses `#!/usr/bin/env bash`, **not** `/bin/sh`. – Charles Duffy Jun 24 '19 at 22:26
  • I'd like to variate the command in the future.. – Student Jun 24 '19 at 22:27
  • @Student, ...that said, `eval` is dangerous **when you're passing it non-constant data**. If it's truly `echo aaa | grep a` *hardcoded in your script*, that's safe to eval. On the other hand, if it's `echo "$something" | grep "$something_else"`, not so much (unless you're very, very careful). – Charles Duffy Jun 24 '19 at 22:28
  • 1
    It is possible to store a command in an array, but only simple commands: you cannot include pipes, redirection, etc, that must be handled by the shell before variable expansion happens. – glenn jackman Jun 24 '19 at 22:28
  • @Student, what part of my example above made you think you couldn't vary the command itself? `if something; then foo() { this; }; else foo() { that; }; fi` -- and of course you can make the command refer to exported environment variables, or to further command-line options. – Charles Duffy Jun 24 '19 at 22:29
  • BTW, there's a bigger problem with your `run-this-if.sh` doing an assignment, insofar as the assignment will happen inside the subprocess that script is invoked it, and thus won't change the parent process. – Charles Duffy Jun 24 '19 at 22:31
  • 1
    Change `"$2"` to `shift; "$@"` and you'll have a much more flexible approach. – Charles Duffy Jun 24 '19 at 22:32
  • 1
    BTW, note also that `==` doesn't work reliably with `#!/bin/sh`, as the only POSIX-specified string comparison operator is `=`. – Charles Duffy Jun 24 '19 at 22:33
  • 2
    I've seen plenty of warnings against texting and using your phone while driving, so can anyone recommend a car charger to let me play a Nintendo Switch behind the wheel instead? – that other guy Jun 24 '19 at 23:27
  • Note that `loop-every 5 echo aaa | grep a` is running `echo` every 5 seconds, but with all the output going to just one copy of `grep`; it's **not** running a new grep every 5 seconds. – Charles Duffy Jun 24 '19 at 23:38
  • 1
    ...by contrast, `loop-every 5 sh -c 'echo "$1" | grep "$2"' _ aaa a` would run a new `echo` *and* a new `grep` every 5 seconds. – Charles Duffy Jun 24 '19 at 23:39
  • Also, note that we frown on answers being edited into questions. If you want to add your own answer, or your own spin on another answer, use the "Add An Answer" button to do so. – Charles Duffy Jun 24 '19 at 23:39
  • 1
    (We also frown on "Edited" markers; a question should be written with the focus on ease-of-reading to someone who's seeing it for the first time, and forcing people to understand its history is counterproductive with regard to that end). – Charles Duffy Jun 24 '19 at 23:40
  • See [Is it better to answer or edit your question to post solution?](https://meta.stackoverflow.com/questions/307553), [Is it OK for users to edit the accepted answer into their question?](https://meta.stackoverflow.com/questions/262806), [What is the appropriate action when the answer to a question is added to the question itself?](https://meta.stackoverflow.com/questions/267434), and many similar duplicates on [meta]. – Charles Duffy Jun 24 '19 at 23:42

1 Answers1

2

Your sample usage can't ever work with an assignment, because assignments are scoped to the current process and its children. Because there's no reason to try to support assignments, things get suddenly far easier:

#!/bin/sh
if [ "$1" = "I-am-happy" ]; then
    shift; "$@"
fi

This then can later use all the usual techniques to run shell pipelines, such as:

run-if-happy "$happiness" \
  sh -c 'echo "$1" | grep "$2"' _ "$untrustedStringOne" "$untrustedStringTwo"

Note that we're passing the execve() syscall an argv with six elements:

  • sh (the shell to run; change to bash etc if preferred)
  • -c (telling the shell that the following argument is the code for it to run)
  • echo "$1" | grep "$2" (the code for sh to parse)
  • _ (a constant which becomes $0)
  • ...whatever the shell variable untrustedStringOne contains... (which becomes $1)
  • ...whatever the shell variable untrustedStringTwo contains... (which becomes $2)

Note here that echo "$1" | grep "$2" is a constant string -- in single-quotes, with no parameter expansions or command substitutions -- and that untrusted values are passed into the slots that fill in $1 and $2, out-of-band from the code being evaluated; this is essential to have any kind of increase in security over what eval would give you.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 2
    Note that changing questions to incorporate answer elements makes it harder for future readers to understand the context of those answers (aka *why* something is being explained, if it looks like the OP already knew it and incorporated it in the question). – Charles Duffy Jun 24 '19 at 22:43