368 lines
15 KiB
Python
368 lines
15 KiB
Python
# (c) Zygmunt Krynicki 2005, 2006, 2007, 2008
|
|
# Licensed under GPL, see COPYING for the whole text
|
|
|
|
from __future__ import (
|
|
print_function,
|
|
absolute_import,
|
|
)
|
|
|
|
import gettext
|
|
import grp
|
|
import json
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import posix
|
|
import sys
|
|
import subprocess
|
|
|
|
from CommandNotFound.db.db import SqliteDatabase
|
|
|
|
if sys.version >= "3":
|
|
_gettext_method = "gettext"
|
|
else:
|
|
_gettext_method = "ugettext"
|
|
_ = getattr(gettext.translation("command-not-found", fallback=True), _gettext_method)
|
|
|
|
|
|
def similar_words(word):
|
|
"""
|
|
return a set with spelling1 distance alternative spellings
|
|
|
|
based on http://norvig.com/spell-correct.html
|
|
"""
|
|
alphabet = 'abcdefghijklmnopqrstuvwxyz-_0123456789'
|
|
s = [(word[:i], word[i:]) for i in range(len(word) + 1)]
|
|
deletes = [a + b[1:] for a, b in s if b]
|
|
transposes = [a + b[1] + b[0] + b[2:] for a, b in s if len(b) > 1]
|
|
replaces = [a + c + b[1:] for a, b in s for c in alphabet if b]
|
|
inserts = [a + c + b for a, b in s for c in alphabet]
|
|
return set(deletes + transposes + replaces + inserts)
|
|
|
|
|
|
def user_can_sudo():
|
|
try:
|
|
groups = posix.getgroups()
|
|
return (grp.getgrnam("sudo")[2] in groups or
|
|
grp.getgrnam("admin")[2] in groups)
|
|
except KeyError:
|
|
return False
|
|
|
|
|
|
# the new style DB - if that exists we skip the legacy DB
|
|
dbpath = "/var/lib/command-not-found/commands.db"
|
|
|
|
|
|
class CommandNotFound(object):
|
|
|
|
programs_dir = "programs.d"
|
|
max_len = 256
|
|
|
|
prefixes = (
|
|
"/snap/bin",
|
|
"/bin",
|
|
"/usr/bin",
|
|
"/usr/local/bin",
|
|
"/sbin",
|
|
"/usr/sbin",
|
|
"/usr/local/sbin",
|
|
"/usr/games")
|
|
|
|
snap_cmd = "/usr/bin/snap"
|
|
|
|
output_fd = sys.stderr
|
|
|
|
def __init__(self, data_dir="/usr/share/command-not-found"):
|
|
self.sources_list = self._getSourcesList()
|
|
# a new style DB means we can skip loading the old legacy static DB
|
|
if os.path.exists(dbpath) and os.access(dbpath, os.R_OK):
|
|
self.db = SqliteDatabase(dbpath)
|
|
else:
|
|
raise FileNotFoundError("Cannot find database")
|
|
self.user_can_sudo = user_can_sudo()
|
|
self.euid = posix.geteuid()
|
|
|
|
def spelling_suggestions(self, word, min_len=3):
|
|
""" try to correct the spelling """
|
|
possible_alternatives = []
|
|
if not (min_len <= len(word) <= self.max_len):
|
|
return possible_alternatives
|
|
for w in similar_words(word):
|
|
packages = self.get_packages(w)
|
|
for (package, ver, comp) in packages:
|
|
possible_alternatives.append((w, package, comp, ver))
|
|
return possible_alternatives
|
|
|
|
def get_packages(self, command):
|
|
return self.db.lookup(command)
|
|
|
|
def get_snaps(self, command):
|
|
exact_result = []
|
|
mispell_result = []
|
|
if not os.path.exists(self.snap_cmd):
|
|
logging.debug("%s not exists" % self.snap_cmd)
|
|
return [], []
|
|
try:
|
|
with open(os.devnull) as devnull:
|
|
output = subprocess.check_output(
|
|
[self.snap_cmd, "advise-snap", "--format=json",
|
|
"--command", command],
|
|
stderr=devnull,
|
|
universal_newlines=True)
|
|
except subprocess.CalledProcessError as e:
|
|
logging.debug("calling snap advice-snap returned an error: %s" % e)
|
|
return [], []
|
|
logging.debug("got %s from snap advise-snap" % output)
|
|
try:
|
|
snaps = json.loads(output)
|
|
except json.JSONDecodeError as e:
|
|
logging.debug("cannot decoding json: %s" % e)
|
|
return [], []
|
|
for snap in snaps:
|
|
if snap["Command"] == command:
|
|
exact_result.append((snap["Snap"], snap["Command"], snap.get("Version")))
|
|
else:
|
|
mispell_result.append((snap["Command"], snap["Snap"], snap.get("Version")))
|
|
return exact_result, mispell_result
|
|
|
|
def getBlacklist(self):
|
|
try:
|
|
with open(os.sep.join((os.getenv("HOME", "/root"), ".command-not-found.blacklist"))) as blacklist:
|
|
return [line.strip() for line in blacklist if line.strip() != ""]
|
|
except IOError:
|
|
return []
|
|
|
|
def _getSourcesList(self):
|
|
try:
|
|
import apt_pkg
|
|
from aptsources.sourceslist import SourcesList
|
|
apt_pkg.init()
|
|
except (SystemError, ImportError):
|
|
return []
|
|
sources_list = set([])
|
|
# The matcher parses info files from
|
|
# /usr/share/python-apt/templates/
|
|
# But we don't use the calculated data, skip it
|
|
try:
|
|
sources = SourcesList(withMatcher=False, deb822=True)
|
|
except TypeError:
|
|
sources = SourcesList(withMatcher=False)
|
|
for source in sources:
|
|
if not source.disabled and not source.invalid:
|
|
for component in source.comps:
|
|
sources_list.add(component)
|
|
return sources_list
|
|
|
|
def install_prompt(self, package_name):
|
|
if not "COMMAND_NOT_FOUND_INSTALL_PROMPT" in os.environ:
|
|
return
|
|
if package_name:
|
|
prompt = _("Do you want to install it? (N/y)")
|
|
if sys.version >= '3':
|
|
answer = input(prompt)
|
|
raw_input = lambda x: x # pyflakes
|
|
else:
|
|
answer = raw_input(prompt)
|
|
if sys.stdin.encoding and isinstance(answer, str):
|
|
# Decode the answer so that we get an unicode value
|
|
answer = answer.decode(sys.stdin.encoding)
|
|
if answer.lower() == _("y"):
|
|
if self.euid == 0:
|
|
command_prefix = ""
|
|
else:
|
|
command_prefix = "sudo "
|
|
install_command = "%sapt install %s" % (command_prefix, package_name)
|
|
print("%s" % install_command, file=sys.stdout)
|
|
subprocess.call(install_command.split(), shell=False)
|
|
|
|
def print_spelling_suggestions(self, word, mispell_packages, mispell_snaps, max_alt=15):
|
|
""" print spelling suggestions for packages and snaps """
|
|
if len(mispell_packages)+len(mispell_snaps) > max_alt:
|
|
print(_("Command '%s' not found, but there are %s similar ones.") % (word, len(mispell_packages)), file=self.output_fd)
|
|
self.output_fd.flush()
|
|
return
|
|
elif len(mispell_packages)+len(mispell_snaps) > 0:
|
|
print(_("Command '%s' not found, did you mean:") % word, file=self.output_fd)
|
|
for (command, snap, ver) in mispell_snaps:
|
|
if ver:
|
|
ver = " (%s)" % ver
|
|
else:
|
|
ver = ""
|
|
print(_(" command '%s' from snap %s%s") % (command, snap, ver), file=self.output_fd)
|
|
for (command, package, comp, ver) in mispell_packages:
|
|
if ver:
|
|
ver = " (%s)" % ver
|
|
else:
|
|
ver = ""
|
|
print(_(" command '%s' from deb %s%s") % (command, package, ver), file=self.output_fd)
|
|
if len(mispell_snaps) > 0:
|
|
print(_("See 'snap info <snapname>' for additional versions."), file=self.output_fd)
|
|
elif len(mispell_packages) > 0:
|
|
if self.user_can_sudo:
|
|
print(_("Try: %s <deb name>") % "sudo apt install", file=self.output_fd)
|
|
else:
|
|
print(_("Try: %s <deb name>") % "apt install", file=self.output_fd)
|
|
self.output_fd.flush()
|
|
|
|
def _print_exact_header(self, command):
|
|
print(_("Command '%(command)s' not found, but can be installed with:") % {
|
|
'command': command}, file=self.output_fd)
|
|
|
|
def advice_single_snap_package(self, command, packages, snaps):
|
|
self._print_exact_header(command)
|
|
snap = snaps[0]
|
|
if self.euid == 0:
|
|
print("snap install %s" % snap[0], file=self.output_fd)
|
|
elif self.user_can_sudo:
|
|
print("sudo snap install %s" % snap[0], file=self.output_fd)
|
|
else:
|
|
print("snap install %s" % snap[0], file=self.output_fd)
|
|
print(_("Please ask your administrator."))
|
|
self.output_fd.flush()
|
|
|
|
def advice_single_deb_package(self, command, packages, snaps):
|
|
self._print_exact_header(command)
|
|
if self.euid == 0:
|
|
print("apt install %s" % packages[0][0], file=self.output_fd)
|
|
self.install_prompt(packages[0][0])
|
|
elif self.user_can_sudo:
|
|
print("sudo apt install %s" % packages[0][0], file=self.output_fd)
|
|
self.install_prompt(packages[0][0])
|
|
else:
|
|
print("apt install %s" % packages[0][0], file=self.output_fd)
|
|
print(_("Please ask your administrator."))
|
|
if not packages[0][2] in self.sources_list:
|
|
print(_("You will have to enable the component called '%s'") % packages[0][2], file=self.output_fd)
|
|
self.output_fd.flush()
|
|
|
|
def sudo(self):
|
|
if self.euid != 0 and self.user_can_sudo:
|
|
return "sudo "
|
|
return ""
|
|
|
|
def advice_multi_deb_package(self, command, packages, snaps):
|
|
self._print_exact_header(command)
|
|
pad = max([len(s[0]) for s in snaps+packages])
|
|
for i, package in enumerate(packages):
|
|
ver = ""
|
|
if package[1]:
|
|
if i == 0 and len(package) > 1:
|
|
ver = " # version %s, or" % (package[1])
|
|
else:
|
|
ver = " # version %s" % (package[1])
|
|
if package[2] in self.sources_list:
|
|
print("%sapt install %-*s%s" % (self.sudo(), pad, package[0], ver), file=self.output_fd)
|
|
else:
|
|
print("%sapt install %-*s%s" % (self.sudo(), pad, package[0], ver) + " (" + _("You will have to enable component called '%s'") % package[2] + ")", file=self.output_fd)
|
|
if self.euid != 0 and not self.user_can_sudo:
|
|
print(_("Ask your administrator to install one of them."), file=self.output_fd)
|
|
self.output_fd.flush()
|
|
|
|
def advice_multi_snap_packages(self, command, packages, snaps):
|
|
self._print_exact_header(command)
|
|
pad = max([len(s[0]) for s in snaps+packages])
|
|
for i, snap in enumerate(snaps):
|
|
ver = ""
|
|
if snap[2]:
|
|
if i == 0 and len(snaps) > 0:
|
|
ver = " # version %s, or" % snap[2]
|
|
else:
|
|
ver = " # version %s" % snap[2]
|
|
print("%ssnap install %-*s%s" % (self.sudo(), pad, snap[0], ver), file=self.output_fd)
|
|
print(_("See 'snap info <snapname>' for additional versions."), file=self.output_fd)
|
|
self.output_fd.flush()
|
|
|
|
def advice_multi_mixed_packages(self, command, packages, snaps):
|
|
self._print_exact_header(command)
|
|
pad = max([len(s[0]) for s in snaps+packages])
|
|
for i, snap in enumerate(snaps):
|
|
ver=""
|
|
if snap[2]:
|
|
if i == 0:
|
|
ver = " # version %s, or" % snap[2]
|
|
else:
|
|
ver = " # version %s" % snap[2]
|
|
print("%ssnap install %-*s%s" % (self.sudo(), pad, snap[0], ver), file=self.output_fd)
|
|
for package in packages:
|
|
ver=""
|
|
if package[1]:
|
|
ver = " # version %s" % package[1]
|
|
print("%sapt install %-*s%s" % (self.sudo(), pad, package[0], ver), file=self.output_fd)
|
|
if len(snaps) == 1:
|
|
print(_("See 'snap info %s' for additional versions.") % snaps[0][0], file=self.output_fd)
|
|
else:
|
|
print(_("See 'snap info <snapname>' for additional versions."), file=self.output_fd)
|
|
self.output_fd.flush()
|
|
|
|
def advise(self, command, ignore_installed=False):
|
|
" give advice where to find the given command to stderr "
|
|
def _in_prefix(prefix, command):
|
|
" helper that returns if a command is found in the given prefix "
|
|
return (os.path.exists(os.path.join(prefix, command))
|
|
and not os.path.isdir(os.path.join(prefix, command)))
|
|
|
|
if len(command) > self.max_len:
|
|
return False
|
|
|
|
if command.startswith("/"):
|
|
if os.path.exists(command):
|
|
prefixes = [os.path.dirname(command)]
|
|
else:
|
|
prefixes = []
|
|
else:
|
|
prefixes = [prefix for prefix in self.prefixes if _in_prefix(prefix, command)]
|
|
|
|
# check if we have it in a common prefix that may not be in the PATH
|
|
if prefixes and not ignore_installed:
|
|
if len(prefixes) == 1:
|
|
print(_("Command '%(command)s' is available in '%(place)s'") % {"command": command, "place": os.path.join(prefixes[0], command)}, file=self.output_fd)
|
|
else:
|
|
print(_("Command '%(command)s' is available in the following places") % {"command": command}, file=self.output_fd)
|
|
for prefix in prefixes:
|
|
print(" * %s" % os.path.join(prefix, command), file=self.output_fd)
|
|
missing = list(set(prefixes) - set(os.getenv("PATH", "").split(":")))
|
|
if len(missing) > 0:
|
|
print(_("The command could not be located because '%s' is not included in the PATH environment variable.") % ":".join(missing), file=self.output_fd)
|
|
if "sbin" in ":".join(missing):
|
|
print(_("This is most likely caused by the lack of administrative privileges associated with your user account."), file=self.output_fd)
|
|
return False
|
|
|
|
# do not give advice if we are in a situation where apt
|
|
# or aptitude are not available (LP: #394843)
|
|
if not (os.path.exists("/usr/bin/apt") or
|
|
os.path.exists("/usr/bin/aptitude")):
|
|
return False
|
|
|
|
if command in self.getBlacklist():
|
|
return False
|
|
|
|
# python is special, on 20.04 and newer we should encourage
|
|
# people to use python3 only, and command-not-found depends on
|
|
# python3, so must be available
|
|
if command == "python":
|
|
print(_("Command '%s' not found, did you mean:") % command, file=self.output_fd)
|
|
print(_(" command '%s' from deb %s%s") % ("python3", "python3", ""), file=self.output_fd)
|
|
print(_(" command '%s' from deb %s%s") % ("python", "python-is-python3", ""), file=self.output_fd)
|
|
return True
|
|
|
|
packages = self.get_packages(command)
|
|
snaps, mispell_snaps = self.get_snaps(command)
|
|
logging.debug("got debs: %s snaps: %s" % (packages, snaps))
|
|
if len(packages) == 0 and len(snaps) == 0:
|
|
mispell_packages = self.spelling_suggestions(command)
|
|
if len(mispell_packages) > 0 or len(mispell_snaps) > 0:
|
|
self.print_spelling_suggestions(command, mispell_packages, mispell_snaps)
|
|
elif len(packages) == 0 and len(snaps) == 1:
|
|
self.advice_single_snap_package(command, packages, snaps)
|
|
elif len(snaps) > 0 and len(packages) == 0:
|
|
self.advice_multi_snap_packages(command, packages, snaps)
|
|
elif len(packages) == 1 and len(snaps) == 0:
|
|
self.advice_single_deb_package(command, packages, snaps)
|
|
elif len(packages) > 1 and len(snaps) == 0:
|
|
self.advice_multi_deb_package(command, packages, snaps)
|
|
elif len(packages) > 0 and len(snaps) > 0:
|
|
self.advice_multi_mixed_packages(command, packages, snaps)
|
|
|
|
return (len(packages) > 0 or len(snaps) > 0 or
|
|
len(mispell_snaps) > 0 or len(mispell_packages) > 0)
|