Posted on ::

It has always bugged me that dnf search has no indication in the output whether a package is already installed. In fact, this feature request already exists for at least 10 years. Let’s do it manually!

The standard output looks like this:

john@thinkpad ~> dnf search --cacheonly keepass
Updating and loading repositories:
Repositories loaded.
Matched fields: name (exact)
 keepass.x86_64       Password manager
Matched fields: name, summary
 perl-File-KeePass.noarch     Interface to KeePass V1 and V2 database files
 python3-pass-import+keepass.noarch   Metapackage for python3-pass-import: keepass extras
 python3-pykeepass.noarch     Python library to interact with keepass databases
Matched fields: name
 keepassxc.x86_64     Cross-platform password manager
Matched fields: summary
 kpcli.noarch   KeePass Command Line Interface (CLI) / interactive shell

First, we fetch the installed packages (and their installation reason) into an indexable list:

~> set -l installed (dnf repoquery --installed --qf '%{name} [%{reason}]\n' "*keepass*" 2>/dev/null)
~> echo $installed[1]
keepassxc [User]

The reason is one of Dependency, External User, Group, User and Weak Dependency, hence we can later use the first character (D/E/G/U/W) as a unique abbreviation.

Now let’s extract the package name from the dnf search output above.

 python3-pass-import+keepass.noarch   Metapackage for python3-pass-import: keepass extras

We can use a simple capture group ^\s+([^\.]+)\..*:

~> string replace -r '^\s+([^\.]+)\..*' '$1' ' python3-pass-import+keepass.noarch   Metapackage for python3-pass-import: keepass extras'
python3-pass-import+keepass

and use this string to extract the reason from the list of installed packages (<pkg_name> [<reason>]).

But we first have to escape all characters that have a special regex meaning:

~> string escape --style=regex "python3-pass-import+keepass"
python3\-pass\-import\+keepass

And voila:

~> string match -rg "^python3\-pass\-import\+keepass \[(.*)\]" $installed
User

If this output is non-zero, then we have the two informations that a) this package is installed and b) the reason why it was installed.

Wrap everything up in a function:

function ds -d "dnf search with [installed] markers"
    set -l term $argv[1]
    if test -z "$term"
        echo "Usage: "(status current-function)" <search-term>"
        return 1
    end

    # installed packages -> local array
    # E.g. `gcc-c++ [User]`
    set -l installed (dnf repoquery --installed --qf '%{name} [%{reason}]\n' "*$term*" 2>/dev/null)

    dnf search --cacheonly "$term" 2>/dev/null | while read -l line

        # package lines start with a space
        # E.g. ' gcc.x86_64     Various compilers (C, C++, Objective-C, ...)'
        if string match -qr '^\s+\S' "$line"

            # extract `gcc`
            set -l pkg_name (string replace -r '^\s+([^\.]+)\..*' '$1' "$line")

            # ecape in case it contains regex special characters (e.g. ++)
            set pkg_name (string escape --style=regex "$pkg_name")

            # extract the reason
            set -l reason (string match -rg "^$pkg_name \[(.*)\]" $installed)

            if set -q reason[1]

                # extract the first character
                set reason (string sub -l 1 "$reason[1]")

                echo "[$reason]$line"

            else
                echo "   $line"
            end
        else
            echo "$line"
        end
    end
end
~> ds keepass
Updating and loading repositories:
Repositories loaded.
Matched fields: name (exact)
    keepass.x86_64      Password manager
Matched fields: name, summary
    perl-File-KeePass.noarch    Interface to KeePass V1 and V2 database files
[U] python3-pass-import+keepass.noarch  Metapackage for python3-pass-import: keepass extras
[D] python3-pykeepass.noarch    Python library to interact with keepass databases
Matched fields: name
[U] keepassxc.x86_64    Cross-platform password manager
Matched fields: summary
    kpcli.noarch        KeePass Command Line Interface (CLI) / interactive shell

This works nicely but the colors are gone!

To fix that, we need a little hack: dnf probably checks internally if it’s attached to a terminal and only then prints color codes, even if we explicitly tell it to do so (--color=always).

Check for yourself:

dnf search --cacheonly --color=always keepass | while read -l line
    echo $line
end

But we can give it the terminal it wants (dnf install util-linux-script):

script -q -c "dnf search --cacheonly --color=always keepass" /dev/null | while read -l line
    echo $line
end

Now the colors should appear, e.g. in our line from above:

 python3-pass-import+keepass.noarch   Metapackage for python3-pass-import: keepass extras

But this is just the rendered version from the terminal.

What’s actually present is the following:

 python3-pass-import+ESC[32mkeepassESC[0m.noarch        Metapackage for python3-pass-import: ESC[32mkeepassESC[0m extras

Since I do not aim to support all ANSI codes, a simple \x1b\[[0-9;]+m should suffice:

            # remove ansi color codes
            set -l pkg_name (string replace -ra '\x1b\[[0-9;]+m' '' "$line")

            # extract `gcc`
            set pkg_name (string replace -r '^\s+([^\.]+)\..*' '$1' "$pkg_name")

With some additional own color codes,

echo (set_color white)"[$reason]"(set_color normal)"$line"

the final version is:

function ds -d "dnf search with [installed] markers"
    argparse 'h/help' -- $argv
    or return

    set -l term $argv[1]
    if set -q _flag_help; or test -z "$term"
        echo "Usage: "(status current-function)" <search-term>"
        printf '\n[X] if installed, with X one of the following reasons:\n'
        printf 'D: Dependency\tE: External User\tG: Group\nU: User \tW: Weak Dependency'
        return 1
    end

    # installed packages -> local array
    # E.g. `gcc-c++ [User]`
    set -l installed (dnf repoquery --installed --qf '%{name} [%{reason}]\n' "*$term*" 2>/dev/null)

    # Exclude color ansi codes from the beginning if stdout is not a tty
    set -l istty false
    if isatty stdout
        set istty true
    end

    begin
        if $istty
            script -q -c "dnf search --color=always --cacheonly \"$term\" 2>/dev/null" /dev/null
        else
            # no color codes
            dnf search --cacheonly --color=never "$term" 2>/dev/null
        end
    end | while read -l line

        # package lines start with a space
        # E.g. ' gcc.x86_64     Various compilers (C, C++, Objective-C, ...)'
        if string match -qr '^\s+\S' "$line"

            # remove ansi color codes
            set -l pkg_name (string replace -ra '\x1b\[[0-9;]+m' '' "$line")

            # extract `gcc`
            set pkg_name (string replace -r '^\s+([^\.]+)\..*' '$1' "$pkg_name")

            # ecape in case it contains regex special characters (e.g. +)
            set pkg_name (string escape --style=regex "$pkg_name")

            # extract the reason
            set -l reason (string match -rg "^$pkg_name \[(.*)\]" $installed)

            # if installed
            if set -q reason[1]

                # extract the first character
                set reason (string sub -l 1 "$reason[1]")

                if $istty
                    # add some color
                    echo (set_color white)"[$reason]"(set_color normal)"$line"
                else
                    echo "[$reason]$line"
                end
            else
                echo "   $line"
            end
        else
            echo "$line"
        end
    end
end
~> ds keepass
Matched fields: name (exact)
    keepass.x86_64    Password manager
Matched fields: name, summary
    perl-File-KeePass.noarch  Interface to KeePass V1 and V2 database files
[U] python3-pass-import+keepass.noarch       Metapackage for python3-pass-import: keepass extras
[D] python3-pykeepass.noarch Python library to interact with keepass databases
Matched fields: name
[U] keepassxc.x86_64 Cross-platform password manager
Matched fields: summary
    kpcli.noarch        KeePass Command Line Interface (CLI) / interactive shell