Search code examples
c++bashshellcsh

Cannot pass space separated argument from shell script to C++ executable


I am invoking an executable from inside a shell script. The scenario which I am coding is different, but I have written a dummy executable and a script to show what I am facing

Script :

#!/bin/sh -h

MYARGS=""

for arg in "$@"; do
  shift
  case $arg in
    *)
    pattern=" "
      if [[ $arg =~ $pattern ]]; then
        MYARGS="$MYARGS \"$arg\""
      else
        MYARGS="$MYARGS $arg"
      fi
      ;;
  esac
done
echo $MYARGS
./a.out $MYARGS

Program:

#include "iostream"

    int main (int argc, char *argv[])
    {
        for (int i = 0; i < argc; i++)
        {
            std::cout << argv[i] << std::endl;
        }
    }

Now, I don't want to use $@ as I have to filter some arguments to be passed to the main program. But, when I try the above piece with space separated string as argument I get following output:

Command : ./a.out "a b"
Output : 
./a.out
a b

Command: a.sh "a b"
Output :
"a b"
./a.out
"a
b"

The problem is that in second case arguments are automatically split and 'a' and 'b' come as different arguments even though I am preserving the quotes ""

What can be the solution to this?


Solution

  • Once a quote gets into a bash variable, it is just a character. Don't confuse what you type with what the variable contains. In this regard, bash is no different from C.

    const char* v = "a b";
    printf("%s\n", v);    // => a b
    const char* w = "\"a b\"";
    printf("%s\n", w);    // => "a b"
    

    Bash is just the same:

    $ v="a b"
    $ echo "$v"
    a b
    $ w="\"a b\""
    $ echo "$w"
    "a b"
    

    In the above, I put quotes around my variable expansions, which you should always do. (Well, almost always. But do it unless you have a really good reason, and can explain your reason.)

    The reason I quote the expansion is to prevent the contents of the string from being word split after the expansion. Word-splitting splits the string into separate words at whitespace (unless $IFS has been set to something different). That's it. It doesn't look through the string for special characters, because there aren't any. Because once a quote is in a string, it is just another character. (And the same goes for all other special characters, like $ and \.

    You can (and people may recommend to you) use complicated constructs using eval or eval-like operations (like bash -c). Those will almost always end in grief. The simplest, best, and recommended solution is to use an array:

    myargs=()
    
    for arg in "$@"; do
      shift
      case "$arg" in
        *) myargs+=("$arg") ;;
      esac
    done
    echo "${myargs[@]}"
    ./a.out "${myargs[@]}"
    

    Note that every quote in the above snippet is essential (except for the ones in case "$arg") and they all have the same purpose: to avoid word-splitting when the variable (or the array elements) is expanded. (The exception for the case word is because the shell does not word-split expansions in that context. But you don't need to remember that; just use the quotes as shown.)