How to capture output of curl piped with grep into a variable in a shell script - bash

I realise there are quite a few posts that show how to capture output of curl or grep, but haven't been able to find any post where I can capture the value of a group in the grep's regular expression.
I need to capture output of the following command in a variable inside a shell script.
I am working on API calls of Qlik tool. The way the APIs work, is that you first have to call the login operation, which returns a session key and then use this session key in the subsequent API calls, like this
https://help.qlik.com/en-US/enterprise-manager/November2021/Content/EnterpriseManager/EnterpriseManager_APIGuide/CurlAPI/api_login.htm
cURL example
Active Directory request
curl -i -k --header "Authorization: Basic cWFAcWE6cWE=" https://computer.network.net/attunityenterprisemanager/api/v1/login
Response
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html
Server: Microsoft-HTTPAPI/2.0
EnterpriseManager.APISessionID: J3cKzWIbi_w6Fr1G-tO03Q
Date: Mon, 26 Dec 2016 17:02:01 GMT
And then use the EnterpriseManager.APISessionID
https://help.qlik.com/en-US/enterprise-manager/November2021/Content/EnterpriseManager/EnterpriseManager_APIGuide/CurlAPI/api_aem_getserver.htm
So, I am trying to get this EnterpriseManager.APISessionID value in a variable, so I can use it in the script to call other operations.
If I run the below command on the command line it prints the value on the terminal,
curl -i -k --header "Authorization: Basic mybase64idpwvalue" https://qlik-qem-dev.abc.com/attunityenterprisemanager/api/v1/login | grep '(EnterpriseManager.APISessionID: )(.*)' | echo "${BASH_REMATCH[2]}"
This is the output on command line
eu_3qrEWJFAz_4hL7bOIvA
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
but if I put it in a .sh file and execute it, then the variable value is empty
API_SESSION_ID=$(curl -i -k --header "Authorization: Basic mybase64idpwvalue" https://qlik-qem-dev.abc.com/attunityenterprisemanager/api/v1/login | grep 'EnterpriseManager.APISessionID: ')
API_SESSION_ID2=$(curl -i -k --header "Authorization: Basic mybase64idpwvalue" https://qlik-qem-dev.abc.com/attunityenterprisemanager/api/v1/login | grep '(EnterpriseManager.APISessionID: )(.*)')
#API_SESSION_ID=$(curl -i -k --header "Authorization: Basic mybase64idpwvalue" https://qlik-qem-dev.abc.com/attunityenterprisemanager/api/v1/login) | grep '(EnterpriseManager.APISessionID: )(.*)' | echo "${BASH_REMATCH[2]}"
echo "API_SESSION_ID : ${API_SESSION_ID}"
echo "API_SESSION_ID2 : ${API_SESSION_ID2}"
I tried some other combinations of the command in the script, but am unable to capture the value in a variable. It is always empty
How can I get that populated?
Thanks

First, I don't know where eu_3qrEWJFAz_4hL7bOIvA comes from in your example. I'll assume you meant J3cKzWIbi_w6Fr1G-tO03Q.
Second, your command line output doesn't make sense, unless you played around with the shell and some global state remains. When I run it, I get a empty output, which is what I expect.
Third, BASH_REMATCH is a bash variable that captures the result of regex matching from bash operators ([[ string =~ regex ]]). It's not aware of grep, perl, sed, or any other programs using regex.
That's why when I run your command I get no output - BASH_REMATCH is empty.
Last, your grep expression uses unquoted parentheses, and you didn't pass -E, so it looks for parentheses in the text, and it wouldn't find them. So even if BATCH_REMATCH was aware of grep, grep would still return an empty result.
All that said, sed does support capture:
API_SESSION_ID="$( \
curl -i -k \
--header "Authorization: Basic mybase64idpwvalue" \
https://qlik-qem-dev.abc.com/attunityenterprisemanager/api/v1/login \
| sed -n 's/\(EnterpriseManager.APISessionID: \)\(.*\)/\2/p' \
)"
echo "API_SESSION_ID : ${API_SESSION_ID}"
That \2 represents the second capture group. But you don't actually need the first:
API_SESSION_ID="$( \
curl -i -k \
--header "Authorization: Basic mybase64idpwvalue" \
https://qlik-qem-dev.abc.com/attunityenterprisemanager/api/v1/login \
| sed -n 's/EnterpriseManager.APISessionID: \(.*\)/\1/p' \
)"
echo "API_SESSION_ID : ${API_SESSION_ID}"

BASH_REMATCH isn't related to grep. It must be called after regex matching.
GNU grep (perl positive look behind):
echo "$example_header" |
grep -Po '(?<=^EnterpriseManager\.APISessionID: ).*'
Pure bash:
[[ $example_header =~ (^|$'\n')(EnterpriseManager\.APISessionID: )([^$'\n']*) ]]
echo "${BASH_REMATCH[3]}"
Both print:
J3cKzWIbi_w6Fr1G-tO03Q
For input:
example_header=\
'Response
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html
Server: Microsoft-HTTPAPI/2.0
EnterpriseManager.APISessionID: J3cKzWIbi_w6Fr1G-tO03Q
Date: Mon, 26 Dec 2016 17:02:01 GMT'
# Or: example_header=$(curl -i -k --header "Authorization: Basic cWFAcWE6cWE=" https://computer.network.net/attunityenterprisemanager/api/v1/login)
Don't forget to escape . and to match the start of the line (to avoid matching NotEnterpriseManager...).

Related

Escaping multiple layers of mixed quotes for a curl command executed inside a bash script

I have the following bash script that uses its arguments to hit a RESTful web service (via curl) and prints out both the curl request made as well as the response:
#! /bin/bash
# arguments:
# $1 - username
# $2 - password
#
# outputs:
# if the script exits with a non-zero status then something went wrong
# verify that we have all 6 required arguments and fail otherwise
if [ "$#" -ne 2 ]; then
echo "Required arguments not provided"
exit 1
fi
# set the script arguments to meaningful variable names
username=$1
password=$2
# login and fetch a valid auth token
req='curl -k -i -H "Content-Type: application/json" -X POST -d ''{"username":"$username","password":"$password"}'' https://somerepo.example.com/flimflam'
resp=$(curl -k -i -H "Content-Type: application/json" -X POST -d ''{"username":"$username","password":"$password"}'' https://somerepo.example.com/flimflam)
# echo the request for troubleshooting
echo "req = $req"
if [ -z "$resp" ]; then
echo "Login failed; unable to parse response"
exit 1
fi
echo "resp = $resp"
When I run this I get:
$ sh myscript.sh myUser 12345#45678
curl: (3) Port number ended with '"'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (6) Could not resolve host: 12345#45678"
100 1107 100 1093 100 14 2849 36 --:--:-- --:--:-- --:--:-- 2849
req = curl -k -i -H "Content-Type: application/json" -X POST -d {"username":"$username","password":"$password"} https://somerepo.example.com/flimflam
resp = HTTP/1.1 400 Bad Request...(rest omitted for brevity)
Obviously, I'm not escaping the various layers of single- and double-quotes inside the curl statement correctly, as is indicated by outputs like:
curl: (6) Could not resolve host: 12345#45678"
and:
req = curl -k -i -H "Content-Type: application/json" -X POST -d {"username":"$username","password":"$password"} https://somerepo.example.com/flimflam
where the username/password variables are not parsing.
In reality my script takes a lot more than 2 arguments, which is why I'm changing them to have meaningful variable names (such as $username instead of $1) so its more understandable and readable.
Can anyone spot where I'm going awry? Thanks in advance!
Update
I tried the suggestion which turns the req into:
curl -k -i -H 'Content-Type: application/json' -X POST -d "{'username':'myUser','password':'12345#45678'}" https://somerepo.example.com/flimflam
However this is still an illegal curl command and instead needs to be:
curl -k -i -H 'Content-Type: application/json' -X POST -d '{"username":"myUser","password":"12345#45678"}' https://somerepo.example.com/flimflam
First, as I said in a comment, storing commands in variables just doesn't work right. Variables are for data, not executable code. Second, you have two levels of quoting here: quotes that're part of the shell syntax (which are parsed, applied, and removed by the shell before the arguments are passed to `curl), and quotes that're part of the JSON syntax.
But the second problem is actually worse than that, because simply embedding an arbitrary string into some JSON may result in JSON syntax errors if the string contains characters that're part of JSON syntax. Which passwords are likely to do. To get the password (and username for that matter) embedded correctly in your JSON, use a tool that understands JSON syntax, like jq:
userinfo=$(jq -n -c --arg u "$username" --arg p "$password" '{"username":$u,"password":$p}')
Explanation: this uses --arg to set the jq variables u and p to the shell variables $username and $password respectively (and the double-quotes around the shell variables will keep the shell from doing anything silly to the values), and creates a JSON snippet with them embedded. jq will automatically add appropriate quoting/escaping/whatever is needed.
Then, to use it with curl, use something like this:
resp=$(curl -k -i -H "Content-Type: application/json" -X POST -d "$userinfo" https://somerepo.example.com/flimflam)
Again, the double-quotes around $userinfo keep the shell from doing anything silly. You should almost always put double-quotes around variables references in the shell.
Note that I never used the req variable to store the command. If you need to print the command (or its equivalent), use something like this:
printf '%q ' curl -k -i -H "Content-Type: application/json" -X POST -d "$userinfo" https://somerepo.example.com/flimflam
echo
The %q format specifier tells the shell to add appropriate quoting/escaping so that you could run the result as a shell command, and it'd work properly. (And the echo is there because printf doesn't automatically add a newline at the end of its output.)
try changing this:
req='curl -k -i -H "Content-Type: application/json" -X POST -d ''{"username":"$username","password":"$password"}'' https://somerepo.example.com/flimflam'
to this
req="curl -k -i -H 'Content-Type: application/json' -X POST -d \"{'username':'$username','password':'$password'}\" https://somerepo.example.com/flimflam"
and similarly for the resp
ah those pesky "curly" thingies...
how 'bout...
req="curl -k -i -H 'Content-Type: application/json' -X POST -d '{\"username\":\"$username\",\"password\":\"$password\"}' https://somerepo.example.com/flimflam"
This needs even more escaping:
With:
resp=$(curl -k -i -H "Content-Type: application/json" -X POST -d "{\"username\":\"$username\",\"password\":\"$password\"}" https://somerepo.example.com/flimflam)
In bash, the variables are still expanded when they're inside single quotes that are inside double quotes.
And you'll need the \" double quotes in the payload as per the JSON definition.
EDIT: I rerun the curl through a HTTP proxy and corrected the script line (see above, removed the single quotes). Results (in raw HTTP) are now:
POST /flimflam HTTP/1.1
Host: somerepo.example.com
User-Agent: curl/7.68.0
Accept: */*
Content-Type: application/json
Content-Length: 44
Connection: close
{"username":"user","password":"12345#abcde"}
(which should be fine)

curl | jq: parse error: Invalid numeric literal at line 2, column 0

I have a simple bash file as below
#!/bin/bash
net=$(curl -s -H "Content-Type: application/json" -H "X-Auth-Token: $token" -d '{"network": {"name": "net1"}}' http://10.1.10.146:18090/network/v2.0/networks 2>&1 | awk '/id/{print $1}' | jq -r .network.id)
echo $net
Running this file gives me an error as below
parse error: Invalid numeric literal at line 2, column 0
tried making the changes according to these links
https://unix.stackexchange.com/questions/354943/setting-jq-output-to-a-bash-variable
Working with Bash and cURL
but nothing helped me, unable to figure out where i am going wrong. let me know the reason for the error and possible changes.
The curl output for the command
curl -s -H "Content-Type: application/json" -H "X-Auth-Token: $token" -d '{"network": {"name": "net1"}}' http://10.1.10.146:18090/network/v2.0/networks
output:
{"network":{"status":"ACTIVE","router:external":false,"availability_zone_hints":[],"availability_zones":[],"description":"","subnets":[],"shared":false,"tenant_id":"d0e75710820c401db3291ac6278f326f","created_at":"2018-05-15T07:37:42Z","tags":[],"ipv6_address_scope":null,"mtu":1450,"updated_at":"2018-05-15T07:37:42Z","admin_state_up":true,"revision_number":2,"ipv4_address_scope":null,"is_default":false,"port_security_enabled":true,"project_id":"d0e75710820c401db3291ac6278f326f","id":"1548df56-a35b-4232-9550-54a3c2266d60","name":"net1"}}
the idea is to get only the id from the output and store into a bash variable, to get the id i used the below command
curl -s -H "Content-Type: application/json" -H "X-Auth-Token: $token" -d '{"network": {"name": "net1"}}' http://10.1.10.146:18090/network/v2.0/networks 2>&1 | awk '/id/{print $1}' | jq -r .network.id
output:
be831582-90c1-499c-875f-9c0b0d1969a6
I have also tried removing the awk and parsing the curl json response, the same error is showing up.
thanks in advance.
The "parse error" message appears because the 2>&1 redirects any STDERR message into jq, which cannot parse it. Compare the output from these commands:
> curl http://no.such.host/network/v2.0/networks 2>&1
curl: (6) Could not resolve host: no.such.host
> curl http://no.such.host/network/v2.0/networks 2>&1 | jq '.'
parse error: Invalid numeric literal at line 1, column 4
Here are some ideas:
Separate all of the piped commands into separate commands.
Try adding set -x near the top of the script to "debug" all of the
commands.
Remove the 2>&1 because it is NOT helping you!
Add error handling to the separate commands.
We could help more if you provided the curl output. (We do not have HTTP access to the 10.1.10.146 host.)

Run multiple curl commands in parallel

I have the following shell script. The issue is that I want to run the transactions parallel/concurrently without waiting for one request to finish to go to the next request. For example if I make 20 requests, I want them to be executed at the same time.
for ((request=1;request<=20;request++))
do
for ((x=1;x<=20;x++))
do
time curl -X POST --header "http://localhost:5000/example"
done
done
Any guide?
You can use xargs with -P option to run any command in parallel:
seq 1 200 | xargs -n1 -P10 curl "http://localhost:5000/example"
This will run curl command 200 times with max 10 jobs in parallel.
Using xargs -P option, you can run any command in parallel:
xargs -I % -P 8 curl -X POST --header "http://localhost:5000/example" \
< <(printf '%s\n' {1..400})
This will run give curl command 400 times with max 8 jobs in parallel.
Update 2020:
Curl can now fetch several websites in parallel:
curl --parallel --parallel-immediate --parallel-max 3 --config websites.txt
websites.txt file:
url = "website1.com"
url = "website2.com"
url = "website3.com"
This is an addition to #saeed's answer.
I faced an issue where it made unnecessary requests to the following hosts
0.0.0.1, 0.0.0.2 .... 0.0.0.N
The reason was the command xargs was passing arguments to the curl command. In order to prevent the passing of arguments, we can specify which character to replace the argument by using the -I flag.
So we will use it as,
... xargs -I '$' command ...
Now, xargs will replace the argument wherever the $ literal is found. And if it is not found the argument is not passed. So using this the final command will be.
seq 1 200 | xargs -I $ -n1 -P10 curl "http://localhost:5000/example"
Note: If you are using $ in your command try to replace it with some other character that is not being used.
Adding to #saeed's answer, I created a generic function that utilises function arguments to fire commands for a total of N times in M jobs at a parallel
function conc(){
cmd=("${#:3}")
seq 1 "$1" | xargs -n1 -P"$2" "${cmd[#]}"
}
$ conc N M cmd
$ conc 10 2 curl --location --request GET 'http://google.com/'
This will fire 10 curl commands at a max parallelism of two each.
Adding this function to the bash_profile.rc makes it easier. Gist
Add “wait” at the end, and background them.
for ((request=1;request<=20;request++))
do
for ((x=1;x<=20;x++))
do
time curl -X POST --header "http://localhost:5000/example" &
done
done
wait
They will all output to the same stdout, but you can redirect the result of the time (and stdout and stderr) to a named file:
time curl -X POST --header "http://localhost:5000/example" > output.${x}.${request}.out 2>1 &
Wanted to share my example how I utilised parallel xargs with curl.
The pros from using xargs that u can specify how many threads will be used to parallelise curl rather than using curl with "&" that will schedule all let's say 10000 curls simultaneously.
Hope it will be helpful to smdy:
#!/bin/sh
url=/any-url
currentDate=$(date +%Y-%m-%d)
payload='{"field1":"value1", "field2":{},"timestamp":"'$currentDate'"}'
threadCount=10
cat $1 | \
xargs -P $threadCount -I {} curl -sw 'url= %{url_effective}, http_status_code = %{http_code},time_total = %{time_total} seconds \n' -H "Content-Type: application/json" -H "Accept: application/json" -X POST $url --max-time 60 -d $payload
.csv file has 1 value per row that will be inserted in json payload
Based on the solution provided by #isopropylcyanide and the comment by #Dario Seidl, I find this to be the best response as it handles both curl and httpie.
# conc N M cmd - fire (N) commands at a max parallelism of (M) each
function conc(){
cmd=("${#:3}")
seq 1 "$1" | xargs -I'$XARGI' -P"$2" "${cmd[#]}"
}
For example:
conc 10 3 curl -L -X POST https://httpbin.org/post -H 'Authorization: Basic dXNlcjpwYXNz' -H 'Content-Type: application/json' -d '{"url":"http://google.com/","foo":"bar"}'
conc 10 3 http --ignore-stdin -F -a user:pass httpbin.org/post url=http://google.com/ foo=bar

How to verify a curl request in bash script?

I have a curl request like this :
curl -s -u $user:$password -X GET -H "Content-Type: application/json" $url
Which returns a json as response. So I will parse the response using jq to get some specific data. Like this :
curl -s -u $user:$password -X GET -H "Content-Type: application/json" $url | jq '<expression>'
Now if the curl request fails then obviously the parsing operation throws ugly error. I want to avoid this. How to store the response first and then later parse it if the request is successful. I don't want to display the json whole response. Also if I add -w "%{http_code}" in my request it appends the status code with the JSON response which messes up the parsing. How to solve this ? I basically want to first check if the curl request is successful or not then get the JSON response and parse it.I also want to get the status code, so that if it fails I can display the status code. But status code is now messing up with json response.
You can combine the --write and --fail options:
# separating the (verbose) curl options into an array for readability
curl_args=(
--write "%{http_code}\n"
--fail
--silent
--user "$user:$password"
--request GET
--header "Content-Type: application/json"
)
if ! output=$(curl "${curl_args[#]}" "$url"); then
echo "Failure: code=$output"
else
# remove the "http_code" line from the end of the output, and parse it
sed '$d' <<<"$output" | jq '...'
fi
Also note: quote your variables!
I found glenn jackman's answer good, but a bit confusingly written, so I rewrote it, and altered it so I can use it as a safer alternative to curl | jq.
#!/bin/bash
# call this with normal curl arguments, especially url argument, e.g.
# safecurl.sh "http://example.com:8080/something/"
# separating the (verbose) curl options into an array for readability
curl_args=(
-H 'Accept:application/json'
-H 'Content-Type:application/json'
--write '\n%{http_code}\n'
--fail
--silent
)
echo "${curl_args[#]}"
# prepend some arguments, but pass on whatever arguments this script was called with
output=$(curl "${curl_args[#]}" "$#")
return_code=$?
if [ 0 -eq $return_code ]; then
# remove the "http_code" line from the end of the output, and parse it
echo "$output" | sed '$d' | jq .
else
# echo to stderr so further piping to jq will process empty output
>&2 echo "Failure: code=$output"
fi
Note: This code does not test for services that ignore the requested content type and respond with HTML. You'd need to test for grep -l '</html>' for that.

Why is this bash/CURL call to REST services giving inconsistent results with parameters?

I have written a smoke-testing script that uses BASH script & Curl to test RESTful web services we're working on. The script reads a file, and interprets each line as a URL suffix and parameters for a Curl REST call.
Unfortunately, the script gives unexpected results when I adapted it to run HTTP POST calls as well as GET calls. It does not give the same results running the command on its own, vs. in script:
The BASH Script:
IFS=$'\n' #Don't split an input URL line at spaces
RESTHOST='hostNameAndPath' #Can't give this out
URL="/activation/v2/activationInfo --header 'Content-Type:Application/xml'"
URL2="/activation/v2/activationInfo"
OUTPUT=`curl -sL -m 30 -w "%{http_code}" -o /dev/null $RESTHOST$URL -d #"./activation_post.txt" -X POST`
echo 'out:' $OUTPUT
OUTPUT2=`curl -sL -m 30 -w "%{http_code}" -o /dev/null $RESTHOST$URL2 --header 'Content-Type:Application/xml' -d #'./activation_post.txt' -X POST`
echo 'out2:' $OUTPUT2
Results Out:
out: 505
out2: 200
So, the first call fails (HTTP return code 505, HTTP Version Not Supported), and the second call succeeds (return code "OK").
Why does the first call fail, and how do I fix it? I've verified they should execute the same command (evaluating in echo). I am sure there is something basic I'm missing, as I am just NOW learning Bash scripting.
I think I have found the problem! It is caused by IFS=$'\n'! Because of this, variable expansion does not work as expected. It does not let to split the arguments specified in the URL string!
As a result the SERVER_PROTOCOL variable on the server side will be set to '--header Content-Type:Application/xml HTTP/1.1' instead of "HTTP/1.1", and the CONTENT_TYPE will be 'application/x-www-form-urlencoded' instead of 'Application/xml'.
To show the root of the problem in detail:
VAR="Solaris East"
printf "+%s+ " $VAR
echo "==="
IFS=$'\n'
printf "+%s+ " $VAR
Output:
+Solaris+ +East+ ===
+Solaris East+
So the $VAR expansion does not work as expected because of IFS=$'\n'!
Solution: Do not use IFS=$'\n' and replace space to %20 in URL!
URL=${URL2// /%20}" --header Content-Type:Application/xml"
In this case your first curl call will work properly!
If You still use IFS=$'\n' and give --header option in the command line it will not work properly if URL contains a space, because the server will fail to process it (I tested on apache)!
Even You still cannot use HEADER="--header Content-Type:Application/xml" as expanding $HEADER will result one(!) argument for curl, namely --header Content-Type:Application/xml instead of splitting them into two.
So I may suggest to replace spaces in URL to %20 anyway!
The single quotes surrounding Content-Type:Application/xml, because they are quoted in the value of URL are treated as literal quotes and not removed when $URL is expanded in that call to curl. As a result, you are passing an invalid HTTP header. Just use
URL="/activation/v2/activationInfo --header Content-Type:Application/xml"
OUTPUT=`curl -sL -m 30 -w "%{http_code}" -o /dev/null $RESTHOST$URL -d #"./activation_post.txt" -X POST`
However, it's not a great idea to rely on word-splitting like this to combine two separate pieces of the call to curl in a single variable. Try something like this instead:
URLPATH="activation/v2/activationInfo"
HEADERS=("--header" "Content-Type:Application/xml")
OUTPUT=$( curl -SL -m 30 -w "%{http_code}" -o /dev/null "$RESTHOST/$URL" "${HEADERS[#]}" -d #'./activation_post.txt' -X POST )

Resources