summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhaedrus Leeds <mwleeds@protonmail.com>2022-04-09 20:41:50 -0700
committerPhaedrus Leeds <mwleeds@protonmail.com>2022-07-25 21:19:29 -0500
commit2e0c62d314f182970729b89449887d2b7912cfe7 (patch)
tree21f30a1d09d5af80d75777e08e88aaea43cdfd1f
parent2c704ef21d2dde938504437aa55808705bfe4eb4 (diff)
downloadflatpak-mwleeds/fuzzy-matching-improvements.tar.gz
run: Implement fuzzy ref matching and alias supportmwleeds/fuzzy-matching-improvements
We've had the feature for a long time that allows you to use a substring instead of a full app ID in the install and uninstall commands. The only reason I didn't add it to the run command back then was that I wasn't sure it was ok to add interactive prompts to flatpak-run (it is often invoked by other programs rather than users directly). But in fact we already have a prompt in case there's ambiguity about which branch to use; this commit just repurposes that to also address ambiguity about which ref to use. Long story short, this allows you to do "flatpak run firefox" instead of "flatpak run org.mozilla.firefox". As discussed in #4848, we let the user avoid needing to respond to a prompt every time by letting them save the alias (trust-on-first-use). The aliases can be added, removed, or viewed with the separate alias command. Helps: #1258
-rw-r--r--app/flatpak-builtins-run.c237
-rw-r--r--common/flatpak-dir-private.h3
-rw-r--r--common/flatpak-dir.c76
-rw-r--r--common/flatpak-utils-private.h12
-rw-r--r--common/flatpak-utils.c57
-rw-r--r--doc/flatpak-run.xml20
-rwxr-xr-x[-rw-r--r--]tests/test-run.sh23
7 files changed, 385 insertions, 43 deletions
diff --git a/app/flatpak-builtins-run.c b/app/flatpak-builtins-run.c
index 9f517374..336304d3 100644
--- a/app/flatpak-builtins-run.c
+++ b/app/flatpak-builtins-run.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2014 Red Hat, Inc
+ * Copyright © 2014-2022 Red Hat, Inc and others
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -16,6 +16,7 @@
*
* Authors:
* Alexander Larsson <alexl@redhat.com>
+ * Matthew Leeds <mwleeds@protonmail.com>
*/
#include "config.h"
@@ -61,6 +62,7 @@ static gboolean opt_parent_share_pids;
static int opt_instance_id_fd = -1;
static char *opt_app_path;
static char *opt_usr_path;
+static gboolean opt_yes;
static GOptionEntry options[] = {
{ "arch", 0, 0, G_OPTION_ARG_STRING, &opt_arch, N_("Arch to use"), N_("ARCH") },
@@ -89,6 +91,7 @@ static GOptionEntry options[] = {
{ "instance-id-fd", 0, 0, G_OPTION_ARG_INT, &opt_instance_id_fd, N_("Write the instance ID to the given file descriptor"), NULL },
{ "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, N_("Use PATH instead of the app's /app"), N_("PATH") },
{ "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, N_("Use PATH instead of the runtime's /usr"), N_("PATH") },
+ { "assumeyes", 'y', 0, G_OPTION_ARG_NONE, &opt_yes, N_("Automatically answer yes for all questions"), NULL },
{ NULL }
};
@@ -110,6 +113,16 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError **
g_autoptr(GError) local_error = NULL;
g_autoptr(GPtrArray) dirs = NULL;
FlatpakRunFlags flags = 0;
+ FindMatchingRefsFlags matching_refs_flags;
+ gboolean id_is_alias;
+ const char *on = "";
+ const char *off = "";
+
+ if (flatpak_fancy_output ())
+ {
+ on = FLATPAK_ANSI_BOLD_ON;
+ off = FLATPAK_ANSI_BOLD_OFF;
+ }
context = g_option_context_new (_("APP [ARGUMENT…] - Run an app"));
g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
@@ -157,30 +170,85 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError **
pref = argv[rest_argv_start];
- if (!flatpak_split_partial_ref_arg (pref, FLATPAK_KINDS_APP | FLATPAK_KINDS_RUNTIME,
- opt_arch, opt_branch,
- &kinds, &id, &arch, &branch, error))
- return FALSE;
+ /* If pref doesn't look like an app ID, check if it's an alias (which cannot
+ * contain a period).
+ */
+ id_is_alias = flatpak_is_valid_alias (pref, NULL);
+ if (id_is_alias && (opt_arch || opt_branch))
+ return usage_error (context, _("The --branch and --arch options must be omitted when an alias is used"), error);
- if (branch == NULL || arch == NULL)
+ /* Look for an existing alias */
+ if (id_is_alias)
{
- g_autoptr(FlatpakDecomposed) current_ref = flatpak_find_current_ref (id, NULL, NULL);
- if (current_ref)
+ for (i = 0; i < dirs->len; i++)
{
- if (branch == NULL)
- branch = flatpak_decomposed_dup_branch (current_ref);
- if (arch == NULL)
- arch = flatpak_decomposed_dup_arch (current_ref);
+ FlatpakDir *dir = g_ptr_array_index (dirs, i);
+ app_ref = flatpak_dir_get_alias_target (dir, pref, NULL);
+ if (app_ref != NULL)
+ {
+ kinds = FLATPAK_KINDS_APP;
+ id = g_strdup (pref);
+ /* just for -Wmaybe-uninitialized */
+ matching_refs_flags = FIND_MATCHING_REFS_FLAGS_FUZZY;
+ arch = flatpak_decomposed_dup_arch (app_ref);
+ branch = flatpak_decomposed_dup_branch (app_ref);
+ break;
+ }
+ }
+ }
+
+ if (app_ref == NULL)
+ {
+ if (!flatpak_allow_fuzzy_matching (pref) || !id_is_alias)
+ matching_refs_flags = FIND_MATCHING_REFS_FLAGS_NONE;
+ else
+ matching_refs_flags = FIND_MATCHING_REFS_FLAGS_FUZZY;
+
+ if (matching_refs_flags & FIND_MATCHING_REFS_FLAGS_FUZZY)
+ {
+ flatpak_split_partial_ref_arg_novalidate (pref, FLATPAK_KINDS_APP | FLATPAK_KINDS_RUNTIME,
+ opt_arch, opt_branch,
+ &kinds, &id, &arch, &branch);
+
+ /* We used _novalidate so that the id can be partial, but we can still validate the branch */
+ if (branch != NULL && !flatpak_is_valid_branch (branch, -1, &local_error))
+ return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF,
+ _("Invalid branch %s: %s"), branch, local_error->message);
+ }
+ else if (!flatpak_split_partial_ref_arg (pref, FLATPAK_KINDS_APP | FLATPAK_KINDS_RUNTIME,
+ opt_arch, opt_branch,
+ &kinds, &id, &arch, &branch, error))
+ {
+ return FALSE;
+ }
+
+ if (branch == NULL || arch == NULL)
+ {
+ g_autoptr(FlatpakDecomposed) current_ref = flatpak_find_current_ref (id, NULL, NULL);
+ if (current_ref)
+ {
+ if (branch == NULL)
+ branch = flatpak_decomposed_dup_branch (current_ref);
+ if (arch == NULL)
+ arch = flatpak_decomposed_dup_arch (current_ref);
+ }
}
}
if ((kinds & FLATPAK_KINDS_APP) != 0)
{
- app_ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_APP, id, arch, branch, &local_error);
+ if (app_ref == NULL)
+ app_ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_APP, id, arch, branch, &local_error);
+
+ if (app_ref == NULL &&
+ (matching_refs_flags & FIND_MATCHING_REFS_FLAGS_FUZZY) != 0 &&
+ g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_INVALID_REF))
+ g_clear_error (&local_error);
+
if (app_ref != NULL)
app_deploy = flatpak_find_deploy_for_ref_in (dirs, flatpak_decomposed_get_ref (app_ref), opt_commit, cancellable, &local_error);
- if (app_deploy == NULL &&
+ if (local_error != NULL &&
(!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED) ||
(kinds & FLATPAK_KINDS_RUNTIME) == 0))
{
@@ -194,28 +262,25 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError **
if (app_deploy == NULL)
{
- g_autoptr(FlatpakDeploy) runtime_deploy = NULL;
+ g_autoptr(FlatpakDeploy) deploy = NULL;
g_autoptr(GError) local_error2 = NULL;
g_autoptr(GPtrArray) ref_dir_pairs = NULL;
RefDirPair *chosen_pair = NULL;
- const char *runtime_arch = arch;
-
- /* If arch is not specified, only run the default arch to avoid asking for prompts for non-primary arches,
- still asks for prompts if there are multiple branches though */
- if (runtime_arch == NULL)
- runtime_arch = flatpak_get_arch ();
/* Whereas for apps we want to default to using the "current" one (see
* flatpak-make-current(1)) runtimes don't have a concept of currentness.
- * So prompt if there's ambiguity about which branch to use */
+ * So prompt if there's ambiguity about which branch to use. Also prompt
+ * if the ref given was a partial app id, e.g. "devhelp" instead of
+ * "org.gnome.Devhelp" (see flatpak-alias(1)).
+ */
ref_dir_pairs = g_ptr_array_new_with_free_func ((GDestroyNotify) ref_dir_pair_free);
for (i = 0; i < dirs->len; i++)
{
FlatpakDir *dir = g_ptr_array_index (dirs, i);
g_autoptr(GPtrArray) refs = NULL;
- refs = flatpak_dir_find_installed_refs (dir, id, branch, runtime_arch, FLATPAK_KINDS_RUNTIME,
- FIND_MATCHING_REFS_FLAGS_NONE, error);
+ refs = flatpak_dir_find_installed_refs (dir, id, branch, arch, kinds,
+ matching_refs_flags, error);
if (refs == NULL)
return FALSE;
else if (refs->len == 0)
@@ -223,7 +288,35 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError **
for (int j = 0; j < refs->len; j++)
{
+ g_autoptr(FlatpakDecomposed) current_ref = NULL;
FlatpakDecomposed *ref = g_ptr_array_index (refs, j);
+ g_autofree char *ref_id = flatpak_decomposed_dup_id (ref);
+
+ /* Exclude app refs for non-current branches */
+ if (flatpak_decomposed_is_app (ref) && (branch == NULL || arch == NULL) &&
+ (current_ref = flatpak_dir_current_ref (dir, ref_id, NULL)) != NULL)
+ {
+ const char *current_branch;
+ g_autofree char *current_arch = NULL;
+
+ current_branch = flatpak_decomposed_get_branch (current_ref);
+ current_arch = flatpak_decomposed_dup_arch (current_ref);
+
+ if (branch == NULL &&
+ !flatpak_decomposed_is_branch (ref, current_branch))
+ continue;
+ if (arch == NULL &&
+ !flatpak_decomposed_is_arch (ref, current_arch))
+ continue;
+ }
+
+ /* Avoid prompting for non-primary arches */
+ if (flatpak_decomposed_is_runtime (ref) && arch == NULL)
+ {
+ if (!flatpak_decomposed_is_arch (ref, flatpak_get_arch ()))
+ continue;
+ }
+
RefDirPair *pair = ref_dir_pair_new (ref, dir);
g_ptr_array_add (ref_dir_pairs, pair);
}
@@ -233,43 +326,107 @@ flatpak_builtin_run (int argc, char **argv, GCancellable *cancellable, GError **
{
g_autoptr(GPtrArray) chosen_pairs = NULL;
g_autoptr(GPtrArray) chosen_dir_array = NULL;
+ FlatpakTernaryPromptResponse response = FLATPAK_TERNARY_PROMPT_RESPONSE_NONE;
chosen_pairs = g_ptr_array_new ();
- if (!flatpak_resolve_matching_installed_refs (TRUE, TRUE, ref_dir_pairs, id, chosen_pairs, error))
- return FALSE;
+ /* Aliases are only for apps not runtimes */
+ if (id_is_alias)
+ {
+ gboolean found_app = FALSE;
+ for (i = 0; i < ref_dir_pairs->len; i++)
+ {
+ RefDirPair *pair = g_ptr_array_index (ref_dir_pairs, i);
+ if (flatpak_decomposed_is_app (pair->ref))
+ found_app = TRUE;
+ }
+ if (!found_app)
+ id_is_alias = FALSE;
+ }
+
+ if (ref_dir_pairs->len > 1 || !id_is_alias)
+ {
+ if (!flatpak_resolve_matching_installed_refs (opt_yes, TRUE, ref_dir_pairs, id, chosen_pairs, error))
+ return FALSE;
+ }
+ else if (id_is_alias)
+ {
+ g_assert (ref_dir_pairs->len == 1);
+ RefDirPair *pair = g_ptr_array_index (ref_dir_pairs, 0);
+ g_autofree char *ref_id = flatpak_decomposed_dup_id (pair->ref);
+ const char *dir_name = flatpak_dir_get_name_cached (pair->dir);
+
+ /* Note: here we print the app ID not the full ref since aliases only apply to the current branch */
+ response = flatpak_yes_no_once_prompt (opt_yes, TRUE, /* include 'no' option */
+ _("Run app %s%s%s (%s) and save an alias %s%s%s to skip future prompts?"),
+ on, ref_id, off, dir_name, on, id, off);
+ if (response == FLATPAK_TERNARY_PROMPT_RESPONSE_NO || response == FLATPAK_TERNARY_PROMPT_RESPONSE_NONE)
+ return flatpak_fail (error, _("No ref chosen to resolve matches for %s%s%s"), on, id, off);
+
+ g_ptr_array_add (chosen_pairs, pair);
+ }
g_assert (chosen_pairs->len == 1);
chosen_pair = g_ptr_array_index (chosen_pairs, 0);
+ if (id_is_alias && response == FLATPAK_TERNARY_PROMPT_RESPONSE_NONE &&
+ flatpak_decomposed_is_app (chosen_pair->ref))
+ {
+ g_autofree char *ref_id = flatpak_decomposed_dup_id (chosen_pair->ref);
+ const char *dir_name = flatpak_dir_get_name_cached (chosen_pair->dir);
+
+ /* Note: here we print the app ID not the full ref since aliases only apply to the current branch */
+ response = flatpak_yes_no_once_prompt (opt_yes, FALSE, /* exclude 'no' option */
+ _("Save an alias %s%s%s for app %s%s%s (%s) to skip future prompts or use only once?"),
+ on, id, off, on, ref_id, off, dir_name);
+ if (response == FLATPAK_TERNARY_PROMPT_RESPONSE_NO || response == FLATPAK_TERNARY_PROMPT_RESPONSE_NONE)
+ return flatpak_fail (error, _("No ref chosen to resolve matches for %s%s%s"), on, id, off);
+ }
+
+ if (response == FLATPAK_TERNARY_PROMPT_RESPONSE_YES)
+ {
+ g_assert (id_is_alias);
+ if (!flatpak_dir_make_alias (chosen_pair->dir, chosen_pair->ref, id, error))
+ return FALSE;
+ }
+
/* For runtimes we don't need to pass a FlatpakDeploy object to
* flatpak_run_app(), but get it anyway because we don't want to run
* something that's not deployed */
chosen_dir_array = g_ptr_array_new ();
g_ptr_array_add (chosen_dir_array, chosen_pair->dir);
- runtime_deploy = flatpak_find_deploy_for_ref_in (chosen_dir_array, flatpak_decomposed_get_ref (chosen_pair->ref),
- opt_commit ? opt_commit : opt_runtime_commit,
- cancellable, &local_error2);
+
+ if (flatpak_decomposed_is_runtime (chosen_pair->ref) && opt_commit == NULL)
+ opt_commit = opt_runtime_commit;
+
+ deploy = flatpak_find_deploy_for_ref_in (chosen_dir_array, flatpak_decomposed_get_ref (chosen_pair->ref),
+ opt_commit, cancellable, &local_error2);
+ if (flatpak_decomposed_is_app (chosen_pair->ref))
+ {
+ app_deploy = g_steal_pointer (&deploy);
+ g_clear_pointer (&app_ref, flatpak_decomposed_unref);
+ app_ref = flatpak_decomposed_ref (chosen_pair->ref);
+ }
+ else
+ runtime_ref = flatpak_decomposed_ref (chosen_pair->ref);
}
- if (runtime_deploy == NULL)
+ if (deploy == NULL && app_deploy == NULL)
{
- /* Report old app-kind error, as its more likely right */
+ /* Report old app-kind error, as it's more likely right */
if (local_error != NULL)
g_propagate_error (error, g_steal_pointer (&local_error));
else if (local_error2 != NULL)
g_propagate_error (error, g_steal_pointer (&local_error2));
else
flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED,
- _("runtime/%s/%s/%s not installed"),
+ _("%s/%s/%s not installed"),
id ?: "*unspecified*",
arch ?: "*unspecified*",
branch ?: "*unspecified*");
return FALSE;
}
- runtime_ref = flatpak_decomposed_ref (chosen_pair->ref);
-
/* Clear app-kind error */
g_clear_error (&local_error);
}
@@ -352,7 +509,7 @@ flatpak_complete_run (FlatpakCompletion *completion)
switch (completion->argc)
{
case 0:
- case 1: /* NAME */
+ case 1: /* NAME or ALIAS */
flatpak_complete_options (completion, global_entries);
flatpak_complete_options (completion, user_entries);
flatpak_complete_options (completion, options);
@@ -368,6 +525,11 @@ flatpak_complete_run (FlatpakCompletion *completion)
flatpak_completion_debug ("find local refs error: %s", error->message);
flatpak_complete_ref_id (completion, refs);
+
+ g_autoptr(GHashTable) aliases = NULL; /* alias → app-id */
+ aliases = flatpak_dir_get_aliases (user_dir);
+ GLNX_HASH_TABLE_FOREACH (aliases, const char *, alias)
+ flatpak_complete_word (completion, "%s", alias);
}
system_dirs = flatpak_dir_get_system_list (NULL, &error);
@@ -388,6 +550,11 @@ flatpak_complete_run (FlatpakCompletion *completion)
flatpak_completion_debug ("find local refs error: %s", error->message);
flatpak_complete_ref_id (completion, refs);
+
+ g_autoptr(GHashTable) aliases = NULL; /* alias → app-id */
+ aliases = flatpak_dir_get_aliases (dir);
+ GLNX_HASH_TABLE_FOREACH (aliases, const char *, alias)
+ flatpak_complete_word (completion, "%s", alias);
}
break;
diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h
index f3880b87..b5bbc5d2 100644
--- a/common/flatpak-dir-private.h
+++ b/common/flatpak-dir-private.h
@@ -607,6 +607,9 @@ gboolean flatpak_dir_config_remove_pattern (Fla
const char *pattern,
GError **error);
GHashTable * flatpak_dir_get_aliases (FlatpakDir *self);
+FlatpakDecomposed * flatpak_dir_get_alias_target (FlatpakDir *self,
+ const char *alias,
+ GError **error);
gboolean flatpak_dir_make_alias (FlatpakDir *self,
FlatpakDecomposed *current_ref,
const char *alias,
diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c
index 90ea0d6b..a8a765a2 100644
--- a/common/flatpak-dir.c
+++ b/common/flatpak-dir.c
@@ -4536,19 +4536,83 @@ flatpak_dir_get_aliases (FlatpakDir *self)
return g_steal_pointer (&aliases);
}
+FlatpakDecomposed *
+flatpak_dir_get_alias_target (FlatpakDir *self,
+ const char *alias,
+ GError **error)
+{
+ g_autoptr(GFile) aliases_dir = NULL;
+ g_autoptr(GFile) alias_file = NULL;
+ g_autoptr(GFileInfo) info = NULL;
+ g_autoptr(GError) local_error = NULL;
+ const char *target;
+ g_autoptr(GFile) target_file = NULL;
+ g_autofree char *target_basename = NULL;
+ g_autoptr(FlatpakDecomposed) current = NULL;
+
+ if (!flatpak_is_valid_alias (alias, error))
+ return NULL;
+
+ if (!flatpak_dir_maybe_ensure_repo (self, NULL, error))
+ return NULL;
+
+ aliases_dir = flatpak_dir_get_aliases_dir (self);
+ alias_file = g_file_get_child (aliases_dir, alias);
+
+ info = g_file_query_info (alias_file,
+ G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ NULL,
+ &local_error);
+ if (info == NULL)
+ {
+ if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ flatpak_fail_error (error, FLATPAK_ERROR_ALIAS_NOT_FOUND,
+ _("Error reading alias '%s' in '%s': it does not exist"),
+ alias, flatpak_file_get_path_cached (aliases_dir));
+ else
+ g_propagate_error (error, g_steal_pointer (&local_error));
+
+ return NULL;
+ }
+
+ target = g_file_info_get_symlink_target (info);
+ if (target == NULL)
+ {
+ flatpak_fail_error (error, FLATPAK_ERROR_ALIAS_NOT_FOUND,
+ _("Error reading alias '%s': could not determine symlink target"),
+ alias);
+ return NULL;
+ }
+
+ target_file = g_file_new_for_path (target);
+ target_basename = g_file_get_basename (target_file);
+ if (target_basename == NULL || !flatpak_is_valid_name (target_basename, -1, NULL))
+ {
+ flatpak_fail_error (error, FLATPAK_ERROR_ALIAS_NOT_FOUND,
+ _("Error reading alias '%s': symlink does not point to app ID"),
+ alias);
+ return NULL;
+ }
+
+ current = flatpak_dir_current_ref (self, target_basename, NULL);
+ if (current == NULL)
+ {
+ flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED,
+ _("Error reading alias '%s': app ID doesn't have current ref"),
+ alias);
+ return NULL;
+ }
+ return g_steal_pointer (&current);
+}
+
gboolean
flatpak_dir_remove_alias (FlatpakDir *self,
const char *alias,
GError **error)
{
- g_autoptr(GFile) exports = NULL;
- g_autoptr(GFile) bindir = NULL;
- g_autoptr(GFile) runner = NULL;
g_autoptr(GFile) aliases = NULL;
g_autoptr(GFile) alias_file = NULL;
- g_autofree char *runner_relpath = NULL;
- g_autofree char *symlink_target = NULL;
- g_autofree char *app_id = NULL;
g_autoptr(GError) local_error = NULL;
if (!flatpak_is_valid_alias (alias, error))
diff --git a/common/flatpak-utils-private.h b/common/flatpak-utils-private.h
index 8333ddfa..2b8b0f65 100644
--- a/common/flatpak-utils-private.h
+++ b/common/flatpak-utils-private.h
@@ -853,6 +853,18 @@ gboolean flatpak_yes_no_prompt (gboolean default_yes,
const char *prompt,
...) G_GNUC_PRINTF (2, 3);
+typedef enum {
+ FLATPAK_TERNARY_PROMPT_RESPONSE_NONE = 0,
+ FLATPAK_TERNARY_PROMPT_RESPONSE_YES = 1 << 0,
+ FLATPAK_TERNARY_PROMPT_RESPONSE_NO = 1 << 1,
+ FLATPAK_TERNARY_PROMPT_RESPONSE_ONCE = 1 << 2,
+} FlatpakTernaryPromptResponse;
+
+FlatpakTernaryPromptResponse flatpak_yes_no_once_prompt (gboolean assume_yes,
+ gboolean include_no,
+ const char *prompt,
+ ...) G_GNUC_PRINTF (3, 4);
+
long flatpak_number_prompt (gboolean default_yes,
int min,
int max,
diff --git a/common/flatpak-utils.c b/common/flatpak-utils.c
index 2039d0d6..62741662 100644
--- a/common/flatpak-utils.c
+++ b/common/flatpak-utils.c
@@ -7540,6 +7540,63 @@ flatpak_yes_no_prompt (gboolean default_yes, const char *prompt, ...)
}
}
+FlatpakTernaryPromptResponse
+flatpak_yes_no_once_prompt (gboolean assume_yes,
+ gboolean include_no,
+ const char *prompt, ...)
+{
+ char buf[512];
+ va_list var_args;
+ g_autofree char *s = NULL;
+
+ /* For --assumeyes we return once since yes is more security sensitive,
+ * may cause a polkit prompt, and ought to require user interaction.
+ */
+ if (assume_yes)
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_ONCE;
+
+ va_start (var_args, prompt);
+ s = g_strdup_vprintf (prompt, var_args);
+ va_end (var_args);
+
+ /* Below we return FLATPAK_TERNARY_PROMPT_RESPONSE_NO on the error code paths
+ * which is consistent with flatpak_yes_no_prompt()
+ */
+ while (TRUE)
+ {
+ if (include_no)
+ g_print ("%s %s: ", s, "[y/n/once]");
+ else
+ g_print ("%s %s: ", s, "[y/once]");
+
+ if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
+ {
+ g_print ("n\n");
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_NO;
+ }
+
+ if (fgets (buf, sizeof (buf), stdin) == NULL)
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_NO;
+
+ g_strstrip (buf);
+
+ if (g_ascii_strcasecmp (buf, "y") == 0 ||
+ g_ascii_strcasecmp (buf, "yes") == 0)
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_YES;
+
+ if (include_no &&
+ (g_ascii_strcasecmp (buf, "n") == 0 ||
+ g_ascii_strcasecmp (buf, "no") == 0))
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_NO;
+
+ if (g_ascii_strcasecmp (buf, "o") == 0 ||
+ g_ascii_strcasecmp (buf, "once") == 0)
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_ONCE;
+ }
+
+ return FLATPAK_TERNARY_PROMPT_RESPONSE_NO;
+}
+
static gboolean
is_number (const char *s)
{
diff --git a/doc/flatpak-run.xml b/doc/flatpak-run.xml
index f8d9e5ee..8aae29e7 100644
--- a/doc/flatpak-run.xml
+++ b/doc/flatpak-run.xml
@@ -56,6 +56,15 @@
<option>--arch</option>.
</para>
<para>
+ If <arg choice="plain">REF</arg> does not exactly match an installed app or runtime
+ but it's similar (e.g. "firefox" is similar to "org.mozilla.firefox") you will be
+ prompted to confirm, and either use the shorter name only once or set up a persistent
+ alias to avoid confirmation prompts in the future (see
+ <citerefentry><refentrytitle>flatpak-alias</refentrytitle><manvolnum>1</manvolnum></citerefentry>).
+ This fuzzy matching behavior is disabled when <arg choice="plain">REF</arg> contains
+ any slashes or periods.
+ </para>
+ <para>
By default, Flatpak will look for the application or runtime in the per-user
installation first, then in all system installations. This can be overridden
with the <option>--user</option>, <option>--system</option> and
@@ -207,6 +216,14 @@
</varlistentry>
<varlistentry>
+ <term><option>-y</option></term>
+ <term><option>--assumeyes</option></term>
+ <listitem><para>
+ Automatically answer yes to all questions (or "once" when that's an option). This is useful for automation.
+ </para></listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><option>--arch=ARCH</option></term>
<listitem><para>
@@ -737,6 +754,9 @@ key=v1;v2;
<command>$ flatpak run org.gnome.gedit</command>
</para>
<para>
+ <command>$ flatpak run gedit</command>
+ </para>
+ <para>
<command>$ flatpak run --devel --command=bash org.gnome.Builder</command>
</para>
<para>
diff --git a/tests/test-run.sh b/tests/test-run.sh
index 7138bc64..61be82f0 100644..100755
--- a/tests/test-run.sh
+++ b/tests/test-run.sh
@@ -24,7 +24,7 @@ set -euo pipefail
skip_without_bwrap
skip_revokefs_without_fuse
-echo "1..20"
+echo "1..21"
# Use stable rather than master as the branch so we can test that the run
# command automatically finds the branch correctly
@@ -76,6 +76,25 @@ assert_file_has_content hello_out '^Hello world, from a sandbox$'
ok "hello"
+# Note: this fuzzy matching wouldn't normally work without user interaction but
+# we have FLATPAK_FORCE_ALLOW_FUZZY_MATCHING=1 in the env and specify -y
+run -y hello &> hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+
+# It should NOT work without -y if no alias exists
+if run hello 2> run-error-log >&2; then
+ assert_not_reached "Unexpectedly able to run non-existent alias without -y"
+fi
+assert_file_has_content run-error-log "error: No ref chosen to resolve matches for hello"
+
+# It should work without -y if an alias exists
+${FLATPAK} ${U} alias hello org.test.Hello
+run hello &> hello_out
+assert_file_has_content hello_out '^Hello world, from a sandbox$'
+${FLATPAK} ${U} alias --remove hello
+
+ok "hello w/ fuzzy matching"
+
# XDG_RUNTIME_DIR is set to <temp directory>/runtime by libtest.sh,
# so we always have the necessary setup to reproduce #4372
assert_not_streq "$XDG_RUNTIME_DIR" "/run/user/$(id -u)"
@@ -172,7 +191,7 @@ assert_file_has_content run-error-log "error: runtime/org\.test\.Platform/$ARCH/
if run runtime/org.test.Nonexistent 2> run-error-log >&2; then
assert_not_reached "Unexpectedly able to run non-existent runtime"
fi
-assert_file_has_content run-error-log "error: runtime/org\.test\.Nonexistent/\*unspecified\*/\*unspecified\* not installed"
+assert_file_has_content run-error-log "error: org\.test\.Nonexistent/\*unspecified\*/\*unspecified\* not installed"
ok "error handling for invalid refs"