Skip to content

Instantly share code, notes, and snippets.

@ExpHP
Last active January 21, 2019 04:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ExpHP/741d095d78a288049ec6e1b3aec5b08e to your computer and use it in GitHub Desktop.
Save ExpHP/741d095d78a288049ec6e1b3aec5b08e to your computer and use it in GitHub Desktop.
xdg open_generic test

Testing setup

I improved my testing setup a bit from what I used in this comment. Now you can see when argdebug gets called multiple times.

$HOME/bin/argdebug

#!/bin/sh
echo >&2 'argdebug'
for x in "$@"; do
	echo >&2 "    '$x'"
done

$HOME/.local/share/applications/test-protocol.desktop

#!/usr/bin/env xdg-open
[Desktop Entry]
Name=Test Protocol
Exec=argdebug "%u"
Icon=emacs-icon
Type=Application
Terminal=false
MimeType=x-scheme-handler/test-protocol;
NoDisplay=true

Don't forget to sudo update-desktop-database! (as well as anything specific to your DE, such as kbuildsyscocoa5 in my case)

Overriding desktop-environment detection in xdg-open

On most systems, xdg-open itself does extremely little work. It tries to detect your desktop environment, and then typically delegates to whatever tools your DE provides.

$HOME/bin/xdg-open-ExpHP is a modified copy of xdg-open that lets us select the desktop environment it uses at runtime.

--- /usr/bin/xdg-open   2017-05-30 17:23:44.243430717 -0400
+++ /home/lampam/xdg-open-ExpHP 2017-08-20 12:08:53.409952763 -0400
@@ -974,6 +974,10 @@
         ;;
 esac
 
+if [ x"$OVERRIDE_DE" != x"" ]; then
+    DE=$OVERRIDE_DE
+fi
+
 case "$DE" in
     kde)
     open_kde "$url"

Results

First, a review

Let's review the facts:

  • On my system, xdg-open calls kde-open5.
    • kde-open5 is called with two colons...
    • ...but then kde-open5 calls argdebug with only one colon...
    • ...and adding a third slash after the first colon somehow prevents this from happening.
  • @nerikj reports that xdg-open calls gio open on his system.
    • once again, gio open is called with two colons...
    • ...but then gio open calls emacs-capture with only one colon...
    • ...and adding a third slash after the first colon somehow prevents this from happening.

What we see here are two different toolkits exhibiting the exact same behavior. So either:

  • Possibility A: The way these programs treat URLs is actually the correct way.
    • I wouldn't know and I'm too lazy to RTFP right now, but, uh... that would seem really really weird??
  • Possibility B: The behavior is in some library that both desktop environments depend on.

To help rule out possibility A, I will try forcing xdg-open to use its fallback logic.

Point of comparison.

First, for a point of comparison, let's repeat my experiments from here.

Here's what I normally see using xdg-open.

$ xdg-open-gio "test-protocol://lol://a/b/c"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-protocol"
kf5.kio.core: "preferred service for x-scheme-handler/test-protocol" "test-protocol"
argdebug
    'test-protocol://lol//a/b/c

You can see that argdebug is called without the second colon. However, it isn't xdg-open's fault, because if you look at the trace output, you see that the colon is still there in the call to kde-open5:

$ bash -x $(which xdg-open-ExpHP) "test-protocol://lol://a/b/c" 2>&1 | tail -n12
+ case "${KDE_SESSION_VERSION}" in
+ kde-open5 test-protocol://lol://a/b/c
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-protocol"
kf5.kio.core: "preferred service for x-scheme-handler/test-protocol" "test-protocol"
argdebug
    'test-protocol://lol//a/b/c'
+ '[' 0 -eq 0 ']'
+ exit_success
+ '[' 0 -gt 0 ']'
+ exit 0

Ok, so far so good; this is consistent with what I saw before.

Forcing xdg-open to use its fallback logic

Here comes the new stuff.

When xdg-open cannot detect your DE, it falls back to open_generic, which is its own built-in fallback logic for parsing URLs and .desktop files. We can force it to use this logic by setting DE to 'generic':

$ OVERRIDE_DE=generic xdg-open-ExpHP "test-protocol://lol://L/a/b/c"
argdebug
    '"%u"'
    'test-protocol://lol://L/a/b/c'

-oof. That's odd. So, this output tells us two things:

  • The fallback logic in xdg-open does NOT eat the colon!!! (so we see that xdg-open IS capable of calling the program correctly, with enough prodding!)
  • For some reason, we're seeing that open_generic calls our program with two arguments.
    • For some reason, the "%u" is still there!

Okay, so what's the deal with the "%u"?

Looking closely at the search_desktop_file function in xdg-open, we see logic for performing the substitutions:

set -- $(get_key "${file}" "Exec" | last_word)
# We need to replace any occurrence of "%f", "%F" and
# the like by the target file. We examine each
# argument and append the modified argument to the
# end then shift.
local args=$#
local replaced=0
while [ $args -gt 0 ]; do
    case $1 in
        %[c])
            replaced=1
            arg="${localised_name}"
            shift
            set -- "$@" "$arg"
            ;;
        %[fFuU])
            replaced=1
            arg="$target"
            shift
            set -- "$@" "$arg"
            ;;
        %[i])
            replaced=1
            shift
            set -- "$@" "--icon" "$icon"
            ;;
        *)
            arg="$1"
            shift
            set -- "$@" "$arg"
            ;;
    esac
    args=$(( $args - 1 ))
done
[ $replaced -eq 1 ] || set -- "$@" "$target"

Ah. It is failing to substitute "%u" because it is looking specifically for %u (without quotes). It ends up hitting the set in the last statement, which basically says, "if you didn't find anything to replace, just add the filename to the end of the arguments in Exec."

...okey dokey then. I see two possible fixes for this. I wonder, will either of them also fix it for kde-open5?

Two more configurations to test

~/.local/share/applications/test-b-protocol.desktop (%u without quotes in Exec)

#!/usr/bin/env xdg-open
[Desktop Entry]
Name=Test Protocol
Exec=argdebug %u
Icon=emacs-icon
Type=Application
Terminal=false
MimeType=x-scheme-handler/test-b-protocol;
NoDisplay=true

~/.local/share/applications/test-c-protocol.desktop (no %u pattern in Exec)

#!/usr/bin/env xdg-open
[Desktop Entry]
Name=Test Protocol
Exec=argdebug
Icon=emacs-icon
Type=Application
Terminal=false
MimeType=x-scheme-handler/test-c-protocol;
NoDisplay=true

As expected, both of these configurations work perfectly with open_generic:

$ OVERRIDE_DE=generic xdg-open-ExpHP "test-b-protocol://lol://L/a/b/c"
argdebug
    'test-b-protocol://lol://L/a/b/c'
$ OVERRIDE_DE=generic xdg-open-ExpHP "test-c-protocol://lol://L/a/b/c"
argdebug
    'test-c-protocol://lol://L/a/b/c'

But neither work correctly with kde-open5.

$ xdg-open-ExpHP "test-b-protocol://lol://L/a/b/c"                  
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-b-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-b-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-b-protocol"
kf5.kio.core: "preferred service for x-scheme-handler/test-b-protocol" "test-b-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-b-protocol"
argdebug
    'test-b-protocol://lol//L/a/b/c'
$ xdg-open-ExpHP "test-c-protocol://lol://L/a/b/c"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-c-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-c-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-c-protocol"
kf5.kio.core: "preferred service for x-scheme-handler/test-c-protocol" "test-c-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-c-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-c-protocol"
kf5.kio.core: Refilling KProtocolInfoFactory cache in the hope to find "test-c-protocol"
kf5.kio.core: "preferred service for x-scheme-handler/test-c-protocol" "test-c-protocol"
argdebug
    ''

We see here that, for KDE, %u works like "%u", and it does not add the raw URL if you omit the %u. (instead it does the moronic act of calling argdebug with a single empty argument (as opposed to no arguments like any sane program would do). My money is on this being a default-initialized std::string :P)

Summary

  • xdg-open itself never deletes the second colon, not even when you let it do all of the work itself. (open_generic)
    • if you force it to do all of the work itself, then Exec=_emacs-client %u (no quotes!) or Exec=_emacs-client will produce the correct behavior.
    • alas, these still won't work for KDE
  • gio open and kde-open5 share the exact same colon-eating bug!
    • They probably use the same library for something... but what?
  • There is no solace. Only pain and misfortune.
    • xdg-open and kde-open5 disagree about how to handle a double-quoted "%u", or the omission of %u.
    • Either the .desktop format is horribly underspecified, or all of its implementations just suck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment