MacOS Better ip info in terminal

One particular painful point, coming from linux, was the ip command in the Terminal. The Default MacOS ifconfig command is very - well let’s say “old fashioned” :)

Coming from Linux you mostly use ip for everyting ip a to display your interfaces ip r your routes, and so on.

Luckily you can install the iproute2 Package on MacOS which give you the ip command.

Installing iproute2 on MacOS

The easiest way is using homebrew, if you not using it now you probably should:

brew install iproute2mac

You can also install via MacPorts or manually from the GitHub repository, though Homebrew is the most straightforward approach.

After installation you should be able to use the ip command simple by typing ip addr as a test.

As a final Note; The iproute2mac package is a wrapper around macOS native tools like ifconfig, route, and networksetup. It is not a full port of Linux iproute2. Some functionality is limited compared to Linux and command output isn’t fully compatible. Nevertheless I find it way better then relying on the ifconfig MacOS provides.

Cleaning up the Output

Unfortunately the output of a ip a on MacOS is everything but not something I would consider clean and minimal. There are alot of internal interfaces and you might be at first sight a little lost.

One way is to only show active interfaces via:

ip link | grep -B1 "state UP"

My ip alias / fish function

I love clean output which give me needed information about my interfaces at the first sight. As I prefer fish shell over the MacOS default zsh this only works if you also using fish. I will write something about my fish setup in another post.

But if you are on fish shell for MacOS you can copy the function below to your fish config file. The Function is actually a wrapper for the ip command. After this wrapper is active if you run ip without any argument it detects the default interface and deduplicate others finally trying to get IP Addr and some stats for each active interface. The display looks like this:

output of ip wrapper function fish shell MacOS

pretty ip wrapper for fish config

As said, just add this function either directly to your fish config file or in the custom fish function directory.

A user-friendly fish wrapper around the ip tool that prints aligned, colorized IPv4 interface info (including active utun tunnels), plus a filtered view of routes and extra subnets.

    # prettify the ip Command (simple, aligned, colored)
    function ip --wraps ip --description 'ip information (aligned IPv4 column; includes active utun tunnels; dedup; dynamic name padding)'
        if test -n "$argv"
            grc /opt/homebrew/bin/ip -p -4 $argv
            return
        end

        set -l IP /opt/homebrew/bin/ip
        set -l defif (route -n get default 2>/dev/null | string match -r --groups-only 'interface:\s*([^\s]+)')

        # Collect and deduplicate interfaces (include en* and utun*)
        set -l ifaces (ifconfig -l | tr ' ' '\n' | command grep -E '^(en[0-9]+|utun[0-9]+)$' | sort -u)

        set_color brcyan
        echo "Interfaces:"
        set_color normal

        # Pre-pass: compute max visible interface name length (incl. " *" for default)
        set -l name_target 0
        for n in $ifaces
            set -l nv $n
            if test "$n" = "$defif"
                set nv "$nv *"
            end
            set -l nl (string length -- $nv)
            if test $nl -gt $name_target
                set name_target $nl
            end
        end

        for name in $ifaces
            set -l cfg (ifconfig $name)

            # IP (prefer 'ip', fallback to ifconfig incl. tunnels)
            set -l ipcidr ($IP -4 addr show dev $name 2>/dev/null | awk '/inet /{print $2; exit}')
            if test -z "$ipcidr"
                set -l ip (string match -r --groups-only 'inet\s+([0-9.]+)' -- $cfg | head -n 1)
                set -l peer_cidr (string match -r --groups-only 'peer\s+([0-9.]+/[0-9]+)' -- $cfg | head -n 1)
                if test -n "$ip"
                    if test -n "$peer_cidr"
                        set -l prefix (string replace -r '^[0-9.]+/' '' -- $peer_cidr)
                        set ipcidr "$ip/$prefix"
                    else
                        set ipcidr "$ip"
                    end
                end
            end
            test -n "$ipcidr"; or continue

            # State/flags and tunnel activity
            set -l state (string upper -- (string match -r --groups-only 'status:\s*([A-Za-z]+)' -- $cfg))
            set -l flags (string match -r --groups-only 'flags=[0-9]+<([^>]+)>' -- $cfg)
            set -l is_tun (string match -rq '^utun[0-9]+$' -- $name; and echo 1; or echo 0)
            set -l is_running 0
            if test -n "$flags"
                if string match -rq '(^|,)RUNNING(,|$)' -- $flags
                    set is_running 1
                end
            end
            if test -z "$state"
                if test $is_running -eq 1
                    set state ACTIVE
                else
                    set state UNKNOWN
                end
            end
            if test $is_tun -eq 1
                if not string match -q "ACTIVE" "$state"
                    and test $is_running -ne 1
                    continue
                end
            end

            set -l mtu (string match -r --groups-only 'mtu\s+([0-9]+)' -- $cfg)
            set -l mac (string match -r --groups-only 'ether\s+([0-9a-f:]+)' -- $cfg)
            if test -z "$mac"; set mac "-"; end

            # Name column padding (dynamic)
            set -l name_visible $name
            if test "$name" = "$defif"
                set name_visible "$name_visible *"
            end
            set -l nlen (string length -- $name_visible)
            set -l pad (math "$name_target - $nlen"); test $pad -lt 0; and set pad 0

            set_color green; printf "%s" $name
            if test "$name" = "$defif"; set_color yellow; printf " *"; end
            set_color normal; printf "%s" (string repeat -n (math "$pad + 2") " ")

            set_color brblack; printf "State: "
            if string match -q "ACTIVE" "$state"; set_color green; else; set_color red; end
            printf "%s" "$state"

            # IPv4 padded to fixed min width for aligned MTU column
            set -l ip_target 18
            set -l ip_len (string length -- $ipcidr)
            set -l ip_pad (math "$ip_target - $ip_len"); test $ip_pad -lt 0; and set $ip_pad 0

            set_color brblack; printf "\tIPv4: "; set_color brwhite; printf "%s" "$ipcidr"
            set_color normal; printf "%s" (string repeat -n $ip_pad " ")
            set_color brblack; printf "\tMTU: "; set_color brwhite; printf "%s" "$mtu"
            set_color brblack; printf "\tMAC: "; set_color brwhite; printf "%s\n" "$mac"
            set_color normal
        end

        # Routes: default interface only (old approach) + extra subnets not on default iface
        set -l routes_all ($IP -4 route show)

        if test -n "$defif"
            echo
            set_color brmagenta; echo "Routes ($defif):"; set_color normal
            set -l def_prefixes

            for line in (string split \n -- $routes_all)
                string match -qr " dev $defif( |\$)" -- $line; or continue

                # default on default iface
                if string match -qr '^default( |\$)' -- $line
                    set_color yellow; echo "  $line"; set_color normal
                    continue
                end

                # skip host /32, and noisy prefixes
                string match -qr '/32( |\$)' -- $line; and continue
                set -l prefix (string split ' ' -- $line)[1]
                string match -qr '^(127\.|169\.254\.|255\.|224\.)' -- $prefix; and continue

                set_color blue; echo "  $line"; set_color normal
                set -a def_prefixes $prefix
            end

            # Extra subnets from non-default ifaces, not already in def_prefixes
            set -l extras
            for line in (string split \n -- $routes_all)
                string match -qr " dev $defif( |\$)" -- $line; and continue
                string match -qr '^default( |\$)' -- $line; and continue
                string match -qr '/32( |\$)' -- $line; and continue

                set -l prefix (string split ' ' -- $line)[1]
                string match -qr '^(127\.|169\.254\.|255\.|224\.)' -- $prefix; and continue
                contains -- $prefix $def_prefixes; and continue

                set -a extras $line
            end

            if test (count $extras) -gt 0
                echo
                set_color brmagenta; echo "Extra subnets:"; set_color normal
                for line in $extras
                    set -l prefix (string split ' ' -- $line)[1]
                    set -l via (string match -r --groups-only ' via ([^ ]+)' -- $line)
                    set -l iface (string match -r --groups-only ' dev ([^ ]+)' -- $line)
                    set_color green
                    if test -n "$via"
                        echo "  $prefix via $via on $iface"
                    else
                        echo "  $prefix on $iface"
                    end
                    set_color normal
                end
            end
        end
    end