0
checkcommand() {
   $1
}

checkcommand "which git || apt install git"

it will fail. How can I make it work and get the output?

D C
  • 417
  • 6
  • 13
  • 2
    [BashFAQ #50](http://mywiki.wooledge.org/BashFAQ/050) has more-than-passing relevance. – Charles Duffy Nov 06 '19 at 16:12
  • 2
    Re `which`, also see [How to check if a program exists from a Bash script?](https://stackoverflow.com/q/592620/608639). You should use `command -v`, not `which`. `command -v` takes aliases and paths into account. – jww Nov 06 '19 at 16:51

3 Answers3

3

You just need to wrap the string in eval keyword

checkcommand() {
  eval "$1"
}

Quoting it ("$1", not $1) ensures that the exact string is evaluated as code; otherwise, it gets split into words, each word is expanded as a glob, and the resulting words are concatenated back together; if you're lucky, this didn't change the meaning of your command -- but one may not always be lucky, so it's always best to quote your expansions unless you explicitly know why you're leaving them unquoted in any contrary case.

See the man page for POSIX eval.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Ruelos Joel
  • 2,209
  • 3
  • 19
  • 33
  • would this work if the commands (& args) contained embedded spaces? – Darren Smith Nov 06 '19 at 16:12
  • @DarrenSmith, it works if the command's literal contents are able to be successfully parsed as bash code, whether or not spaces are present. – Charles Duffy Nov 06 '19 at 16:13
  • its worth remembering there are dangers with eval: https://stackoverflow.com/questions/17529220/why-should-eval-be-avoided-in-bash-and-what-should-i-use-instead – Darren Smith Nov 06 '19 at 16:16
  • 1
    Yup -- it should only be used with a hardcoded string hand-vetted to be safely parsed as code. In the OP's use case here, they *do* provide precisely that. BTW, I'd suggest [BashFAQ #48](https://mywiki.wooledge.org/BashFAQ/048) as an ideal such warning. – Charles Duffy Nov 06 '19 at 16:19
  • @DarrenSmith, ...btw, I edited this answer with an explanation of why the quoting is necessary -- does that help wrt. the question about how spaces &c. are handled? – Charles Duffy Nov 06 '19 at 16:30
  • @Charles, yes I think its good to bring attention to this aspect of the solution. – Darren Smith Nov 06 '19 at 16:37
  • thanks! Actually, the problem is more complex: https://stackoverflow.com/questions/58740831/how-to-pass-a-nested-command-to-function-in-a-command-line-to-ssh – D C Nov 07 '19 at 02:03
1

Don't try to run a single string as if it were a command -- pass an explicit argument list instead. That gives you the flexibility of picking whichever usage mode works for you:

## first, define your function as:
checkcommand() {
   "$@"   # expands to "$1" "$2" "$3" ...etc, for all arguments that actually exist
}

## then, you can pass it any simple command...
type git || checkcommand apt install git

## ...or you can use it with eval...
checkcommand eval "type git || apt install git"

## ...or you can pass it a function with an argument list...
installPkg() { for pkg; do type "$pkg" || apt install "$pkg"; done; }
checkcommand installPkg git tig gcc

## ...or you can tell it to invoke a whole new shell instance...
checkcommand sh -c 'type git || apt install git'

For a detailed description of why treating a string as if it were a command is doomed to fail (in sufficiently complex use cases), see BashFAQ #50.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • my concern with this is it leads to noisy string quoting, e.g. if I want to run & check `echo "two spaces"` (assume two spaces between these words) , I'd have to escape the quote chars: `checkcommand eval "echo \"two spaces\""` ... this gets messy – Darren Smith Nov 06 '19 at 18:20
  • It's only the `eval` option (of the three choices that using `"$@"` enables) that requires any such quoting. Just `checkcommand echo "two spaces"` works fine. And `checkcommand eval 'echo "two spaces"'`, of course, is a lower-overhead eval-based option (with [all of `eval`'s usual caveats](https://mywiki.wooledge.org/BashFAQ/048)). – Charles Duffy Nov 06 '19 at 18:22
  • okay, nice one of the alternative works without soucre code adornments, but I think there is a lot to mentally manage here, e.g., when to use eval, how to deal with spaces etc. – Darren Smith Nov 06 '19 at 18:30
  • "One"? I'd argue that there aren't any "adornments" in the function case either; it's only `eval` that's a general bad idea (but necessary if we want shell syntax to be used, which the OP explicitly asked for). – Charles Duffy Nov 06 '19 at 18:33
0

I'm going to suggest an alternative approach, I acknowledge doesn't answer directly.

I suggest this approach because it can be complex to pass command & args and aggregated groups of commands & args to another function to be executing and checked.

Instead a simpler approach is execute your commands and then run a check function after that, e.g.

check_error()
{
    lasterr=$?
    msg="$*"

    if [ "${lasterr}X" != "0X" ];
    then
        echo "command failed, errcode ${lasterr}, $msg"
        exit 1
    fi
}

...

which git || apt install git
check_error "git failed"

... note that check_error function must be the next statement immediately after the commands you wish to check.

Darren Smith
  • 2,261
  • 16
  • 16
  • 1
    The `"${foo}X"` hack is only needed for ancient, 1970s-vintage shells. There hasn't been any reason to use it whatsoever since at least the early 90s, so long as deprecated constructs like using `-a` and `-o` to build compound test commands are avoided. (See the `OB` obsolescence markers in the [POSIX `test` standard](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html)). – Charles Duffy Nov 06 '19 at 16:09
  • The whole `$?` test is unnecessary if we only use the function following a failed command (i.e. `which git || apt install git || check_error "git failed"`). – Toby Speight Nov 06 '19 at 16:47
  • @Toby: that's an acceptable alternative solution, although I think its more intrusive to the code being checked. As example, I typically copy paste commands from vim into terminal, and so having the shell commands appear unadorned in the source file is helpful then. Also danger of the `|| check` accidentally finding its way into an if condition (by unsuspecting user). – Darren Smith Nov 06 '19 at 17:29
  • @Charles, hack or not, I see this approach (using `test`) in plenty of contemporary low level scripts., e.g., just about every configure script I have ever opened. It's pretty simple and robust. – Darren Smith Nov 06 '19 at 17:44
  • 1
    `configure` is built by autoconf, which intentionally is compatible with 1970s shells. That's why configure scripts also test whether your shell treats `^` as a pipe character. No shell made in the last four decades (except for [the Heirloom project's Bourne implementation](http://heirloom.sourceforge.net/), a project which exists specifically to have a version of 1970s-era UNIX tools that build on modern systems) has actually done that, though. – Charles Duffy Nov 06 '19 at 18:06
  • (one exception to the above claim -- SunOS was shipping pre-POSIX Bourne into the 2000s -- it was finally killed for good in Solaris 11 -- but even then, they *also* shipped a POSIX-compliant `sh` as well. And even Sun's non-POSIX `sh`, and the Heirloom one derived from it, don't need the `"x${foo}"`/`"${foo}x"` hack). – Charles Duffy Nov 06 '19 at 18:10
  • ...keep in mind that autoconf goes a long way back -- it already existed before the first release of automake in 1994. That was 25 years ago; needing to be paranoid about systems with 10-year-old shells then was different from being paranoid about systems with 30-year-old shells today. I'd call only one of those two paranoias justifiable. – Charles Duffy Nov 06 '19 at 18:16