Search code examples
windowsbashcmdcygwinquoting

Bash calling windows program, how does quoting work?


I'm doing some scripting for my virtual machines and I'm using cygwin. I need to set computer name and ip address. This part is easy, in that order:

wmic computersystem where caption=name "vm-01"
netsh interface ip set address "Local Area Connection 2" static 10.155.155.50 255.255.255.0

This works just fine in cmd.exe. Now I want to execute this from bash. I want to see command I'm executing, so I'm using this bash function to do it:

call() {
    echo "$@"
    $@
}

I tried it in intuitive way of escaping quotes:

$ call wmic computersystem where caption=name \"wm-01\"
Executing (\\BOH\ROOT\CIMV2:Win32_ComputerSystem.Name="XXX")->rename()
Method execution successful.
Out Parameters:
instance of __PARAMETERS
{
        ReturnValue = 87;
};

Which doesnt work (don't be fouled by "Method execution successful" as I was at first), error code 87.

$ call netsh interface ip set address \"Local Area Connection 2\" static 10.155.155.50 255.255.255.0

On the other hand, this works great.

I managed to solve it for the wmic command by using

$ call wmic computersystem where caption=name \'wm-01\'
Executing (\\BOH\ROOT\CIMV2:Win32_ComputerSystem.Name="XXX")->rename()
Method execution successful.
Out Parameters:
instance of __PARAMETERS
{
        ReturnValue = 0;
};

which does work. I tried the same with netsh

$ call netsh interface ip set address \'Local Area Connection 2\' static 10.155.155.101 255.255.255.0
The filename, directory name, or volume label syntax is incorrect.

which doesn't. What I'm trying to understand is why one command needs \' and second one \"?


Solution

  • There are several things it is important to recognize here:

    • the significance of the quoting is to the shell, not (generally) to the command being launched. The shell removes the quotes that are significant for that purpose before identifying the command name and handing off its arguments, but after all other command-line processing.

    • Only unquoted quotes present in the original text of a command are significant to the shell for quoting purposes. In particular, quote characters arising from parameter expansion are not special -- they just represent themselves.

    • Although the shell splits command lines into tokens before performing any expansions, it later performs word splitting on the expanded command. Expansions that occur within recognized quotes are protected from word splitting, but, again, quotes that arise from expansion are not recognized as special; they do not protect against word splitting.

    Consider, then, this version of your command:

    call netsh interface ip set address "Local Area Connection 2" static 10.155.155.50 255.255.255.0
    

    bash reads the line, splits it into words, trivially performs expansions, performs word splitting, and then performs quote removal, resulting in these words:

    • call
    • netsh
    • interface
    • ip
    • set
    • address
    • Local Area Connection 2
    • static
    • 10.155.155.50
    • 255.255.255.0

    Note that "Local Area Connection 2" (without the quotes) is one 'word'. The first word, 'call', is the command, and the remaining words are the arguments, which are represented by $@ (and also by $* and the individual positional parameters $1, etc.) inside function call().

    Now consider what happens when call() gets around to executing the the command: it goes much the same as before up until word splitting, but there, the connection name is split into multiple arguments, and presented that way to the netsh command.

    I'm sure you already appreciated that much, but now consider this variation on your command:

    call netsh interface ip set address \"Local Area Connection 2\" static 10.155.155.50 255.255.255.0
    

    In that case, the " characters are escaped, and therefore do not serve the purpose of making the connection name a single shell word. It is therefore split even before reaching call(). You therefore get the same result as before, except that two of the arguments to netsh have quote characters in them.

    So what's the solution? You've almost got it already. The difference between the special parameters $@ and $* is their interaction with word splitting. When $@ is expanded inside quotes, it is a special exception to the rule that word splitting is not performed on the result -- the result is split between elements of $@, but not within them. The primary purpose is for exactly the kind of thing you're trying to do: to pass on the shell's positional parameters to another command.

    In other words, use this version of your shell function:

    call() {
      echo $@
      "$@"
    }
    

    and call it with your original command line, with unquoted quotes.