Files
flatpak/app/flatpak-complete.c
Matthew Leeds 5fe2a0aabe uninstall: Add support for fuzzy matching
This adds support for fuzzy matching ref names (AKA "typo helper") to
the uninstall command to mirror what the install command has. In short,
this means you can do "flatpak uninstall gedit" instead of "flatpak
uninstall org.gnome.gedit". Flatpak will prompt you to choose between
similarly named installed refs, and will only make the choice for you if
--assumeyes was used and there's only one match.

Note that this commit does have the side effect that if there are
multiple matching refs with the same ID (e.g. with different branches or
in different installations) you are prompted to choose between them.
Previously you were shown an error message.

Closes: #2330
Approved by: matthiasclasen
2018-11-17 13:00:33 +00:00

535 lines
16 KiB
C

/*
* Copyright © 2018 Red Hat, Inc
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Alexander Larsson <alexl@redhat.com>
*/
#include "config.h"
#include "flatpak-complete.h"
#include "flatpak-utils-private.h"
/* Uncomment to get debug traces in /tmp/flatpak-completion-debug.txt (nice
* to not have it interfere with stdout/stderr)
*/
#if 0
void
flatpak_completion_debug (const gchar *format, ...)
{
va_list var_args;
gchar *s;
static FILE *f = NULL;
va_start (var_args, format);
s = g_strdup_vprintf (format, var_args);
if (f == NULL)
f = fopen ("/tmp/flatpak-completion-debug.txt", "a+");
fprintf (f, "%s\n", s);
fflush (f);
g_free (s);
}
#else
void
flatpak_completion_debug (const gchar *format, ...)
{
}
#endif
static gboolean
is_word_separator (char c)
{
return g_ascii_isspace (c);
}
void
flatpak_complete_file (FlatpakCompletion *completion,
const char *file_type)
{
flatpak_completion_debug ("completing FILE");
g_print ("%s\n", file_type);
}
void
flatpak_complete_dir (FlatpakCompletion *completion)
{
flatpak_completion_debug ("completing DIR");
g_print ("%s\n", "__FLATPAK_DIR");
}
void
flatpak_complete_word (FlatpakCompletion *completion,
char *format, ...)
{
va_list args;
const char *rest;
const char *shell_cur;
const char *shell_cur_end;
g_autofree char *string = NULL;
g_return_if_fail (format != NULL);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
va_start (args, format);
string = g_strdup_vprintf (format, args);
va_end (args);
#pragma GCC diagnostic pop
if (!g_str_has_prefix (string, completion->cur))
return;
shell_cur = completion->shell_cur ? completion->shell_cur : "";
rest = string + strlen (completion->cur);
shell_cur_end = shell_cur + strlen (shell_cur);
while (shell_cur_end > shell_cur &&
rest > string &&
shell_cur_end[-1] == rest[-1] &&
/* I'm not sure exactly what bash is doing here with =, but this seems to work... */
shell_cur_end[-1] != '=')
{
rest--;
shell_cur_end--;
}
flatpak_completion_debug ("completing word: '%s' (%s)", string, rest);
g_print ("%s\n", rest);
}
void
flatpak_complete_ref (FlatpakCompletion *completion,
OstreeRepo *repo)
{
g_autoptr(GHashTable) refs = NULL;
flatpak_completion_debug ("completing REF");
if (ostree_repo_list_refs (repo,
NULL,
&refs, NULL, NULL))
{
GHashTableIter hashiter;
gpointer hashkey, hashvalue;
g_hash_table_iter_init (&hashiter, refs);
while ((g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue)))
{
const char *ref = (const char *) hashkey;
if (!(g_str_has_prefix (ref, "runtime/") ||
g_str_has_prefix (ref, "app/")))
continue;
flatpak_complete_word (completion, "%s", ref);
}
}
}
static int
find_current_element (const char *str)
{
int count = 0;
if (g_str_has_prefix (str, "app/"))
str += strlen ("app/");
else if (g_str_has_prefix (str, "runtime/"))
str += strlen ("runtime/");
while (str != NULL && count <= 3)
{
str = strchr (str, '/');
count++;
if (str != NULL)
str = str + 1;
}
return count;
}
void
flatpak_complete_partial_ref (FlatpakCompletion *completion,
FlatpakKinds kinds,
const char *only_arch,
FlatpakDir *dir,
const char *remote)
{
FlatpakKinds matched_kinds;
const char *pref;
g_autofree char *id = NULL;
g_autofree char *arch = NULL;
g_autofree char *branch = NULL;
g_auto(GStrv) refs = NULL;
int element;
const char *cur_parts[4] = { NULL };
g_autoptr(GError) error = NULL;
int i;
pref = completion->cur;
element = find_current_element (pref);
flatpak_split_partial_ref_arg_novalidate (pref, kinds,
NULL, NULL,
&matched_kinds, &id, &arch, &branch);
cur_parts[1] = id;
cur_parts[2] = arch ? arch : "";
cur_parts[3] = branch ? branch : "";
if (remote)
{
refs = flatpak_dir_find_remote_refs (dir, completion->argv[1],
(element > 1) ? id : NULL,
(element > 3) ? branch : NULL,
NULL, /* default branch */
(element > 2) ? arch : only_arch,
NULL, /* default arch */
matched_kinds,
FIND_MATCHING_REFS_FLAGS_NONE,
NULL, &error);
}
else
{
refs = flatpak_dir_find_installed_refs (dir,
(element > 1) ? id : NULL,
(element > 3) ? branch : NULL,
(element > 2) ? arch : only_arch,
matched_kinds,
FIND_MATCHING_REFS_FLAGS_NONE,
&error);
}
if (refs == NULL)
flatpak_completion_debug ("find refs error: %s", error->message);
for (i = 0; refs != NULL && refs[i] != NULL; i++)
{
int j;
g_autoptr(GString) comp = NULL;
g_auto(GStrv) parts = flatpak_decompose_ref (refs[i], NULL);
if (parts == NULL)
continue;
if (!g_str_has_prefix (parts[element], cur_parts[element]))
continue;
if (flatpak_id_has_subref_suffix (parts[element]))
{
char *last_dot = strrchr (parts[element], '.');
if (last_dot == NULL)
continue; /* Shouldn't really happen */
/* Only complete to subrefs is fully matching real part.
* For example, only match org.foo.Bar.Sources for
* "org.foo.Bar", "org.foo.Bar." or "org.foo.Bar.S", but
* not for "org.foo" or other shorter prefixes.
*/
if (strncmp (parts[element], cur_parts[element], last_dot - parts[element] - 1) != 0)
continue;
}
comp = g_string_new (pref);
g_string_append (comp, parts[element] + strlen (cur_parts[element]));
/* Only complete on the last part if the user explicitly adds a / */
if (element >= 2)
{
for (j = element + 1; j < 4; j++)
{
g_string_append (comp, "/");
g_string_append (comp, parts[j]);
}
}
flatpak_complete_word (completion, "%s", comp->str);
}
}
static gboolean
switch_already_in_line (FlatpakCompletion *completion,
GOptionEntry *entry)
{
guint i = 0;
guint line_part_len = 0;
for (; i < completion->original_argc; ++i)
{
line_part_len = strlen (completion->original_argv[i]);
if (line_part_len > 2 &&
g_strcmp0 (&completion->original_argv[i][2], entry->long_name) == 0)
return TRUE;
if (line_part_len == 2 &&
completion->original_argv[i][1] == entry->short_name)
return TRUE;
}
return FALSE;
}
static gboolean
should_filter_out_option_from_completion (FlatpakCompletion *completion,
GOptionEntry *entry)
{
switch (entry->arg)
{
case G_OPTION_ARG_NONE:
case G_OPTION_ARG_STRING:
case G_OPTION_ARG_INT:
case G_OPTION_ARG_FILENAME:
case G_OPTION_ARG_DOUBLE:
case G_OPTION_ARG_INT64:
return switch_already_in_line (completion, entry);
default:
return FALSE;
}
}
void
flatpak_complete_options (FlatpakCompletion *completion,
GOptionEntry *entries)
{
GOptionEntry *e = entries;
int i;
while (e->long_name != NULL)
{
if (e->arg_description)
{
g_autofree char *prefix = g_strdup_printf ("--%s=", e->long_name);
if (g_str_has_prefix (completion->cur, prefix))
{
if (strcmp (e->arg_description, "ARCH") == 0)
{
const char *arches[] = {"i386", "x86_64", "aarch64", "arm"};
for (i = 0; i < G_N_ELEMENTS (arches); i++)
flatpak_complete_word (completion, "%s%s ", prefix, arches[i]);
}
else if (strcmp (e->arg_description, "SHARE") == 0)
{
for (i = 0; flatpak_context_shares[i] != NULL; i++)
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_shares[i]);
}
else if (strcmp (e->arg_description, "DEVICE") == 0)
{
for (i = 0; flatpak_context_devices[i] != NULL; i++)
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_devices[i]);
}
else if (strcmp (e->arg_description, "FEATURE") == 0)
{
for (i = 0; flatpak_context_features[i] != NULL; i++)
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_features[i]);
}
else if (strcmp (e->arg_description, "SOCKET") == 0)
{
for (i = 0; flatpak_context_sockets[i] != NULL; i++)
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_sockets[i]);
}
else if (strcmp (e->arg_description, "FILE") == 0)
{
flatpak_complete_file (completion, "__FLATPAK_FILE");
}
else
flatpak_complete_word (completion, "%s", prefix);
}
else
flatpak_complete_word (completion, "%s", prefix);
}
else
{
/* If this is just a switch, then don't add it multiple
* times */
if (!should_filter_out_option_from_completion (completion, e))
{
flatpak_complete_word (completion, "--%s ", e->long_name);
}
else
{
flatpak_completion_debug ("switch --%s is already in line %s", e->long_name, completion->line);
}
}
/* We may end up checking switch_already_in_line twice, but this is
* for simplicity's sake - the alternative solution would be to
* continue the loop early and have to increment e. */
if (e->short_name != 0)
{
/* This is a switch, we may not want to add it */
if (!e->arg_description)
{
if (!should_filter_out_option_from_completion (completion, e))
{
flatpak_complete_word (completion, "-%c ", e->short_name);
}
else
{
flatpak_completion_debug ("switch -%c is already in line %s", e->short_name, completion->line);
}
}
else
{
flatpak_complete_word (completion, "-%c ", e->short_name);
}
}
e++;
}
}
void
flatpak_complete_context (FlatpakCompletion *completion)
{
flatpak_complete_options (completion, flatpak_context_get_option_entries ());
}
static gchar *
pick_word_at (const char *s,
int cursor,
int *out_word_begins_at)
{
int begin, end;
if (s[0] == '\0')
{
if (out_word_begins_at != NULL)
*out_word_begins_at = -1;
return NULL;
}
if (is_word_separator (s[cursor]) && ((cursor > 0 && is_word_separator (s[cursor - 1])) || cursor == 0))
{
if (out_word_begins_at != NULL)
*out_word_begins_at = cursor;
return g_strdup ("");
}
while (!is_word_separator (s[cursor - 1]) && cursor > 0)
cursor--;
begin = cursor;
end = begin;
while (!is_word_separator (s[end]) && s[end] != '\0')
end++;
if (out_word_begins_at != NULL)
*out_word_begins_at = begin;
return g_strndup (s + begin, end - begin);
}
static gboolean
parse_completion_line_to_argv (const char *initial_completion_line,
FlatpakCompletion *completion)
{
gboolean parse_result = g_shell_parse_argv (initial_completion_line,
&completion->original_argc,
&completion->original_argv,
NULL);
/* Make a shallow copy of argv, which will be our "working set" */
completion->argc = completion->original_argc;
completion->argv = g_memdup (completion->original_argv,
sizeof (gchar *) * (completion->original_argc + 1));
return parse_result;
}
FlatpakCompletion *
flatpak_completion_new (const char *arg_line,
const char *arg_point,
const char *arg_cur)
{
FlatpakCompletion *completion;
g_autofree char *initial_completion_line = NULL;
int _point;
char *endp;
int cur_begin;
int i;
_point = strtol (arg_point, &endp, 10);
if (endp == arg_point || *endp != '\0')
return NULL;
completion = g_new0 (FlatpakCompletion, 1);
completion->line = g_strdup (arg_line);
completion->shell_cur = g_strdup (arg_cur);
completion->point = _point;
flatpak_completion_debug ("========================================");
flatpak_completion_debug ("completion_point=%d", completion->point);
flatpak_completion_debug ("completion_shell_cur='%s'", completion->shell_cur);
flatpak_completion_debug ("----");
flatpak_completion_debug (" 0123456789012345678901234567890123456789012345678901234567890123456789");
flatpak_completion_debug ("'%s'", completion->line);
flatpak_completion_debug (" %*s^", completion->point, "");
/* compute cur and prev */
completion->prev = NULL;
completion->cur = pick_word_at (completion->line, completion->point, &cur_begin);
if (cur_begin > 0)
{
gint prev_end;
for (prev_end = cur_begin - 1; prev_end >= 0; prev_end--)
{
if (!is_word_separator (completion->line[prev_end]))
{
completion->prev = pick_word_at (completion->line, prev_end, NULL);
break;
}
}
initial_completion_line = g_strndup (completion->line, cur_begin);
}
else
initial_completion_line = g_strdup ("");
flatpak_completion_debug ("'%s'", initial_completion_line);
flatpak_completion_debug ("----");
flatpak_completion_debug (" cur='%s'", completion->cur);
flatpak_completion_debug ("prev='%s'", completion->prev);
if (!parse_completion_line_to_argv (initial_completion_line,
completion))
{
/* it's very possible the command line can't be parsed (for
* example, missing quotes etc) - in that case, we just
* don't autocomplete at all
*/
flatpak_completion_free (completion);
return NULL;
}
flatpak_completion_debug ("completion_argv %i:", completion->original_argc);
for (i = 0; i < completion->original_argc; i++)
flatpak_completion_debug (completion->original_argv[i]);
flatpak_completion_debug ("----");
return completion;
}
void
flatpak_completion_free (FlatpakCompletion *completion)
{
g_free (completion->cur);
g_free (completion->prev);
g_free (completion->line);
g_free (completion->argv);
g_strfreev (completion->original_argv);
g_free (completion);
}