| /* completion.c - complete a command, a disk, a partition or a file */ |
| /* |
| * GRUB -- GRand Unified Bootloader |
| * Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2007,2008,2009 Free Software Foundation, Inc. |
| * |
| * GRUB is free software: you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation, either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * GRUB 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 General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with GRUB. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| #include <grub/normal.h> |
| #include <grub/misc.h> |
| #include <grub/err.h> |
| #include <grub/mm.h> |
| #include <grub/partition.h> |
| #include <grub/disk.h> |
| #include <grub/file.h> |
| #include <grub/parser.h> |
| #include <grub/extcmd.h> |
| #include <grub/charset.h> |
| |
| /* The current word. */ |
| static const char *current_word; |
| |
| /* The matched string. */ |
| static char *match; |
| |
| /* The count of candidates. */ |
| static int num_found; |
| |
| /* The string to be appended. */ |
| static const char *suffix; |
| |
| /* The callback function to print items. */ |
| static void (*print_func) (const char *, grub_completion_type_t, int); |
| |
| /* The state the command line is in. */ |
| static grub_parser_state_t cmdline_state; |
| |
| |
| /* Add a string to the list of possible completions. COMPLETION is the |
| string that should be added. EXTRA will be appended if COMPLETION |
| matches uniquely. The type TYPE specifies what kind of data is added. */ |
| static int |
| add_completion (const char *completion, const char *extra, |
| grub_completion_type_t type) |
| { |
| if (grub_strncmp (current_word, completion, grub_strlen (current_word)) == 0) |
| { |
| num_found++; |
| |
| switch (num_found) |
| { |
| case 1: |
| match = grub_strdup (completion); |
| if (! match) |
| return 1; |
| suffix = extra; |
| break; |
| |
| case 2: |
| if (print_func) |
| print_func (match, type, 0); |
| |
| /* Fall through. */ |
| |
| default: |
| { |
| char *s = match; |
| const char *t = completion; |
| |
| if (print_func) |
| print_func (completion, type, num_found - 1); |
| |
| /* Detect the matched portion. */ |
| while (*s && *t && *s == *t) |
| { |
| s++; |
| t++; |
| } |
| s = match + grub_getend (match, s); |
| |
| *s = '\0'; |
| } |
| break; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int |
| iterate_partition (grub_disk_t disk, const grub_partition_t p, |
| void *data __attribute__ ((unused))) |
| { |
| const char *disk_name = disk->name; |
| char *name; |
| int ret; |
| char *part_name; |
| |
| part_name = grub_partition_get_name (p); |
| if (! part_name) |
| return 1; |
| |
| name = grub_xasprintf ("%s,%s", disk_name, part_name); |
| grub_free (part_name); |
| |
| if (! name) |
| return 1; |
| |
| ret = add_completion (name, ")", GRUB_COMPLETION_TYPE_PARTITION); |
| grub_free (name); |
| return ret; |
| } |
| |
| static int |
| iterate_dir (const char *filename, const struct grub_dirhook_info *info, |
| void *data __attribute__ ((unused))) |
| { |
| if (! info->dir) |
| { |
| const char *prefix; |
| if (cmdline_state == GRUB_PARSER_STATE_DQUOTE) |
| prefix = "\" "; |
| else if (cmdline_state == GRUB_PARSER_STATE_QUOTE) |
| prefix = "\' "; |
| else |
| prefix = " "; |
| |
| if (add_completion (filename, prefix, GRUB_COMPLETION_TYPE_FILE)) |
| return 1; |
| } |
| else if (grub_strcmp (filename, ".") && grub_strcmp (filename, "..")) |
| { |
| char *fname; |
| |
| fname = grub_xasprintf ("%s/", filename); |
| if (add_completion (fname, "", GRUB_COMPLETION_TYPE_FILE)) |
| { |
| grub_free (fname); |
| return 1; |
| } |
| grub_free (fname); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| iterate_dev (const char *devname, void *data __attribute__ ((unused))) |
| { |
| grub_device_t dev; |
| |
| /* Complete the partition part. */ |
| dev = grub_device_open (devname); |
| |
| if (!dev) |
| { |
| grub_errno = GRUB_ERR_NONE; |
| return 0; |
| } |
| |
| if (grub_strcmp (devname, current_word) == 0) |
| { |
| if (add_completion (devname, ")", GRUB_COMPLETION_TYPE_PARTITION)) |
| { |
| grub_device_close (dev); |
| return 1; |
| } |
| |
| if (dev->disk) |
| if (grub_partition_iterate (dev->disk, iterate_partition, NULL)) |
| { |
| grub_device_close (dev); |
| return 1; |
| } |
| } |
| else if (add_completion (devname, "", GRUB_COMPLETION_TYPE_DEVICE)) |
| { |
| grub_device_close (dev); |
| return 1; |
| } |
| |
| grub_device_close (dev); |
| grub_errno = GRUB_ERR_NONE; |
| return 0; |
| } |
| |
| /* Complete a device. */ |
| static int |
| complete_device (void) |
| { |
| /* Check if this is a device or a partition. */ |
| char *p = grub_strchr (++current_word, ','); |
| grub_device_t dev; |
| |
| if (! p) |
| { |
| /* Complete the disk part. */ |
| if (grub_disk_dev_iterate (iterate_dev, NULL)) |
| return 1; |
| } |
| else |
| { |
| /* Complete the partition part. */ |
| *p = '\0'; |
| dev = grub_device_open (current_word); |
| *p = ','; |
| grub_errno = GRUB_ERR_NONE; |
| |
| if (dev) |
| { |
| if (dev->disk) |
| { |
| if (grub_partition_iterate (dev->disk, iterate_partition, NULL)) |
| { |
| grub_device_close (dev); |
| return 1; |
| } |
| } |
| |
| grub_device_close (dev); |
| } |
| else |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| /* Complete a file. */ |
| static int |
| complete_file (void) |
| { |
| char *device; |
| char *dir; |
| char *last_dir; |
| grub_fs_t fs; |
| grub_device_t dev; |
| int ret = 0; |
| |
| device = grub_file_get_device_name (current_word); |
| if (grub_errno != GRUB_ERR_NONE) |
| return 1; |
| |
| dev = grub_device_open (device); |
| if (! dev) |
| { |
| ret = 1; |
| goto fail; |
| } |
| |
| fs = grub_fs_probe (dev); |
| if (! fs) |
| { |
| ret = 1; |
| goto fail; |
| } |
| |
| dir = grub_strchr (current_word + (device ? 2 + grub_strlen (device) : 0), |
| '/'); |
| last_dir = grub_strrchr (current_word, '/'); |
| if (dir) |
| { |
| char *dirfile; |
| |
| current_word = last_dir + 1; |
| |
| dir = grub_strdup (dir); |
| if (! dir) |
| { |
| ret = 1; |
| goto fail; |
| } |
| |
| /* Cut away the filename part. */ |
| dirfile = grub_strrchr (dir, '/'); |
| dirfile[1] = '\0'; |
| |
| /* Iterate the directory. */ |
| (fs->dir) (dev, dir, iterate_dir, NULL); |
| |
| grub_free (dir); |
| |
| if (grub_errno) |
| { |
| ret = 1; |
| goto fail; |
| } |
| } |
| else |
| { |
| current_word += grub_strlen (current_word); |
| match = grub_strdup ("/"); |
| if (! match) |
| { |
| ret = 1; |
| goto fail; |
| } |
| |
| suffix = ""; |
| num_found = 1; |
| } |
| |
| fail: |
| if (dev) |
| grub_device_close (dev); |
| grub_free (device); |
| return ret; |
| } |
| |
| /* Complete an argument. */ |
| static int |
| complete_arguments (char *command) |
| { |
| grub_command_t cmd; |
| grub_extcmd_t ext; |
| const struct grub_arg_option *option; |
| char shortarg[] = "- "; |
| |
| cmd = grub_command_find (command); |
| |
| if (!cmd || !(cmd->flags & GRUB_COMMAND_FLAG_EXTCMD)) |
| return 0; |
| |
| ext = cmd->data; |
| if (!ext->options) |
| return 0; |
| |
| if (add_completion ("-u", " ", GRUB_COMPLETION_TYPE_ARGUMENT)) |
| return 1; |
| |
| /* Add the short arguments. */ |
| for (option = ext->options; option->doc; option++) |
| { |
| if (! option->shortarg) |
| continue; |
| |
| shortarg[1] = option->shortarg; |
| if (add_completion (shortarg, " ", GRUB_COMPLETION_TYPE_ARGUMENT)) |
| return 1; |
| |
| } |
| |
| /* First add the built-in arguments. */ |
| if (add_completion ("--help", " ", GRUB_COMPLETION_TYPE_ARGUMENT)) |
| return 1; |
| if (add_completion ("--usage", " ", GRUB_COMPLETION_TYPE_ARGUMENT)) |
| return 1; |
| |
| /* Add the long arguments. */ |
| for (option = ext->options; option->doc; option++) |
| { |
| char *longarg; |
| if (!option->longarg) |
| continue; |
| |
| longarg = grub_xasprintf ("--%s", option->longarg); |
| if (!longarg) |
| return 1; |
| |
| if (add_completion (longarg, " ", GRUB_COMPLETION_TYPE_ARGUMENT)) |
| { |
| grub_free (longarg); |
| return 1; |
| } |
| grub_free (longarg); |
| } |
| |
| return 0; |
| } |
| |
| |
| static grub_parser_state_t |
| get_state (const char *cmdline) |
| { |
| grub_parser_state_t state = GRUB_PARSER_STATE_TEXT; |
| char use; |
| |
| while (*cmdline) |
| state = grub_parser_cmdline_state (state, *(cmdline++), &use); |
| return state; |
| } |
| |
| |
| /* Try to complete the string in BUF. Return the characters that |
| should be added to the string. This command outputs the possible |
| completions by calling HOOK, in that case set RESTORE to 1 so the |
| caller can restore the prompt. */ |
| char * |
| grub_normal_do_completion (char *buf, int *restore, |
| void (*hook) (const char *, grub_completion_type_t, int)) |
| { |
| int argc; |
| char **argv; |
| |
| /* Initialize variables. */ |
| match = 0; |
| num_found = 0; |
| suffix = ""; |
| print_func = hook; |
| |
| *restore = 1; |
| |
| if (grub_parser_split_cmdline (buf, 0, 0, &argc, &argv)) |
| return 0; |
| |
| if (argc == 0) |
| current_word = ""; |
| else |
| current_word = argv[argc - 1]; |
| |
| if (argc > 1 && ! grub_strcmp (argv[0], "set")) |
| { |
| char *equals = grub_strchr (current_word, '='); |
| if (equals) |
| /* Complete the value of the variable. */ |
| current_word = equals + 1; |
| } |
| |
| /* Determine the state the command line is in, depending on the |
| state, it can be determined how to complete. */ |
| cmdline_state = get_state (buf); |
| |
| if (argc == 1 || argc == 0) |
| { |
| /* Complete a command. */ |
| grub_command_t cmd; |
| FOR_COMMANDS(cmd) |
| { |
| if (cmd->prio & GRUB_COMMAND_FLAG_ACTIVE) |
| { |
| if (add_completion (cmd->name, " ", GRUB_COMPLETION_TYPE_COMMAND)) |
| goto fail; |
| } |
| } |
| } |
| else if (*current_word == '-') |
| { |
| if (complete_arguments (buf)) |
| goto fail; |
| } |
| else if (*current_word == '(' && ! grub_strchr (current_word, ')')) |
| { |
| /* Complete a device. */ |
| if (complete_device ()) |
| goto fail; |
| } |
| else |
| { |
| /* Complete a file. */ |
| if (complete_file ()) |
| goto fail; |
| } |
| |
| /* If more than one match is found those matches will be printed and |
| the prompt should be restored. */ |
| if (num_found > 1) |
| *restore = 1; |
| else |
| *restore = 0; |
| |
| /* Return the part that matches. */ |
| if (match) |
| { |
| char *ret; |
| char *escstr; |
| char *newstr; |
| int current_len; |
| int match_len; |
| int spaces = 0; |
| |
| current_len = grub_strlen (current_word); |
| match_len = grub_strlen (match); |
| |
| /* Count the number of spaces that have to be escaped. XXX: |
| More than just spaces have to be escaped. */ |
| for (escstr = match + current_len; *escstr; escstr++) |
| if (*escstr == ' ') |
| spaces++; |
| |
| ret = grub_malloc (match_len - current_len + grub_strlen (suffix) + spaces + 1); |
| newstr = ret; |
| for (escstr = match + current_len; *escstr; escstr++) |
| { |
| if (*escstr == ' ' && cmdline_state != GRUB_PARSER_STATE_QUOTE |
| && cmdline_state != GRUB_PARSER_STATE_QUOTE) |
| *(newstr++) = '\\'; |
| *(newstr++) = *escstr; |
| } |
| *newstr = '\0'; |
| |
| if (num_found == 1) |
| grub_strcpy (newstr, suffix); |
| |
| if (*ret == '\0') |
| { |
| grub_free (ret); |
| goto fail; |
| } |
| |
| if (argc != 0) |
| grub_free (argv[0]); |
| grub_free (match); |
| return ret; |
| } |
| |
| fail: |
| if (argc != 0) |
| { |
| grub_free (argv[0]); |
| grub_free (argv); |
| } |
| grub_free (match); |
| grub_errno = GRUB_ERR_NONE; |
| |
| return 0; |
| } |