/* bashline.c -- Bash's interface to the readline library. */
-/* Copyright (C) 1987-2023 Free Software Foundation, Inc.
+/* Copyright (C) 1987-2024 Free Software Foundation, Inc.
This file is part of GNU Bash, the Bourne Again SHell.
/* Tell the completer that we want a crack first. */
rl_attempted_completion_function = attempt_shell_completion;
- /* Tell the completer that we might want to follow symbolic links or
- do other expansion on directory names. */
- set_directory_hook ();
-
- rl_filename_rewrite_hook = bash_filename_rewrite_hook;
-
- rl_filename_stat_hook = bash_filename_stat_hook;
+ bashline_set_filename_hooks ();
/* Tell the filename completer we want a chance to ignore some names. */
rl_ignore_some_completions_function = filename_completion_ignore;
rl_bind_key_if_unbound_in_map ('@', posix_edit_macros, vi_movement_keymap);
# endif
+ rl_add_defun ("bash-vi-complete", bash_vi_complete, -1);
rl_bind_key_in_map ('\\', bash_vi_complete, vi_movement_keymap);
rl_bind_key_in_map ('*', bash_vi_complete, vi_movement_keymap);
rl_bind_key_in_map ('=', bash_vi_complete, vi_movement_keymap);
rl_filename_quote_characters = default_filename_quote_characters;
set_filename_bstab (rl_filename_quote_characters);
- set_directory_hook ();
- rl_filename_stat_hook = bash_filename_stat_hook;
+ rl_filename_quoting_function = bash_quote_filename;
+
+ bashline_set_filename_hooks ();
bashline_reset_event_hook ();
char *temp, buffer[256], name[256];
register int i, start;
+#ifdef __MSYS__
+ file = fopen (filename, "rt");
+#else
file = fopen (filename, "r");
+#endif
if (file == 0)
return;
continue;
/* OK, it matches. Add it to the list. */
- if (nmatch >= (rsize - 1))
+ if (nmatch + 1 >= rsize)
{
rsize = (rsize + 16) - (rsize % 16);
result = strvec_resize (result, rsize);
if (rl_explicit_arg)
{
- command = (char *)xmalloc (strlen (edit_command) + 8);
- sprintf (command, "%s %d", edit_command, count);
+ size_t clen;
+ /* 32 exceeds strlen (itos (INTMAX_MAX)) (19) */
+ clen = strlen (edit_command) + 32;
+ command = (char *)xmalloc (clen);
+ snprintf (command, clen, "%s %d", edit_command, count);
}
else
{
break;
text = rl_copy_text (wbeg, wend);
+ if (text == 0 || *text == 0)
+ break;
newdir = dirspell (text);
if (newdir)
static inline int
check_redir (int ti)
{
- register int this_char, prev_char;
+ int this_char, prev_char, next_char;
/* Handle the two character tokens `>&', `<&', and `>|'.
We are not in a command position after one of these. */
this_char = rl_line_buffer[ti];
prev_char = (ti > 0) ? rl_line_buffer[ti - 1] : 0;
+ next_char = (ti < rl_end) ? rl_line_buffer[ti + 1] : 0;
if ((this_char == '&' && (prev_char == '<' || prev_char == '>')) ||
(this_char == '|' && prev_char == '>'))
return (1);
- else if (this_char == '{' && prev_char == '$') /*}*/
+ else if (this_char == '{' && prev_char == '$' && FUNSUB_CHAR (next_char) == 0) /*}*/
return (1);
#if 0 /* Not yet */
else if (this_char == '(' && prev_char == '$') /*)*/
complete_fullquote = 1; /* full filename quoting by default */
rl_filename_quote_characters = default_filename_quote_characters;
set_filename_bstab (rl_filename_quote_characters);
- set_directory_hook ();
- rl_filename_stat_hook = bash_filename_stat_hook;
+ bashline_set_filename_hooks ();
rl_sort_completion_matches = 1; /* sort by default */
s1 = s = e1;
break;
}
+ else if (s > e)
+ {
+ s = s1 = start; e = e1 = end; /* reset */
+ }
/* Skip over assignment statements preceding a command name. If we
don't find a command name at all, we can perform command name
completion. If we find a partial command name, we should perform
end == index of where word to be completed ends
if (s == start) we are doing command word completion for sure
if (e1 == end) we are at the end of the command name and completing it */
- if (start == 0 && end == 0 && e != 0 && text[0] == '\0') /* beginning of non-empty line */
+ if (start == 0 && end == 0 && e != 0 && e1 < rl_end && text[0] == '\0') /* beginning of non-empty line */
foundcs = 0;
else if (start == end && start == s1 && e != 0 && e1 > end) /* beginning of command name, leading whitespace */
foundcs = 0;
+ else if (start == 0 && start == end && start < s1 && e != 0 && e1 > end && text[0] == '\0' && have_progcomps) /* no command name, leading whitespace only */
+ prog_complete_matches = programmable_completions (EMPTYCMD, text, s, e, &foundcs);
else if (e == 0 && e == s && text[0] == '\0' && have_progcomps) /* beginning of empty line */
prog_complete_matches = programmable_completions (EMPTYCMD, text, s, e, &foundcs);
else if (start == end && text[0] == '\0' && s1 > start && whitespace (rl_line_buffer[start]))
/* If we have defined a compspec for the initial (command) word, call
it and process the results like any other programmable completion. */
if (in_command_position && have_progcomps && foundcs == 0 && iw_compspec)
- prog_complete_matches = programmable_completions (INITIALWORD, text, s, e, &foundcs);
+ {
+ /* Do some sanity checking on S and E. Room for more here. */
+ if (s > rl_point)
+ s = rl_point;
+ if (e > rl_end)
+ e = rl_end;
+ prog_complete_matches = programmable_completions (INITIALWORD, text, s, e, &foundcs);
+ }
FREE (n);
/* XXX - if we found a COMPSPEC for the command, just return whatever
{
if (qc != '\'' && text[1] == '(') /* ) */
matches = rl_completion_matches (text, command_subst_completion_function);
+ else if (qc != '\'' && text[1] == '{' && FUNSUB_CHAR (text[2])) /* } */
+ matches = rl_completion_matches (text, command_subst_completion_function);
else
{
matches = rl_completion_matches (text, variable_completion_function);
rl_completion_suppress_append = 1;
rl_filename_completion_desired = 0;
}
-#if 0
- /* TAG:bash-5.3 jidanni@jidanni.org 11/11/2022 */
+#if 1
else if (matches[0] && matches[1] && STREQ (matches[0], matches[1]) &&
matches[2] && STREQ (matches[1], matches[2]) && CMD_IS_DIR (matches[0]))
#else
result = search_for_command (cname, 0);
if (result)
{
+ FREE (*name);
*name = result;
return 1;
}
/* This gets an unquoted filename, so we need to quote special characters
in the filename before the completion hook gets it. */
-#if 0
- f = savestring (filename);
-#else
c = 0;
f = bash_quote_filename ((char *)filename, SINGLE_MATCH, &c);
-#endif
bash_directory_completion_hook (&f);
r = searching_path ? executable_file (f) : executable_or_directory (f);
free (dequoted_hint);
if (hint)
free (hint);
+ dequoted_hint = hint = (char *)NULL;
mapping_over = searching_path = 0;
hint_is_dir = CMD_IS_DIR (hint_text);
if (rl_completion_found_quote && rl_completion_quote_character == 0)
dequoted_hint = bash_dequote_filename (hint, 0);
- path = get_string_value ("PATH");
+ path = path_value ("PATH", 0);
path_index = dot_in_path = 0;
/* Initialize the variables for each type of command word. */
local_index = 0;
if (glob_matches[1] && rl_completion_type == TAB) /* multiple matches are bad */
- return ((char *)NULL);
+ {
+ strvec_dispose (glob_matches);
+ glob_matches = (char **)NULL;
+ return ((char *)NULL);
+ }
}
while (val = glob_matches[local_index++])
free (fnhint);
if (filename_hint)
free (filename_hint);
+ fnhint = filename_hint = (char *)NULL;
filename_hint = sh_makepath (current_path, hint, 0);
/* Need a quoted version (though it doesn't matter much in most
t1 = make_absolute (val, t);
free (t);
cval = sh_canonpath (t1, PATH_CHECKDOTDOT|PATH_CHECKEXISTS);
+ free (t1);
}
else
#endif
text++;
else if (*text == '$' && text[1] == '(') /* ) */
text += 2;
+ else if (*text == '$' && text[1] == '{' && FUNSUB_CHAR (text[2])) /*}*/
+ text += 3; /* nofork command substitution */
/* If the text was quoted, suppress any quote character that the
readline completion code would insert. */
rl_completion_suppress_quote = 1;
start_len = text - orig_start;
filename_text = savestring (text);
if (matches)
- free (matches);
+ {
+ free (matches);
+ matches = (char **)NULL;
+ }
/*
* At this point we can entertain the idea of re-parsing
}
}
-/* Make NEW_LINE be the current readline line. This frees NEW_LINE. */
+/* Make NEW_LINE be the current readline line. This frees NEW_LINE. We use
+ several heuristics to decide where to put rl_point. */
+
static void
set_up_new_line (char *new_line)
{
- int old_point, at_end;
+
+ int old_point, old_end, dist, nb;
+ char *s, *t;
+
+ /* If we didn't expand anything, don't change anything. */
+ if (STREQ (new_line, rl_line_buffer))
+ {
+ free (new_line);
+ return;
+ }
old_point = rl_point;
- at_end = rl_point == rl_end;
+ old_end = rl_end;
+ dist = rl_end - rl_point;
+ nb = rl_end - old_end;
/* If the line was history and alias expanded, then make that
be one thing to undo. */
free (new_line);
/* Place rl_point where we think it should go. */
- if (at_end)
+ if (dist == 0)
rl_point = rl_end;
- else if (old_point < rl_end)
+ else if (old_point == 0)
+ {
+ /* this is what the old code did, but separate it out so we can treat
+ it differently */
+ rl_point = 0;
+ if (!whitespace (rl_line_buffer[rl_point]))
+ rl_forward_word (1, 0);
+ }
+ else if (rl_point < old_point+nb)
{
+ /* let's assume that this means the new point is within the changed region */
rl_point = old_point;
if (!whitespace (rl_line_buffer[rl_point]))
rl_forward_word (1, 0);
}
+ else
+ rl_point = rl_end - dist; /* try to put point the same distance from end */
+
+ if (rl_point < 0)
+ rl_point = 0;
+ else if (rl_point > rl_end)
+ rl_point = rl_end;
+ rl_mark = 0; /* XXX */
}
#if defined (ALIAS)
for (nlen = strlen (name), p = fignore.ignores; p->val; p++)
{
- if (nlen > p->len && p->len > 0 && STREQ (p->val, &name[nlen - p->len]))
+ if (nlen >= p->len && p->len > 0 && STREQ (p->val, &name[nlen - p->len]))
return (0);
}
int r;
fn = bash_tilde_expand (name, 0);
+
+#if __CYGWIN
+ /* stat("//server") can only be successful as a directory, but can take
+ seconds to time out on failure. It is much faster to assume that
+ "//server" is a valid name than it is to wait for a stat, even if it
+ gives false positives on bad names. */
+ if (fn[0] == '/' && fn[1] == '/' && ! strchr (&fn[2], '/'))
+ {
+ free (fn);
+ return 1;
+ }
+#endif
+
r = file_isdir (fn);
free (fn);
rl_directory_rewrite_hook = hookf;
}
+/* Set the readline hooks that affect how directories and filenames are
+ converted. Extern so other parts of the shell can use. */
+void
+bashline_set_filename_hooks (void)
+{
+ /* Tell the completer that we might want to follow symbolic links or
+ do other expansion on directory names. */
+ set_directory_hook ();
+
+ rl_filename_rewrite_hook = bash_filename_rewrite_hook;
+ rl_completion_rewrite_hook = bash_filename_rewrite_hook;
+ rl_filename_stat_hook = bash_filename_stat_hook;
+}
+
/* Check whether not DIRNAME, with any trailing slash removed, exists. If
SHOULD_DEQUOTE is non-zero, we dequote the directory name first. */
static int
{
rl_filename_completion_desired = 1;
FREE (matches);
+ matches = (char **)NULL;
if (globorig != globtext)
FREE (globorig);
FREE (globtext);
+ globorig = globtext = (char *)NULL;
ttext = bash_tilde_expand (text, 0);
#endif /* SPECIFIC_COMPLETION_FUNCTIONS */
#if defined (VI_MODE)
+/* This does pretty much what _rl_vi_advance_point does. */
+static inline int
+vi_advance_point (void)
+{
+ int point;
+ DECLARE_MBSTATE;
+
+ point = rl_point;
+ if (rl_point < rl_end)
+#if defined (HANDLE_MULTIBYTE)
+ {
+ if (locale_mb_cur_max == 1)
+ rl_point++;
+ else
+ {
+ point = rl_point;
+ ADVANCE_CHAR (rl_line_buffer, rl_end, rl_point);
+ if (point == rl_point || rl_point > rl_end)
+ rl_point = rl_end;
+ }
+ }
+#else
+ rl_point++:
+#endif
+ return point;
+}
+
/* Completion, from vi mode's point of view. This is a modified version of
rl_vi_complete which uses the bash globbing code to implement what POSIX
- specifies, which is to append a `*' and attempt filename generation (which
- has the side effect of expanding any globbing characters in the word). */
+ specifies, which is to optinally append a `*' and attempt filename
+ generation (which has the side effect of expanding any globbing characters
+ in the word). */
static int
bash_vi_complete (int count, int key)
{
{
if (!whitespace (rl_line_buffer[rl_point + 1]))
rl_vi_end_word (1, 'E');
- rl_point++;
+ vi_advance_point ();
}
/* Find boundaries of current word, according to vi definition of a
ret = (char *)xmalloc (l + 1);
for (quoted = quote_char, p = text, r = ret; p && *p; p++)
{
- /* Allow backslash-escaped characters to pass through unscathed. */
- if (*p == '\\')
+ /* Allow backslash-escaped characters to pass through unscathed. Backslashes
+ aren't special in single quotes. */
+ if (quoted != '\'' && *p == '\\')
{
- /* Backslashes are preserved within single quotes. */
- if (quoted == '\'')
- *r++ = *p;
/* Backslashes are preserved within double quotes unless the
character is one that is defined to be escaped */
- else if (quoted == '"' && ((sh_syntaxtab[(unsigned char)p[1]] & CBSDQUOTE) == 0))
+ if (quoted == '"' && ((sh_syntaxtab[(unsigned char)p[1]] & CBSDQUOTE) == 0))
*r++ = *p;
*r++ = *++p;
}
}
+void
+uw_restore_parser_state (void *ps)
+{
+ restore_parser_state (ps);
+}
+
+void
+uw_rl_set_signals (void *ignore)
+{
+ rl_set_signals ();
+}
+
+static void
+unbind_readline_variables (void)
+{
+ check_unbind_variable ("READLINE_LINE");
+ check_unbind_variable ("READLINE_POINT");
+ check_unbind_variable ("READLINE_MARK");
+ check_unbind_variable ("READLINE_ARGUMENT");
+ array_needs_making = 1;
+}
+
+static void
+uw_unbind_readline_variables (void *ignore)
+{
+ unbind_readline_variables ();
+}
+
int
bash_execute_unix_command (int count, int key)
{
kslen = rl_key_sequence_length;
/* If we have a numeric argument, chop it off the front of the key sequence */
- if (count > 1 || rl_explicit_arg)
+ if (count != 1 || rl_explicit_arg)
{
i = rl_trim_arg_from_keyseq (rl_executing_keyseq, rl_key_sequence_length, rl_get_keymap ());
if (i > 0)
if (v)
VSETATTR (v, att_exported);
- if (count > 1 || rl_explicit_arg)
+ if (count != 1 || rl_explicit_arg)
{
value = inttostr (count, ibuf, sizeof (ibuf));
v = bind_int_variable ("READLINE_ARGUMENT", value, 0);
}
array_needs_making = 1;
+ begin_unwind_frame ("execute-unix-command");
save_parser_state (&ps);
rl_clear_signals ();
+ add_unwind_protect (uw_unbind_readline_variables, 0);
+ add_unwind_protect (uw_restore_parser_state, &ps);
+ add_unwind_protect (uw_rl_set_signals, 0);
r = parse_and_execute (savestring (cmd), "bash_execute_unix_command", SEVAL_NOHIST);
rl_set_signals ();
restore_parser_state (&ps);
maybe_make_readline_line (v ? value_cell (v) : 0);
v = find_variable ("READLINE_POINT");
- if (v && legal_number (value_cell (v), &mi))
+ if (v && valid_number (value_cell (v), &mi))
readline_set_char_offset (mi, &rl_point);
v = find_variable ("READLINE_MARK");
- if (v && legal_number (value_cell (v), &mi))
+ if (v && valid_number (value_cell (v), &mi))
readline_set_char_offset (mi, &rl_mark);
- check_unbind_variable ("READLINE_LINE");
- check_unbind_variable ("READLINE_POINT");
- check_unbind_variable ("READLINE_MARK");
- check_unbind_variable ("READLINE_ARGUMENT");
- array_needs_making = 1;
+ unbind_readline_variables ();
+ discard_unwind_frame ("execute-unix-command");
/* and restore the readline buffer and display after command execution. */
/* If we clear the last line of the prompt above, redraw only that last
return 0;
}
+/* This only has to handle macros/shell commandsfrom print_unix_command_map */
+static void
+print_unix_command (const char *kseq, const char *value, int readable, const char *prefix)
+{
+ if (readable)
+ fprintf (rl_outstream, "\"%s%s\" \"%s\"\n", prefix ? prefix : "", kseq, value ? value : "");
+ else
+ fprintf (rl_outstream, "%s%s outputs %s\n", prefix ? prefix : "", kseq, value ? value : "");
+}
+
int
print_unix_command_map (void)
{
Keymap save, cmd_xmap;
+ rl_macro_print_func_t *old_printer;
save = rl_get_keymap ();
cmd_xmap = get_cmd_xmap_from_keymap (save);
rl_set_keymap (cmd_xmap);
+ old_printer = rl_macro_display_hook;
+ rl_macro_display_hook = print_unix_command;
rl_macro_dumper (1);
+ rl_macro_display_hook = old_printer;
rl_set_keymap (save);
return 0;
}
{
Keymap kmap, cmd_xmap;
char *kseq, *value;
- int i, kstart;
+ int i, kstart, translate;
kmap = rl_get_keymap ();
/* We duplicate some of the work done by rl_parse_and_bind here, but
- this code only has to handle `"keyseq": ["]command["]' and can
+ this code only has to handle `"keyseq"[:][ \t]+["]command["]' and can
generate an error for anything else. */
i = isolate_sequence (line, 0, 1, &kstart);
if (i < 0)
/* Create the key sequence string to pass to rl_generic_bind */
kseq = substring (line, kstart, i);
- for ( ; line[i] && line[i] != ':'; i++)
+ /* Allow colon or whitespace to separate the key sequence and command string. */
+ for ( ; line[i] && line[i] != ':' && whitespace (line[i]) == 0; i++)
;
- if (line[i] != ':')
+ if (line[i] != ':' && whitespace (line[i]) == 0)
{
- builtin_error (_("%s: missing colon separator"), line);
+ builtin_error (_("%s: missing separator"), line);
FREE (kseq);
return -1;
}
- i = isolate_sequence (line, i + 1, 0, &kstart);
+ /* If we have a whitespace separator we're going to call rl_macro_bind so
+ we get the readline-translated version of the value (backslash-escapes
+ handled, etc.) */
+ translate = line[i] != ':';
+
+ /* Kind of tricky. If we use whitespace as a delimiter, we can backslash-
+ quote double quotes and have them preserved in the value. However, we
+ still want to be able to auto-detect quoted strings and only require
+ them with whitespace delimiters. */
+ i = isolate_sequence (line, i + 1, translate, &kstart);
if (i < 0)
{
FREE (kseq);
/* Save the command to execute and the key sequence in the CMD_XMAP */
cmd_xmap = get_cmd_xmap_from_keymap (kmap);
- rl_generic_bind (ISMACR, kseq, value, cmd_xmap);
+ if (translate)
+ rl_macro_bind (kseq, value, cmd_xmap);
+ else
+ rl_generic_bind (ISMACR, kseq, value, cmd_xmap);
/* and bind the key sequence in the current keymap to a function that
understands how to execute from CMD_XMAP */