Bash – How to add single quote inside of a bash variable

bashcurl

I'm trying to make a call to an API and want to set some curl defaults into a single variable so when I make multiple calls, it uses the same set of 'defaults'.

For some reason, curl is not recognizing the -H 'Content-type: application/json' and I'm baffled as to why not.

opts=" -v -H 'Content-type: application/json' "
curl $opts -d '{"hi":1}' https://google.com

The above prints out

$ opts=" -v -H 'Content-type: application/json' "
$ curl $opts -d '{"hi":1}' https://google.com
* Could not resolve host: application                <-------- !!
* Closing connection 0
curl: (6) Could not resolve host: application
* Rebuilt URL to: https://google.com/
*   Trying 172.217.4.174...
* TCP_NODELAY set
* Connected to google.com (172.217.4.174) port 443 (#1)
* TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.google.com
* Server certificate: Google Internet Authority G2
* Server certificate: GeoTrust Global CA
> POST / HTTP/1.1
> Host: google.com
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 8
> Content-Type: application/x-www-form-urlencoded    <-------- !!
>
* upload completely sent off: 8 out of 8 bytes

I marked 2 lines where there are issues. Curl is thinking that application/json is a host/path and it's not overriding the Content-Type.

However, this is fine:

$ curl -v -H 'Content-type: application/json' -d '{"hi":1}' https://google.com
* Rebuilt URL to: https://google.com/
*   Trying 172.217.12.46...
* TCP_NODELAY set
*   Trying 2607:f8b0:4000:813::200e...
* TCP_NODELAY set
* Immediate connect fail for 2607:f8b0:4000:813::200e: No route to host
* Connected to google.com (172.217.12.46) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.google.com
* Server certificate: Google Internet Authority G2
* Server certificate: GeoTrust Global CA
> POST / HTTP/1.1
> Host: google.com
> User-Agent: curl/7.54.0
> Accept: */*
> Content-type: application/json                     <---------- yay
> Content-Length: 8
>
* upload completely sent off: 8 out of 8 bytes

So how can I get curl to recognize the -H parameter the way I want? And why is it behaving like this?

(yes, I know I'm using Google as my API endpoint and getting 4xx codes.. its just a webserver I can test against right now to make my point)

Best Answer

Short answer: see BashFAQ #50: "I'm trying to put a command in a variable, but the complex cases always fail!".

Long answer: Putting commands (or parts of commands) into variables and then getting them back out intact is complicated. The basic problem you're running into is that the shell parses quotes before expanding variable references, so putting quotes in a variable doesn't do anything useful -- by the time they're part of the command line, it's too late for them to have their intended effect.

There are a couple of ways to do this; which is best will depend on exactly what you're trying to do.

Option 1 is to use an array instead of a plain text variable:

opts=(-v -H 'Content-type: application/json')
curl "${opts[@]}" -d '{"hi":1}' https://google.com

The syntax is a bit messy (all of the quotes, parentheses, brackets etc I used above are actually necessary for it to work right), and arrays are a bash feature (not available in all shells, so be sure use a shebang like #!/bin/bash or #!/usr/bin/env bash). On the other hand, you can build the option array dynamically if you need to:

opts=(-v -H 'Content-type: application/json')
if [[ -n "$fakeuseragent" ]]; then
    opts+=(-A "$fakeuseragent")
fi
curl "${opts[@]}" -d '{"hi":1}' https://google.com

Option 2 is to use a function to wrap curl, and add your custom options:

mycurl() {
    curl -v -H 'Content-type: application/json' "$@"
}
mycurl -d '{"hi":1}' https://google.com