Merge lp:~mvo/command-not-found/snap-support into lp:command-not-found

Proposed by Michael Vogt
Status: Merged
Merge reported by: Brian Murray
Merged at revision: not available
Proposed branch: lp:~mvo/command-not-found/snap-support
Merge into: lp:command-not-found
Diff against target: 199 lines (+62/-39)
3 files modified
CommandNotFound/CommandNotFound.py (+59/-38)
command-not-found (+1/-1)
debian/dirs (+2/-0)
To merge this branch: bzr merge lp:~mvo/command-not-found/snap-support
Reviewer Review Type Date Requested Status
Ubuntu Core Development Team Pending
Review via email: mp+335516@code.launchpad.net

Description of the change

This is a first preview for snap support in command-not-found. The idea is that snapd will populate /usr/share/command-not-found/snaps.d/all-main.db for command-not-found to consume.

To post a comment you must log in.
Revision history for this message
Brian Murray (brian-murray) wrote :

I believe this landed in Bionic.

command-not-found (0.3ubuntu18.04.0~pre3) bionic; urgency=medium

  * CommandNotFound/CommandNotFound.py:
    - limit input to 256 chars to avoid DoS (LP: #1605732)
    - add support for suggesting commands snap from snaps
      (needs snapd 2.31+ to work)
    - add "snapd" to suggests

 -- Michael Vogt <email address hidden> Thu, 15 Feb 2018 09:15:40 +0100

Revision history for this message
Brian Murray (brian-murray) wrote :

I'm setting this to merged to take it off the sponsorship list. Feel free to flip it back if I'm wrong.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CommandNotFound/CommandNotFound.py'
2--- CommandNotFound/CommandNotFound.py 2017-12-21 10:34:37 +0000
3+++ CommandNotFound/CommandNotFound.py 2017-12-21 11:57:34 +0000
4@@ -41,45 +41,39 @@
5 key = key.encode('utf-8', 'xmlcharrefreplace')
6 if self.db and key in self.db:
7 return self.db[key].decode('utf-8')
8- else:
9- return None
10+ return None
11
12
13 class FlatDatabase(object):
14
15 def __init__(self, filename):
16- self.rows = []
17+ self.cmd_to_pkgs = {}
18 with open(filename) as dbfile:
19- for line in (line.strip() for line in dbfile):
20- self.rows.append(line.split("|"))
21-
22- def lookup(self, column, text):
23- result = []
24- for row in self.rows:
25- if row[column] == text:
26- result.append(row)
27- return result
28-
29- def createColumnByCallback(self, cb, column):
30- for row in self.rows:
31- row.append(cb(row[column]))
32-
33- def lookupWithCallback(self, column, cb, text):
34- result = []
35- for row in self.rows:
36- if cb(row[column], text):
37- result.append(row)
38- return result
39+ for line in dbfile:
40+ line = line.strip()
41+ cmd, pkgs = line.split(":", 2)
42+ self.cmd_to_pkgs[cmd] = pkgs
43+
44+ def lookup(self, key):
45+ if key in self.cmd_to_pkgs:
46+ return self.cmd_to_pkgs[key]
47+ return None
48
49
50 class ProgramDatabase(object):
51
52 (PACKAGE, BASENAME_PATH) = range(2)
53
54- def __init__(self, filename):
55+ def __init__(self, filename, pkgmanager):
56+ if filename.endswith(".db"):
57+ self.db = BinaryDatabase(filename)
58+ elif filename.endswith(".txt"):
59+ self.db = FlatDatabase(filename)
60+ else:
61+ raise Exception("unsupported database %s" % filename)
62+ self.pkgmanager = pkgmanager
63 basename = os.path.basename(filename)
64- (self.arch, self.component) = basename.split(".")[0].split("-")
65- self.db = BinaryDatabase(filename)
66+ self.arch, self.component = basename.split(".")[0].split("-")
67
68 def lookup(self, command):
69 result = self.db.lookup(command)
70@@ -107,6 +101,7 @@
71 class CommandNotFound(object):
72
73 programs_dir = "programs.d"
74+ snaps_dir = "snaps.d"
75
76 prefixes = (
77 "/bin",
78@@ -128,8 +123,10 @@
79 'non-free', 'multiverse']
80 self.components.reverse()
81 self.sources_list = self._getSourcesList()
82- for filename in os.listdir(os.path.sep.join([data_dir, self.programs_dir])):
83- self.programs.append(ProgramDatabase(os.path.sep.join([data_dir, self.programs_dir, filename])))
84+ for filename in os.listdir(os.path.join(data_dir, self.programs_dir)):
85+ self.programs.append(ProgramDatabase(os.path.join(data_dir, self.programs_dir, filename), "apt"))
86+ for fn in os.listdir(os.path.join(data_dir, self.snaps_dir)):
87+ self.programs.append(ProgramDatabase(os.path.join(data_dir, self.snaps_dir, fn), "snap"))
88 try:
89 self.user_can_sudo = grp.getgrnam("sudo")[2] in posix.getgroups() or grp.getgrnam("admin")[2] in posix.getgroups()
90 except KeyError:
91@@ -141,7 +138,7 @@
92 return
93 possible_alternatives = []
94 for w in similar_words(word):
95- packages = self.getPackages(w)
96+ packages = self.get_packages(w)
97 for (package, comp) in packages:
98 possible_alternatives.append((w, package, comp))
99 if len(possible_alternatives) > max_alt:
100@@ -151,9 +148,11 @@
101 for (w, p, c) in possible_alternatives:
102 print(_(" Command '%s' from package '%s' (%s)") % (w, p, c), file=sys.stderr)
103
104- def getPackages(self, command):
105+ def get_packages(self, command, pkgmanager):
106 result = set()
107 for db in self.programs:
108+ if db.pkgmanager != pkgmanager:
109+ continue
110 result.update([(pkg, db.component) for pkg in db.lookup(command)])
111 return list(result)
112
113@@ -263,35 +262,57 @@
114
115 if command in self.getBlacklist():
116 return False
117- packages = self.getPackages(command)
118- if len(packages) == 0:
119+
120+ packages_apt = self.get_packages(command, "apt")
121+ packages_snap = self.get_packages(command, "snap")
122+ if len(packages_apt) == 0 and len(packages_snap) == 0:
123 self.print_spelling_suggestion(command)
124- elif len(packages) == 1:
125+ return
126+ self.advice_on_apt(command, packages_apt)
127+ # advice on snaps as well
128+ if len(packages_snap) > 0:
129+ pkgmanager="snap"
130+ print()
131+ print(_("It is also available as a snap package:"))
132+ for package in packages_snap:
133+ print(" * %s" % package[0])
134+ if posix.geteuid() == 0:
135+ print(_("Try: %s <selected package>") % "%s install" % pkgmanager, file=sys.stderr)
136+ elif self.user_can_sudo:
137+ print(_("Try: %s <selected package>") % "sudo %s install" % pkgmanager, file=sys.stderr)
138+ else:
139+ print(_("Ask your administrator to install one of them"), file=sys.stderr)
140+ return len(packages_apt) + len(packages_snap)
141+
142+ def advice_on_apt(self, command, packages):
143+ pkgmanager = "apt"
144+ if len(packages) == 1:
145 print(_("The program '%s' is currently not installed. ") % command, end="", file=sys.stderr)
146 if posix.geteuid() == 0:
147 print(_("You can install it by typing:"), file=sys.stderr)
148- print("apt install %s" % packages[0][0], file=sys.stderr)
149+ print("%s install %s" % (packages[0][0], pkgmanager), file=sys.stderr)
150 self.install_prompt(packages[0][0])
151 elif self.user_can_sudo:
152 print(_("You can install it by typing:"), file=sys.stderr)
153- print("sudo apt install %s" % packages[0][0], file=sys.stderr)
154+ print("sudo %s install %s" % (pkgmanager, packages[0][0]), file=sys.stderr)
155 self.install_prompt(packages[0][0])
156 else:
157- print(_("To run '%(command)s' please ask your administrator to install the package '%(package)s'") % {'command': command, 'package': packages[0][0]}, file=sys.stderr)
158+ print(_("To run '%(command)s' please ask your administrator to install the %(pkgmanager)s package '%(package)s'") % {'command': command, 'package': packages[0][0], 'pkgmanager': pkgmanager}, file=sys.stderr)
159 if not packages[0][1] in self.sources_list:
160 print(_("You will have to enable the component called '%s'") % packages[0][1], file=sys.stderr)
161 elif len(packages) > 1:
162 packages.sort(key=cmp_to_key(self.sortByComponent))
163 print(_("The program '%s' can be found in the following packages:") % command, file=sys.stderr)
164+ # FIXME: deal with mixed apt/snap results
165 for package in packages:
166 if package[1] in self.sources_list:
167 print(" * %s" % package[0], file=sys.stderr)
168 else:
169 print(" * %s" % package[0] + " (" + _("You will have to enable component called '%s'") % package[1] + ")", file=sys.stderr)
170 if posix.geteuid() == 0:
171- print(_("Try: %s <selected package>") % "apt install", file=sys.stderr)
172+ print(_("Try: %s <selected package>") % "%s install" % pkgmanager, file=sys.stderr)
173 elif self.user_can_sudo:
174- print(_("Try: %s <selected package>") % "sudo apt install", file=sys.stderr)
175+ print(_("Try: %s <selected package>") % "sudo %s install" % pkgmanager, file=sys.stderr)
176 else:
177 print(_("Ask your administrator to install one of them"), file=sys.stderr)
178 return len(packages) > 0
179
180=== modified file 'command-not-found'
181--- command-not-found 2016-06-17 14:51:57 +0000
182+++ command-not-found 2017-12-21 11:57:34 +0000
183@@ -25,7 +25,7 @@
184 from optparse import OptionParser
185
186 from CommandNotFound.util import crash_guard
187- from CommandNotFound import CommandNotFound
188+ from CommandNotFound.CommandNotFound import CommandNotFound
189 except KeyboardInterrupt:
190 import sys
191 sys.exit(127)
192
193=== modified file 'debian/dirs'
194--- debian/dirs 2017-08-30 13:32:56 +0000
195+++ debian/dirs 2017-12-21 11:57:34 +0000
196@@ -1,1 +1,3 @@
197 usr/bin
198+usr/share/command-not-found/programs.d
199+usr/share/command-not-found/snaps.d

Subscribers

People subscribed via source and target branches

to all changes: