Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New parser: update-alternatives --query #230

Closed
chriscroome opened this issue Apr 21, 2022 · 29 comments
Closed

New parser: update-alternatives --query #230

chriscroome opened this issue Apr 21, 2022 · 29 comments
Assignees

Comments

@chriscroome
Copy link
Contributor

The Debian alternatives system has a update-alternatives command which has a --query option which will:

Display information about the link group like --display does, but in a machine parseable way (since version 1.15.0, see section QUERY FORMAT below).

For example for the editor (omitting the Slaves output):

update-alternatives --query editor
Name: editor
Link: /usr/bin/editor
Status: manual
Best: /bin/nano
Value: /usr/bin/vim.basic

Alternative: /bin/nano
Priority: 40

Alternative: /usr/bin/nvim
Priority: 30

Alternative: /usr/bin/vim.basic
Priority: 30

Alternative: /usr/bin/vim.tiny
Priority: 15

Perhaps this might be a useful new parser for jc, the Ansible alternatives module is only able to set alternatives, not read them, so I've written some Bash to use as a Ansible facts.d script for converting the output into JSON:

#!/usr/bin/env bash

set -euo pipefail

jo $(
  echo "${1}"=$( 
    if query=$(update-alternatives --query "${1}")
    then
      declare -a alternatives=()
      readarray -t alternatives < <(grep -e '^Alternative' <<< "${query}" | sed 's/Alternative: //')
      declare -a priorities=()
      readarray -t priorities < <(grep -e '^Priority' <<< "${query}" | sed 's/Priority: //')
      jo name="${1}" link=$(
        grep ^Link <<< "${query}" | sed 's/^Link: //'
      ) value=$(
        grep ^Value <<< "${query}" | sed 's/^Value: //'
      ) best=$(
        grep ^Best <<< "${query}" | sed 's/Best: //'
      ) alternatives=$(
        jo -a $(
          i=0
          while [[ "${i}" -lt "${#alternatives[@]}" ]]
          do
            jo path="${alternatives[${i}]}" priority="${priorities[${i}]}" 
            ((i++))
          done
        )
      )
    else
      jo state=absent
    fi
  )
)

Running this script with an argument, for example editor results in:

{
  "editor": {
    "name": "editor",
    "link": "/usr/bin/editor",
    "value": "/usr/bin/vim.basic",
    "best": "/bin/nano",
    "alternatives": [
      {
        "path": "/bin/nano",
        "priority": 40
      },
      {
        "path": "/usr/bin/nvim",
        "priority": 30
      },
      {
        "path": "/usr/bin/vim.basic",
        "priority": 30
      },
      {
        "path": "/usr/bin/vim.tiny",
        "priority": 15
      }
    ]
  }
}
@kellyjonbrazil
Copy link
Owner

Thank you for the parser suggestion. Sweet script! I’ll definitely take a look at this.

@kellyjonbrazil
Copy link
Owner

kellyjonbrazil commented Apr 22, 2022

Is this fairly representative of the output? Are there any fields you see missing?

Name: editor
Link: /usr/bin/editor
Slaves:
 editor.1.gz /usr/share/man/man1/editor.1.gz
 editor.da.1.gz /usr/share/man/da/man1/editor.1.gz
 editor.de.1.gz /usr/share/man/de/man1/editor.1.gz
 editor.fr.1.gz /usr/share/man/fr/man1/editor.1.gz
 editor.it.1.gz /usr/share/man/it/man1/editor.1.gz
 editor.ja.1.gz /usr/share/man/ja/man1/editor.1.gz
 editor.pl.1.gz /usr/share/man/pl/man1/editor.1.gz
 editor.ru.1.gz /usr/share/man/ru/man1/editor.1.gz
Status: auto
Best: /bin/nano
Value: /bin/nano

Alternative: /bin/ed
Priority: -100
Slaves:
 editor.1.gz /usr/share/man/man1/ed.1.gz

Alternative: /bin/nano
Priority: 40
Slaves:
 editor.1.gz /usr/share/man/man1/nano.1.gz

Alternative: /usr/bin/vim.basic
Priority: 30
Slaves:
 editor.1.gz /usr/share/man/man1/vim.1.gz
 editor.da.1.gz /usr/share/man/da/man1/vim.1.gz
 editor.de.1.gz /usr/share/man/de/man1/vim.1.gz
 editor.fr.1.gz /usr/share/man/fr/man1/vim.1.gz
 editor.it.1.gz /usr/share/man/it/man1/vim.1.gz
 editor.ja.1.gz /usr/share/man/ja/man1/vim.1.gz
 editor.pl.1.gz /usr/share/man/pl/man1/vim.1.gz
 editor.ru.1.gz /usr/share/man/ru/man1/vim.1.gz

Alternative: /usr/bin/vim.tiny
Priority: 15
Slaves:
 editor.1.gz /usr/share/man/man1/vim.1.gz
 editor.da.1.gz /usr/share/man/da/man1/vim.1.gz
 editor.de.1.gz /usr/share/man/de/man1/vim.1.gz
 editor.fr.1.gz /usr/share/man/fr/man1/vim.1.gz
 editor.it.1.gz /usr/share/man/it/man1/vim.1.gz
 editor.ja.1.gz /usr/share/man/ja/man1/vim.1.gz
 editor.pl.1.gz /usr/share/man/pl/man1/vim.1.gz
 editor.ru.1.gz /usr/share/man/ru/man1/vim.1.gz

Looks pretty simple to parse - I can probably get this into the next release.

The schema could look something like this:

{
    "name":                     string,
    "link":                     string,
    "slaves": [
        {
            "name":             string,
            "path":             string
        }
    ],
    "status":                   string,
    "best":                     string,
    "value":                    string,    # null if 'none'?
    "alternatives": [
        {
            "name":             string,
            "priority":         integer,
            "slaves": [
                {
                    "name":     string,
                    "path":     string
                }
            ]
        }
    ]
}

Also, I wonder if there would be any value to creating a parser for updated-alternatives --get-selections?

@kellyjonbrazil kellyjonbrazil self-assigned this Apr 22, 2022
@chriscroome
Copy link
Contributor Author

Yes to all the above!

I've had a quick scan through the output of this:

for alt in $(update-alternatives --get-selections | awk '{ print $1 }'); do update-alternatives --query ${alt}; done | less

And I didn't see anything that didn't match the pattern above.

I agree that it would be good to also generate JSON from update-alternatives --get-selections.

I'm currently working on a Ansible role for which I picked a slightly different format, a dictionary rather than a list:

alternatives_update:
  'php':
    state: present
    link: /usr/bin/php
    alternatives:
      - path: /usr/bin/php7.4
        state: present
        priority: 50
      - path: /usr/bin/php8.0
        state: present
        priority: 80
      - path: /usr/bin/php8.1
        state: present
        priority: 81
  'php-fpm.sock':
    link: /run/php/php-fpm.sock
    state: present
    alternatives:
      - path: /run/php/php7.4-fpm.sock
        state: present
        priority: 74
      - path: /run/php/php8.0-fpm.sock
        state: present
        priority: 80
      - path: /run/php/php8.1-fpm.sock
        state: present
        priority: 81

But your format probably makes more sense so I'll look at updating it, your version in YAML is this:

name: string
link: string
slaves:
  - name: string
    path: string
status: string
best: string
value: string
alternatives:
  - name: string
    priority: integer
    slaves:
      - name: string
        path: string

@kellyjonbrazil
Copy link
Owner

I notice you have a state field in your examples. Is that needed in the jc parser?

@chriscroome
Copy link
Contributor Author

Sorry I should have omitted that above, state is just something I added, when state: absent I'll use Ansible to remove selection groups or alternatives -- unless jc adds additional variables for any other parsers then I don't think you should for update-alternatives.

@kellyjonbrazil
Copy link
Owner

I have a working version in dev for update-alternatives --query if you would like to test:

https://github.com/kellyjonbrazil/jc/blob/dev/jc/parsers/update_alt_q.py

You can copy this file to your plugin folder and it should work like update-alternatives --query | jc --update-alt-q or jc update-alternatives --query.

@kellyjonbrazil
Copy link
Owner

A working version of the update-alternatives --get-selections parser is also available in dev if you would like to test:

https://github.com/kellyjonbrazil/jc/blob/dev/jc/parsers/update_alt_gs.py

Usage is similar: update-alternatives --get-selections | jc --update-alt-gs or jc update-alternatives --get-selections

@chriscroome
Copy link
Contributor Author

Thanks, I've done this:

pip install jc
mkdir -p "${HOME}/.local/share/jc/jcparsers"
cd "${HOME}/.local/share/jc/jcparsers"
wget https://raw.githubusercontent.com/kellyjonbrazil/jc/dev/jc/parsers/update_alt_q.py 
cd
update-alternatives --get-selections | jc --update-alt-q

And the output is:

jc:     missing or incorrect arguments

Have I missed something? I also tried:

update-alternatives --query editor | jc --update-alt-q

And that has the same result.

@chriscroome
Copy link
Contributor Author

Sorry, I half read the first of your comments above via email and then the second via the web and got them mixed up... trying again...

cd "${HOME}/.local/share/jc/jcparsers"
wget https://raw.githubusercontent.com/kellyjonbrazil/jc/dev/jc/parsers/update_alt_gs.py -O update_alt_gs.py
wget https://raw.githubusercontent.com/kellyjonbrazil/jc/dev/jc/parsers/update_alt_q.py -O update_alt_q.py
cd
update-alternatives --query editor | jc --update-alt-q

jc:     missing or incorrect arguments

update-alternatives --get-selections | jc --update-alt-gs

jc:     missing or incorrect arguments

Am I doing something wrong?

@kellyjonbrazil
Copy link
Owner

I tried the above on an ubuntu VM and it seemed to work. What happens if you do jc -h and jc -ap. Does --update-alt-q show up at the bottom?

$ jc -h
<snip>
        --xrandr         `xrandr` command parser
        --yaml           YAML file parser
        --zipinfo        `zipinfo` command parser
        --update-alt-q   `update-alternatives --query` command parser

Options:
        -a    about jc
        -C    force color output even when using pipes (overrides -m)
        -d    debug (-dd for verbose debug)
        -h    help (-h --parser_name for parser documentation)
        -m    monochrome output
        -p    pretty print output
        -q    quiet - suppress parser warnings (-qq to ignore streaming errors)
        -r    raw JSON output
        -u    unbuffer output
        -v    version info

Examples:
        Standard Syntax:
            $ dig www.google.com | jc --dig -p

        Magic Syntax:
            $ jc -p dig www.google.com

        Parser Documentation:
            $ jc -h --dig
$ jc -ap
<snip>
    {
      "name": "update_alt_q",
      "argument": "--update-alt-q",
      "version": "1.0",
      "description": "`update-alternatives --query` command parser",
      "author": "Kelly Brazil",
      "author_email": "[email protected]",
      "compatible": [
        "linux"
      ],
      "magic_commands": [
        "update-alternatives --query"
      ],
      "plugin": true
    }
  ]
}

@chriscroome
Copy link
Contributor Author

Weird, yes the plugins are there:

jc -h 2>&1 | grep update
        --update_alt_gs  `update-alternatives --get-selections` command parser
        --update_alt_q   `update-alternatives --query` command parser

And jc -ap also lists the two plugins at the end...

@kellyjonbrazil
Copy link
Owner

kellyjonbrazil commented Apr 22, 2022

Funny because this feels like you have an older version of jc. Was this recently installed? Curious of the version of jc running. I did recently add plugin support for parser-names containing dashes. (v1.18.2)

@chriscroome
Copy link
Contributor Author

chriscroome commented Apr 22, 2022

I was just wondering if this was the issue also, I appear to be running the Debian Buster version:

cat $(which jc) | head
#!/usr/bin/python3
# EASY-INSTALL-ENTRY-SCRIPT: 'jc==1.14.3','console_scripts','jc'
import re
import sys

# for compatibility with easy_install; see #2198
__requires__ = 'jc==1.14.3'

try:
    from importlib.metadata import distribution

@kellyjonbrazil
Copy link
Owner

kellyjonbrazil commented Apr 22, 2022

In that case I think it will work if you remove the underscores in the filenames. I haven't tested on a version that old, though. jc -v or jc -a will give you the running version. Then you will also need to remove the dashes when calling the parser in the command line.

@chriscroome
Copy link
Contributor Author

This did the trick...

wget "https://github.com/kellyjonbrazil/jc/releases/download/v1.18.6/jc_1.18.6-1_amd64.deb"
gdebi jc_1.18.6-1_amd64.deb

Now both these work:

update-alternatives --query editor | jc --update-alt-q
update-alternatives --get-selections | jc --update-alt-gs

That's fantastic!

I'll stop writing Bash to parse the output and write an Ansible role to install the latest version of jc instead!

@kellyjonbrazil
Copy link
Owner

Right on! I still need to write some tests and then I'll add these into v1.18.7. I'll probably release it by next week some time. Thanks for your contributions!

@chriscroome
Copy link
Contributor Author

Thanks again for your work on this @kellyjonbrazil, I've written an Ansible role to install jc using debs from GitHub and I'll now work on a role that uses the parsers you have written to update alternatives.

@chriscroome
Copy link
Contributor Author

One thought, the name which is in caps here:

{
    "name":                     string,
    "link":                     string,
    "slaves": [
        {
            "name":             string,
            "path":             string
        }
    ],
    "status":                   string,
    "best":                     string,
    "value":                    string,    # null if 'none'?
    "alternatives": [
        {
            "NAME":             string,
            "priority":         integer,
            "slaves": [
                {
                    "name":     string,
                    "path":     string
                }
            ]
        }
    ]
}

Is described like this in the man page:

alternative (or alternative path)
The name of a specific file in the filesystem, which may be made accessible via a generic name using the alternatives system.

Since it is a absolute path it might make more sense to use path or even alternative to avoid potential confusion?

@kellyjonbrazil
Copy link
Owner

Yeah, that makes sense. There are a lot of ‘name’s there that can get confusing. I can change that no problem.

@chriscroome
Copy link
Contributor Author

chriscroome commented Apr 25, 2022

Thinking about this some more I think the best approach would be to stick closely to what is in the --query output and the manpage, so use alternative rather than name for Alternative, eg like in this copy of the file, I've tested this and created a pull request for this in case that helps.

@kellyjonbrazil
Copy link
Owner

Sounds good - yeah I was thinking the same thing, too.

@kellyjonbrazil
Copy link
Owner

I'm going to release this now in the v1.18.7 release on PIP-only. If things test ok, then I'll bump the github release as well. Thanks!

@chriscroome
Copy link
Contributor Author

There seems to be a problem here:

update-alternatives --query php-fpm.sock
Name: php-fpm.sock
Link: /run/php/php-fpm.sock
Status: auto
Best: /run/php/php8.1-fpm.sock
Value: /run/php/php8.1-fpm.sock

Alternative: /run/php/php7.4-fpm.sock
Priority: 74

Alternative: /run/php/php8.0-fpm.sock
Priority: 80

Alternative: /run/php/php8.1-fpm.sock
Priority: 81
jc -p update-alternatives --query php-fpm.sock
{
  "name": "php-fpm.sock",
  "link": "/run/php/php-fpm.sock",
  "status": "auto",
  "best": "/run/php/php8.1-fpm.sock",
  "value": "/run/php/php8.1-fpm.sock",
  "alternatives": [
    {
      "alternative": "/run/php/php8.1-fpm.sock",
      "priority": 81
    }
  ]
}

Why are the alternatives missing from the JSON version? Any suggestions regarding how I might help debug this?

@kellyjonbrazil
Copy link
Owner

kellyjonbrazil commented Apr 27, 2022

let me take a look - it probably has to do with certain expected values missing (e.g. slaves)

@kellyjonbrazil
Copy link
Owner

Yep, was a little bug in the logic. Cleaned that up and the fix will go into v1.18.8. You can test it here:

https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/update_alt_q.py

@chriscroome
Copy link
Contributor Author

Great, thanks, I've tested it and it is working as expected, jc -p update-alternatives --query php-fpm.sock now results in:

{
  "name": "php-fpm.sock",
  "link": "/run/php/php-fpm.sock",
  "status": "auto",
  "best": "/run/php/php8.1-fpm.sock",
  "value": "/run/php/php8.1-fpm.sock",
  "alternatives": [
    {
      "alternative": "/run/php/php7.4-fpm.sock",
      "priority": 74
    },
    {
      "alternative": "/run/php/php8.0-fpm.sock",
      "priority": 80
    },
    {
      "alternative": "/run/php/php8.1-fpm.sock",
      "priority": 81
    }
  ]
}

I'm going to try to find time this evening to test a lot of other alternatives but first I want to write a Ansible yq role so I can generate YAML versions of the queries for feeding into the Ansible alternatives role I'm working on... BTW have you ever considered adding an option for jc to output YAML directly?

@kellyjonbrazil
Copy link
Owner

Nice! Let me know if you run into any other issues. I'm packaging v1.18.8 now. It's already on PIP and the binaries should be in github Releases soon.

It would be possible to output YAML in addition to JSON. I might think about that for v1.19.0. Maybe open a feature request issue so we can track it.

@chriscroome
Copy link
Contributor Author

Cheers, have done #237

@chriscroome
Copy link
Contributor Author

The Ansible role I was planning to use to test the jc JSON update-alternatives --query output doesn't seem to work for alternatives with lots of slaves like aptitude and editor and I won't have time to look at this more until next week sometime -- however I can't see any jc bugs :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants