Home > database >  How to select an element in an array based on two conditions in JMESPath?
How to select an element in an array based on two conditions in JMESPath?

Time:12-15

I'm trying to select the SerialNumber of a specific AWS MFADevice for different profiles.

This command returns the list of MFADevices for a certain profile:

aws iam list-mfa-devices --profile xxx

and this is a sample JSON output:

{
    "MFADevices": [
        {
            "UserName": "[email protected]",
            "SerialNumber": "arn:aws:iam::000000000000:mfa/foo",
            "EnableDate": "2022-12-06T16:23:41 00:00"
        },
        {
            "UserName": "[email protected]",
            "SerialNumber": "arn:aws:iam::111111111111:mfa/bar_cli",
            "EnableDate": "2022-12-12T09:13:10 00:00"
        }
    ]
}

I would like to select the SerialNumber of the device containing the string cli. But in case there is only one device in the list (regardless of the presence or absence of the string cli), I'd like to get its SerialNumber.

I have this expression which already filters for the first condition, namely the desired string:

aws iam list-mfa-devices --profile xxx --query 'MFADevices[].SerialNumber | [?contains(@,`cli`)] | [0]'

However I still haven't been able to figure out how to add the if number_of_devices == 1 then return the serial of that single device.

I can get the number of MFADevices with this command:

aws iam list-mfa-devices --profile yyy --query 'length(MFADevices)'

And as a first step towards my final solution I wanted to initially get the SerialNumber only in the case the list has exactly one element, so, I thought of something like this:

aws iam list-mfa-devices --profile yyy --query 'MFADevices[].SerialNumber | [?length(MFADevices) ==`1`]'

but actually already at this stage I get the error below (left alone the fact that I still need to combine it with the cli part):

In function length(), invalid type for value: None, expected one of: ['string', 'array', 'object'], received: "null"

Does anybody know how to achieve what I want?

I know that I could just pipe the raw output to jq and do the filtering there, but I was wondering if there is a way to do it directly in the command using some JMESPath expression.

CodePudding user response:

With stedolan/jq you can filter for the substring and unconditonally add the first, then take the first of them:

.MFADevices | map(.SerialNumber) | first((.[] | select(contains("cli"))), first)

Demo

or

[.MFADevices[].SerialNumber] | map(select(contains("cli")))   .[:1] | first

Demo

Output:

arn:aws:iam::111111111111:mfa/bar_cli

CodePudding user response:

In order to do those kind of condition in JMESPath you will have to rely on logical or (||) and logical and (&&), because the language does not have a conditional keyword, per se.

So, in pseudo-code, instead of doing:

if length(MFADevices) == 1
  MFADevices[0]
else
  MFADevices[?someFilter]

You have to do, like in :

length(MFADevices) == 1 and MFADevices[0] or MFADevices[?someFilter]

So, in JMESPath:

length(MFADevices) == `1` 
  && MFADevices[0].SerialNumber 
  || (MFADevices[?contains(SerialNumber, `cli`)] | [0]).SerialNumber

Note: this assumes that, if there are more than one element but none contains cli, we should get null.


If you want the first element, even when there are multiple devices and the SerialNumber does not contains cli, then you can simplify it further and simply do a logical or, when the contains filter return nothing (as a null result will evaluates to false):

(MFADevices[?contains(SerialNumber, `cli`)] | [0]).SerialNumber 
  || MFADevices[0].SerialNumber
  • Related