Using an `and` condition in a `select` behaves (sometimes) like an `or`

Viewed 48

I need to find those ethernet interfaces, which are up.

I tried this:

$ ip -d -j a s | jq 'map(select ((.link_type == "ether") and (.flags[] | contains 
("UP")))) | map(.ifname)'

But it returns two results,

[
  "enp0s3",
  "enp0s3"
]

although ip lists this interface only once.

$ ip -d -j a s | jq 'map(.ifname)'
[
  "lo",
  "enp0s3",
  "enp0s8"
]

It seems to me that the and in the select behaves like an or. It returns those interfaces, which have .link_type = "ether" or .flags[] | contains ("UP").

But when I try the same with a more basic example,

$ jq 'map(select((. > 1) and (. < 3)))' <<<'[1, 2, 3]'

it works as expected:

[
  2
]

What did I wrong?

2 Answers

This is one of common pitfalls associated with using select(..). You are actually expanding the data twice, once w.r.t. to the root level and once again with .flags[] leading to combinatorial explosion

The solution would be to move .flags[] outside select as below

map(.flags[] as $x | select ((.link_type == "ether") and ( $x | contains ("UP"))))

Also it seems the flags can take the values UP and LOWER_UP, if you were looking for an exact match, use an equality operator == or a strict regex match, e.g. with test(..)

A simple lookup for just UP in case if you are not running into the above explosion would be to just do

map(select( (.link_type == "ether") and (.flags[] == "UP") ))

It turns out, that contains does a sub-string search.

Changing .flags[] | contains ("UP") to .flags[] | . == "UP" fixes the problem:

$ ip -d -j a s | jq 'map (select (.link_type == "ether")) | map (select 
(.flags[] | . == "UP")) | map(.ifname)'
[
  "enp0s3"
]
Related