Skip to main content

Why So Hard?: Multi-line bash argument.


In many situations, when writing statements or commands, I want to orient the command so that it presents well vertically. While we write statements and commands for readability we nearly always have to consider the horizontal constraints before the vertical constraints. The only time I hit a vertical constraint is really with run on functions or inline documentation.

Recently I was capturing a number of curl commands that I was using for testing some rest endpoints. While I know I could probably just STDIN/file-load the data, I really wanted a way to express the whole command in documentation such that it could be copy/pasted into a bash command and just work. No file downloads.

This seemingly simple task had me stumped for about an hour before I came to the following solution:

DATATYPE="Content-Type: application/json"
for ELEMENT in \
'{"suite":"token", "case": "refreshToken", "params": ['\
"'"; do
eval "$(echo curl -H \"$DATATYPE\" http://localhost:7071/api/functest -d $DATA)"

The Problem

Usually when breaking up a bash command you simply add a continuation slash \ as the last character in a line. This was not sufficient with the above command because the JSON string was a single argument. My REST calls are typically small in nature so this wasn't really an issue until I shoved a token into a request.

Combined with the length of the command, I also had an issue where I wanted to express the JSON data with double quotes (without escaping every quote!) Although you can use single quotes to solve the immediate issue, the single quotes are immediately evaluated and any dereference of a variable will contain the interpretable spaces and special characters.

The Solution

From here I was thinking, "I'll just split up the token and concatenate each of the pieces into an environment variable. In python and C these are automatically concatenated, but in bash we must manually concat the string literals via a loop or echo. Turns out that since I was attempting to break up a contiguous set of characters that couldn't have spaces added I needed to real string concatenation. (Note: The spaces come from the indent I added to the snippet for readability.) Therefore I used a for loop that joined each of the string pieces.

To maintain single quotes around our concatenated string, we prefix and suffix the string split with a set of "'"s. Finally, we simply echo this string into the DATA variable.

Naturally I'm thinking, great! Lets run it!

$ curl -H "Content-Type: application/json" http://localhost:7071/api/functest -d $DATA
Invalid request.curl: (6) Could not resolve host: "case"
curl: (6) Could not resolve host: "refreshToken",
curl: (6) Could not resolve host: "params"
curl: (3) bad range specification in URL position 2:

Bah! It's still striping the single quotes from the string that is dereferenced from $DATA. Turns out the key to all of this is to use the eval keyword. I can verify that the command looks the way I want with an echo:

echo curl -H \"Content-Type: application/json\" http://localhost:7071/api/functest -d $DATA

Then all I need to do is feed the output of that into an eval so it runs the command as-is:

eval $(echo curl -H \"Content-Type: application/json\" http://localhost:7071/api/functest -d $DATA)

And then boom, it works.


While in most situations a STDIN or file load would likely be more appropriate, you can use this method for all-in-one documentation or presentation snippets.

The big takeaway here is that if you find yourself in a situation where you want to do something like '$VAR', try eval as a way to get something that works. More generically:

LITERAL_VAR="literal value"
eval $(echo "'"${LITERAL_VAR}"'")

Caution: This technique is not impervious to command injection so don't consider it safe for end user input. (i.e. only use it for canned commands).