11

I'd like to pretty print the ip addresses and show the output in in tabular format, including all the meta data such as valid_lft, temporary, etc.

I figured out that ip -j addr show eth0 is giving me the JSON I need. If I run ip -j addr show eth0 | jq -r '.[0].addr_info' the data is already filtered for what I'm interested in:

[
  {
    "family": "inet",
    "local": "192.168.x.y",
    "prefixlen": 24,
    "broadcast": "192.168.1.255",
    "scope": "global",
    "dynamic": true,
    "noprefixroute": true,
    "label": "eth0",
    "valid_life_time": 2339,
    "preferred_life_time": 2339
  },
  {
    "family": "inet6",
    "local": "2003:mm:nn:pp:ww:xx:yy:zz",
    "prefixlen": 64,
    "scope": "global",
    "temporary": true,
    "dynamic": true,
    "valid_life_time": 14342,
    "preferred_life_time": 1742
  },
  {
    "family": "inet6",
    "local": "2003:mm:nn:pp:qq:rr:ss:tt",
    "prefixlen": 64,
    "scope": "global",
    "dynamic": true,
    "mngtmpaddr": true,
    "noprefixroute": true,
    "valid_life_time": 14342,
    "preferred_life_time": 1742
  },
  {
    "family": "inet6",
    "local": "fd4e:gg:hh:ii:jj:kk:ll:mm",
    "prefixlen": 64,
    "scope": "global",
    "temporary": true,
    "dynamic": true,
    "valid_life_time": 14342,
    "preferred_life_time": 1742
  },
  {
    "family": "inet6",
    "local": "fd4e:gg:hh:ii:qq:rr:ss:tt",
    "prefixlen": 64,
    "scope": "global",
    "dynamic": true,
    "mngtmpaddr": true,
    "noprefixroute": true,
    "valid_life_time": 14342,
    "preferred_life_time": 1742
  },
  {
    "family": "inet6",
    "local": "fe80::qq:rr:ss:tt",
    "prefixlen": 64,
    "scope": "link",
    "noprefixroute": true,
    "valid_life_time": 4294967295,
    "preferred_life_time": 4294967295
  }
]

I know that I can use @tsv to get a table format and to accommodate for different value lengths I can pipe to column -ts $'\t'.

What I cannot figure out is how I can iterate through all the objects and extract the keys first, because if I don't do that, the output values will be in incorrect columns, based on what keys each of the object has (or rather not has). I already succeeded in extracted the keys using

$ ip -j addr show eth0 | jq -r '[.[0].addr_info | .[] | keys_unsorted[]] | reduce .[] as $a ([]; if IN(.[]; $a) then . else . += [$a] end)'
[
  "family",
  "local",
  "prefixlen",
  "broadcast",
  "scope",
  "dynamic",
  "noprefixroute",
  "label",
  "valid_life_time",
  "preferred_life_time",
  "temporary",
  "mngtmpaddr"
]

Now I do not know how to combine all of this.

Essentially, the following is yielding the desired result, but I do not like it, because the headers/keys are manually defined:

$ ip -j addr show eth0 | jq -r '(["Family", "Local", "Prefixlen", "Broadcast", "Scope", "Dynamic", "Noprefixroute", "Label", "Valid_Life_Time", "Preferred_Life_Time", "Temporary", "Deprecated", "Mngtmpaddr"] | (., map(length*"-"))), (.[0].addr_info | .[] | [ .family, .local, .prefixlen, .broadcast, .scope, .dynamic, .noprefixroute, .label, .valid_life_time, .preferred_life_time, .temporary, .deprecated, .mngtmpaddr ] | map(.//"-")) | @tsv' | column -ts $'\t'
Family  Local                                  Prefixlen  Broadcast      Scope   Dynamic  Noprefixroute  Label  Valid_Life_Time  Preferred_Life_Time  Temporary  Deprecated  Mngtmpaddr
------  -----                                  ---------  ---------      -----   -------  -------------  -----  ---------------  -------------------  ---------  ----------  ----------
inet    192.168.x.y                            24         192.168.1.255  global  true     true           eth0   1917             1917                 -          -           -
inet6   2003:mm:nn:pp:ww:xx:yy:zz              64         -              global  true     -              -      14348            1748                 true       -           -
inet6   2003:mm:nn:pp:qq:rr:ss:tt              64         -              global  true     true           -      14348            1748                 -          -           true
inet6   fd4e:gg:hh:ii:jj:kk:ll:mm              64         -              global  true     -              -      14348            1748                 true       -           -
inet6   fd4e:gg:hh:ii:qq:rr:ss:tt              64         -              global  true     true           -      14348            1748                 -          -           true
inet6   fe80::qq:rr:ss:tt                      64         -              link    -        true           -      4294967295       4294967295           -          -           -

Any help is much appreciated.

7
  • 1
    Maybe you can use ip -j addr show eth0 | jq -c '.[].addr_info' | mlr --j2p unsparsify (or --j2t for JSON to TSV instead of to pretty) Commented Sep 10, 2024 at 15:31
  • See also ip -j addr show | jq -c '.[].addr_info' | vd -f json (vd can be found in the visidata package on Debian-based systems at least) Commented Sep 10, 2024 at 15:33
  • Are you on Linux? Can we assume GNU tools? Commented Sep 10, 2024 at 15:38
  • @terdon Yes, the systems in question are all Debian based (i.e. Raspbian, Ubuntu, Debian) Commented Sep 10, 2024 at 15:53
  • 1
    Extracting the keys could be simplified to ip -j a | jq -r '[.[].addr_info[]]|add|keys_unsorted' Commented Sep 10, 2024 at 16:18

3 Answers 3

11

Combining jq (for extracting the addr_info array from the first top-level entry) and mlr (for formatting):

$ ip -j addr show enp1s0 | jq 'first.addr_info' | mlr --j2p --barred unsparsify
+--------+-------------------------+-----------+---------------+--------+---------+--------+-----------------+---------------------+
| family | local                   | prefixlen | broadcast     | scope  | dynamic | label  | valid_life_time | preferred_life_time |
+--------+-------------------------+-----------+---------------+--------+---------+--------+-----------------+---------------------+
| inet   | 192.168.1.191           | 24        | 192.168.1.255 | global | true    | enp1s0 | 68465           | 68465               |
| inet6  | fe80::2a0:aff:fe08:9aa6 | 64        |               | link   |         |        | 4294967295      | 4294967295          |
+--------+-------------------------+-----------+---------------+--------+---------+--------+-----------------+---------------------+

If you want plain CSV output, then change --j2p (JSON-to-pretty-printed) to --j2c (JSON-to-CSV) and drop the --barred option (only available for pretty-printed output).

I'm sure you can extract the addr_info array directly with mlr, but this was short and easy.

The unsparsify verb of mlr ensures that all output records have all fields and adds empty values to non-existing fields (like in the broadcast field in the second record above).

Given your data from the question (fed straight into the mlr command above, as the addr_info array has already been extracted), this would produce

+--------+---------------------------+-----------+---------------+--------+---------+---------------+-------+-----------------+---------------------+-----------+------------+
| family | local                     | prefixlen | broadcast     | scope  | dynamic | noprefixroute | label | valid_life_time | preferred_life_time | temporary | mngtmpaddr |
+--------+---------------------------+-----------+---------------+--------+---------+---------------+-------+-----------------+---------------------+-----------+------------+
| inet   | 192.168.x.y               | 24        | 192.168.1.255 | global | true    | true          | eth0  | 2339            | 2339                |           |            |
| inet6  | 2003:mm:nn:pp:ww:xx:yy:zz | 64        |               | global | true    |               |       | 14342           | 1742                | true      |            |
| inet6  | 2003:mm:nn:pp:qq:rr:ss:tt | 64        |               | global | true    | true          |       | 14342           | 1742                |           | true       |
| inet6  | fd4e:gg:hh:ii:jj:kk:ll:mm | 64        |               | global | true    |               |       | 14342           | 1742                | true      |            |
| inet6  | fd4e:gg:hh:ii:qq:rr:ss:tt | 64        |               | global | true    | true          |       | 14342           | 1742                |           | true       |
| inet6  | fe80::qq:rr:ss:tt         | 64        |               | link   |         | true          |       | 4294967295      | 4294967295          |           |            |
+--------+---------------------------+-----------+---------------+--------+---------+---------------+-------+-----------------+---------------------+-----------+------------+
7
  • Ah, nice! I was hoping someone could come up with something better than that monstrosity I posted. You could just pass the output through perl -pe 's/([a-zA-Z]+)/\u$1/g; to get the capitalization as the OP had it. Commented Sep 10, 2024 at 19:49
  • Thank you, this is elegant. Since I asked to avoid any additional tools, I've accepted @StéphaneChazelas suggestion as answer below. However, I think I will be using this one regardless :-) On a side note, I thought it would be good to add the interface to the output, which can be achieved by using ip -j a | jq 'foreach .[] as $item (0; . +1; $item.addr_info [] as $a | {ifname: $item.ifname} * $a)' | mlr --j2p unsparsify Commented Sep 11, 2024 at 7:29
  • 1
    @hanjo Don't do explicit counting loop when you don't need to: .[] | .ifname as $ifname | .addr_info | map(.ifname=$ifname) | flatten. If you want the ifname field first in the result, then reorder with mlr: mlr --j2p unsparsify then reorder -f ifname Commented Sep 11, 2024 at 7:40
  • @Kusalananda good to know, thanks. Still learning here. On my test machine (Raspberry Pi Model B Rev 2) avoiding the foreach saved 3ms, so I guess the performance advantage is neglectable for this specific use case, but I get the idea. Don't use loops if not absolutely required. Commented Sep 11, 2024 at 7:49
  • @hanjo Well, :-), I hope the millisecond performance gain makes up for the time spent working on the script... Commented Sep 11, 2024 at 7:52
7

While I'd use mlr (or perl/python if you wanted to avoid install additional dependencies such as jq or mlr), if you had to use jq, that could be done with something like:

ip -j address | jq -r '
  [
    .[].addr_info[]
  ] as $rows | 
  (
    $rows |
      add |
      keys_unsorted
  ) as $header |
  $header,
  ($header | map(gsub("."; "-"))),
  (
    $rows[] |
      . as $row |
      $header |
      map($row[.]) |
      map(.//"-")
  ) | @tsv' | column -ts $'\t'

With your input, that gives:

family  local                      prefixlen  broadcast      scope   dynamic  noprefixroute  label  valid_life_time  preferred_life_time  temporary  mngtmpaddr
------  -----                      ---------  ---------      -----   -------  -------------  -----  ---------------  -------------------  ---------  ----------
inet    192.168.x.y                24         192.168.1.255  global  true     true           eth0   2339             2339                 -          -
inet6   2003:mm:nn:pp:ww:xx:yy:zz  64         -              global  true     -              -      14342            1742                 true       -
inet6   2003:mm:nn:pp:qq:rr:ss:tt  64         -              global  true     true           -      14342            1742                 -          true
inet6   fd4e:gg:hh:ii:jj:kk:ll:mm  64         -              global  true     -              -      14342            1742                 true       -
inet6   fd4e:gg:hh:ii:qq:rr:ss:tt  64         -              global  true     true           -      14342            1742                 -          true
inet6   fe80::qq:rr:ss:tt          64         -              link    -        true           -      4294967295       4294967295           -          -

To add the interface name as the first column:

ip -j address | jq -r '
  [
    .[] |
      {"ifname"} as $ifname |
      .addr_info[] | 
      $ifname + .
  ] as $rows | 
  (
    $rows |
      add |
      keys_unsorted
  ) as $header |
  $header,
  ($header | map(gsub("."; "-"))),
  (
    $rows[] |
      . as $row |
      $header |
      map($row[.]) |
      map(.//"-")
  ) | @tsv' | column -ts $'\t'
3
  • This is it, thank you. While I must admit that the solution using mlr is more elegant, I explicitly asked to avoid any additional tools and this is working, so I'm accepting this as an answer. On a side note, I thought it would be good to add the interface to the output, which can be achieved by exchanging .[].addr_info[] in the above with foreach .[] as $item (0; . +1; $item.addr_info [] as $a | {ifname: $item.ifname} * $a) Commented Sep 11, 2024 at 7:22
  • 1
    @hanjo, I've added a command that adds the ifname using an alternative approach. Commented Sep 11, 2024 at 7:58
  • this is even better, I learned above to avoid loops. Perfect 👍 Commented Sep 11, 2024 at 8:16
2

Here's a horrible, clunky, hacky workaround: run ip addr twice, collect the headers from the first pass and then parse the second one:


#!/bin/bash

## Run `ip` once to get the headers
headers=( $(ip -j addr show enp1s0 |
  jq -r \
     '[.[0].addr_info | .[] |
       keys_unsorted[]] | reduce .[] as $a
       ([]; if IN(.[]; $a) then . else . += [$a] end)' |
  perl -lne 's/([a-zA-Z]+)/\u$1/g;
             if(/.*("[^"]+").*/){ push @k,"$1"}
             END{print join(",",@k)}
 ') )

## Run it again to parse it
ip -j addr show enp1s0 | jq -r '(['"${headers[@]}"']| (., map(length*"-"))), (.[0].addr_info | .[] | [ .family, .local, .prefixlen, .broadcast, .scope, .dynamic, .noprefixroute, .label, .valid_life_time, .preferred_life_time, .temporary, .deprecated, .mngtmpaddr ] | map(.//"-")) | @tsv' 

The end result, run on my system, is:

$ foo.sh
Family  Local   Prefixlen   Broadcast   Scope   Dynamic Noprefixroute   Label   Valid_Life_Time Preferred_Life_Time
------  -----   ---------   ---------   -----   ------- -------------   -----   --------------- -------------------
inet    192.168.178.57  24  192.168.178.255 global  true    true    wlp9s0  837776  837776  -   -   -
inet6   2a03:8012:be3:0:929a:7dd0:d7df:53b1 128 -   global  true    true    -   6715    3115    -   -   -
inet6   1a02:8012:de3:0:15a2:2c58:40e5:84ae 64  -   global  true    true    -   7196    3596    -   -   -
inet6   ge80::929b:6ad0:c7df:53b1   64  -   link    -   true    -   4294967295  4294967295  -   -   -
5
  • 1
    It is suggested to run ip addr twice ( it might take too long , it might not be idempotent ) when we can just capture the output to a variable and then reuse that output. Commented Sep 11, 2024 at 8:49
  • Made a typo "It is suggested" == "It is not suggested" Commented Sep 11, 2024 at 19:27
  • Eh, @Prem, there are many things wrong with this answer. It's hacky and inelegant. Sure, I could use a variable or a tmp file, but given how quickly the command runs, it didn't seem worth it. As for idempotency, the first run only gives us the headers, and those don't change as they don't depend on the actual values being returned as far as I know. Commented Sep 12, 2024 at 9:35
  • (1) I was talking about the general cases , where we can (must) avoid running the same command twice [ Eg1 when updating some DB table + Eg2 Downloading some large content ] just for formatting purposes. While that change is almost trivial , it will make the Solution a little bit better , not worse. Hence I mentioned it. (2) Yes , in addition to that , there are quite a few things wrong here [ Eg3 using jq & Perl when Perl itself will do + Eg4 making sure the columns are visibly aligned ] though those changes are not really trivial , hence I did not mention those earlier. Commented Sep 12, 2024 at 10:44
  • 1
    @Prem I know. That's why I started this answer with "Here's a horrible, clunky, hacky workaround". Commented Sep 12, 2024 at 11:06

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.