如何在bash中插入前一条命令的倒数第二个参数

历史展开(history expand)

历史展开是bash交互式操作时的一个重要展开方式,这种展开相对于bash的变量展开。
它的整体逻辑是首先是找到历史上的特定行(通常是前一行,这个也是缺省的操作行),然后从中提取特定行(例如第2个参数),或者执行特定操作(例如替换)。

历史事件(history event)

这个历史行号的寻找可能是通过数字指定行号,或者负数加数字指定后向相对的符号。例如!-1表示前一条命令,!1表示第一条逻辑命令(这个逻辑命令不太好确定,所以通常不用)。
通过??包含的明确搜索命令。例如!?ls?搜索历史上包含ls的命令,这个类似于vi/sed的匹配模式。
情况下则认为是从历史命令中搜索包后续(在遇到空白或者冒号时结束)字符串内容的命令。例如 !ls:表示历史上包括ls的最后一条命令 。


/* Return the event specified at TEXT + OFFSET modifying OFFSET to
   point to after the event specifier.  Just a pointer to the history
   line is returned; NULL is returned in the event of a bad specifier.
   You pass STRING with *INDEX equal to the history_expansion_char that
   begins this specification.
   DELIMITING_QUOTE is a character that is allowed to end the string
   specification for what to search for in addition to the normal
   characters `:', ` ', `\t', `\n', and sometimes `?'.
   So you might call this function like:
   line = get_history_event ("!echo:p", &index, 0);  */
char *
get_history_event (string, caller_index, delimiting_quote)
     const char *string;
     int *caller_index;
     int delimiting_quote;
{
  register int i;
  register char c;
  HIST_ENTRY *entry;
  int which, sign, local_index, substring_okay;
  _hist_search_func_t *search_func;
  char *temp;

  /* The event can be specified in a number of ways.

     !!   the previous command
     !n   command line N
     !-n  current command-line minus N
     !str the most recent command starting with STR
     !?str[?]
	  the most recent command containing STR

     All values N are determined via HISTORY_BASE. */

  i = *caller_index;

  if (string[i] != history_expansion_char)
    return ((char *)NULL);

  /* Move on to the specification. */
  i++;

  sign = 1;
  substring_okay = 0;

#define RETURN_ENTRY(e, w) \
	return ((e = history_get (w)) ? e->line : (char *)NULL)

  /* Handle !! case. */
  if (string[i] == history_expansion_char)
    {
      i++;
      which = history_base + (history_length - 1);
      *caller_index = i;
      RETURN_ENTRY (entry, which);
    }

  /* Hack case of numeric line specification. */
  if (string[i] == '-')
    {
      sign = -1;
      i++;
    }

  if (_rl_digit_p (string[i]))
    {
      /* Get the extent of the digits and compute the value. */
      for (which = 0; _rl_digit_p (string[i]); i++)
	which = (which * 10) + _rl_digit_value (string[i]);

      *caller_index = i;

      if (sign < 0)
	which = (history_length + history_base) - which;

      RETURN_ENTRY (entry, which);
    }

  /* This must be something to search for.  If the spec begins with
     a '?', then the string may be anywhere on the line.  Otherwise,
     the string must be found at the start of a line. */
  if (string[i] == '?')
    {
      substring_okay++;
      i++;
    }

  /* Only a closing `?' or a newline delimit a substring search string. */
  for (local_index = i; c = string[i]; i++)
    {
#if defined (HANDLE_MULTIBYTE)
      if (MB_CUR_MAX > 1 && rl_byte_oriented == 0)
	{
	  int v;
	  mbstate_t ps;

	  memset (&ps, 0, sizeof (mbstate_t));
	  /* These produce warnings because we're passing a const string to a
	     function that takes a non-const string. */
	  _rl_adjust_point ((char *)string, i, &ps);
	  if ((v = _rl_get_char_len ((char *)string + i, &ps)) > 1)
	    {
	      i += v - 1;
	      continue;
	    }
        }

#endif /* HANDLE_MULTIBYTE */
      if ((!substring_okay && (whitespace (c) || c == ':' ||
	  (history_search_delimiter_chars && member (c, history_search_delimiter_chars)) ||
	  string[i] == delimiting_quote)) ||
	  string[i] == '\n' ||
	  (substring_okay && string[i] == '?'))
	break;
    }

  which = i - local_index;
  temp = (char *)xmalloc (1 + which);
  if (which)
    strncpy (temp, string + local_index, which);
  temp[which] = '\0';

  if (substring_okay && string[i] == '?')
    i++;

  *caller_index = i;

#define FAIL_SEARCH() \
  do { \
    history_offset = history_length; free (temp) ; return (char *)NULL; \
  } while (0)

  /* If there is no search string, try to use the previous search string,
     if one exists.  If not, fail immediately. */
  if (*temp == '\0' && substring_okay)
    {
      if (search_string)
        {
          free (temp);
          temp = savestring (search_string);
        }
      else
        FAIL_SEARCH ();
    }

  search_func = substring_okay ? history_search : history_search_prefix;
  while (1)
    {
      local_index = (*search_func) (temp, -1);

      if (local_index < 0)
	FAIL_SEARCH ();

      if (local_index == 0 || substring_okay)
	{
	  entry = current_history ();
	  history_offset = history_length;
	
	  /* If this was a substring search, then remember the
	     string that we matched for word substitution. */
	  if (substring_okay)
	    {
	      FREE (search_string);
	      search_string = temp;

	      FREE (search_match);
	      search_match = history_find_word (entry->line, local_index);
	    }
	  else
	    free (temp);

	  return (entry->line);
	}

      if (history_offset)
	history_offset--;
      else
	FAIL_SEARCH ();
    }
#undef FAIL_SEARCH
#undef RETURN_ENTRY
}

指定动作

当获取到命令行之后,就可以对该命令行的内容进行特定操作。比较常见的是通过s进行字符串替换。 例如

tsecer@harry: fgrep notexit /home/harry/tsecer
grep: /home/harry/tsecer: 没有那个文件或目录
tsecer@harry: !:s/tsecer/fry
fgrep notexit /home/harry/fry
grep: /home/harry/fry: 没有那个文件或目录
tsecer@harry: 

其实也可以看到,这种操作方法并不是很直观,不如直接执行ctrl-p获得前一条命令,然后在这个命令上直接编辑。而且这种展开之后不会在历史命令中存在,下次再编辑也很麻烦。这也是这种展开不是很常用的原因。

提取事件指定参数(extract arg)

这里可以看到,如果last是负数,则表示从后向前数的参数。例如-2表示倒数第二个参数。

/* Extract the args specified, starting at FIRST, and ending at LAST.
   The args are taken from STRING.  If either FIRST or LAST is < 0,
   then make that arg count from the right (subtract from the number of
   tokens, so that FIRST = -1 means the next to last token on the line).
   If LAST is `$' the last arg from STRING is used. */
char *
history_arg_extract (first, last, string)
     int first, last;
     const char *string;
{
  register int i, len;
  char *result;
  int size, offset;
  char **list;

  /* XXX - think about making history_tokenize return a struct array,
     each struct in array being a string and a length to avoid the
     calls to strlen below. */
  if ((list = history_tokenize (string)) == NULL)
    return ((char *)NULL);

  for (len = 0; list[len]; len++)
    ;

  if (last < 0)
    last = len + last - 1;

  if (first < 0)
    first = len + first - 1;

  if (last == '$')
    last = len - 1;

  if (first == '$')
    first = len - 1;

  last++;

  if (first >= len || last > len || first < 0 || last < 0 || first > last)
    result = ((char *)NULL);
  else
    {
      for (size = 0, i = first; i < last; i++)
	size += strlen (list[i]) + 1;
      result = (char *)xmalloc (size + 1);
      result[0] = '\0';

      for (i = first, offset = 0; i < last; i++)
	{
	  strcpy (result + offset, list[i]);
	  offset += strlen (list[i]);
	  if (i + 1 < last)
	    {
      	      result[offset++] = ' ';
	      result[offset] = 0;
	    }
	}
    }

  for (i = 0; i < len; i++)
    free (list[i]);
  free (list);

  return (result);
}

如何指定负数参数

从这个代码看,并没有办法(通过!$)指定除了-1之外的负数last,没有其它方法指定负数参数。所以通过这种事件展开的方式是不能获得前一个命令的倒数第二个命令的,而只能正向指定参数。
通常认为的负数符号'-'在整个过程中被解析为了范围符号,所以在字符选择的时候除了可以通过'$'表示最后一个参数之外,没有其它的办法来获得从后向前数的参数。

/* Return a consed string which is the word specified in SPEC, and found
   in FROM.  NULL is returned if there is no spec.  The address of
   ERROR_POINTER is returned if the word specified cannot be found.
   CALLER_INDEX is the offset in SPEC to start looking; it is updated
   to point to just after the last character parsed. */
static char *
get_history_word_specifier (spec, from, caller_index)
     char *spec, *from;
     int *caller_index;
{
  register int i = *caller_index;
  int first, last;
  int expecting_word_spec = 0;
  char *result;

  /* The range of words to return doesn't exist yet. */
  first = last = 0;
  result = (char *)NULL;

  /* If we found a colon, then this *must* be a word specification.  If
     it isn't, then it is an error. */
  if (spec[i] == ':')
    {
      i++;
      expecting_word_spec++;
    }

  /* Handle special cases first. */

  /* `%' is the word last searched for. */
  if (spec[i] == '%')
    {
      *caller_index = i + 1;
      return (search_match ? savestring (search_match) : savestring (""));
    }

  /* `*' matches all of the arguments, but not the command. */
  if (spec[i] == '*')
    {
      *caller_index = i + 1;
      result = history_arg_extract (1, '$', from);
      return (result ? result : savestring (""));
    }

  /* `$' is last arg. */
  if (spec[i] == '$')
    {
      *caller_index = i + 1;
      return (history_arg_extract ('$', '$', from));
    }

  /* Try to get FIRST and LAST figured out. */

  if (spec[i] == '-')
    first = 0;
  else if (spec[i] == '^')
    {
      first = 1;
      i++;
    }
  else if (_rl_digit_p (spec[i]) && expecting_word_spec)
    {
      for (first = 0; _rl_digit_p (spec[i]); i++)
	first = (first * 10) + _rl_digit_value (spec[i]);
    }
  else
    return ((char *)NULL);	/* no valid `first' for word specifier */

  if (spec[i] == '^' || spec[i] == '*')
    {
      last = (spec[i] == '^') ? 1 : '$';	/* x* abbreviates x-$ */
      i++;
    }
  else if (spec[i] != '-')
    last = first;
  else
    {
      i++;

      if (_rl_digit_p (spec[i]))
	{
	  for (last = 0; _rl_digit_p (spec[i]); i++)
	    last = (last * 10) + _rl_digit_value (spec[i]);
	}
      else if (spec[i] == '$')
	{
	  i++;
	  last = '$';
	}
#if 0
      else if (!spec[i] || spec[i] == ':')
	/* check against `:' because there could be a modifier separator */
#else
      else
	/* csh seems to allow anything to terminate the word spec here,
	   leaving it as an abbreviation. */
#endif
	last = -1;		/* x- abbreviates x-$ omitting word `$' */
    }

  *caller_index = i;

  if (last >= first || last == '$' || last < 0)
    result = history_arg_extract (first, last, from);

  return (result ? result : (char *)&error_pointer);
}

键盘映射方法

在bash中,默认的
"\e.": insert-last-argument
是插入前一个命令行的最后一个参数,在这个功能的附近,还有其它的一组数字命令可以使用,它们用来干什么的呢?

"\e-": digit-argument
"\e0": digit-argument
"\e1": digit-argument
"\e2": digit-argument
"\e3": digit-argument
"\e4": digit-argument
"\e5": digit-argument
"\e6": digit-argument
"\e7": digit-argument
"\e8": digit-argument
"\e9": digit-argument

在vim中,操作可以配置一个数量,例如2diw表示删除两个单词。同样,在bash的命令行中,也可以通过这组按键来指定重复的次数。它们的区别在于不同的数字对应不同的重复次数,而bash的很多内置命令都和vim的命令一样,接收一个数值参数。
按键esc 2默认数值是2,esc 3默认初始值是3,一次类推,尽管还可以继续输入不同数字,但是这个按键之后的第一个数字决定了之后数值的第一位的内容。而由于通常重复都是10以内的,所以同样个按键的不同数字代表不同的重复次数还是比较合理的。
例如,下面的函数除了key之外还有一个参数count参数,这个就是可以通过esc+num输入的操作数量。


/* Delete the character under the cursor.  Given a numeric argument,
   kill that many characters instead. */
int
rl_delete (count, key)
     int count, key;
{
  int xpoint;

  if (count < 0)
    return (_rl_rubout_char (-count, key));

  if (rl_point == rl_end)
    {
      rl_ding ();
      return -1;
    }

  if (count > 1 || rl_explicit_arg)
    {
      xpoint = rl_point;
      if (MB_CUR_MAX > 1 && rl_byte_oriented == 0)
	rl_forward_char (count, key);
      else
	rl_forward_byte (count, key);

      rl_kill_text (xpoint, rl_point);
      rl_point = xpoint;
    }
  else
    {
      xpoint = MB_NEXTCHAR (rl_line_buffer, rl_point, 1, MB_FIND_NONZERO);
      rl_delete_text (rl_point, xpoint);
    }
  return 0;
}
  
/* Rubout the character behind point. */
int
rl_rubout (count, key)
     int count, key;
{
  if (count < 0)
    return (rl_delete (-count, key));

  if (!rl_point)
    {
      rl_ding ();
      return -1;
    }

  if (rl_insert_mode == RL_IM_OVERWRITE)
    return (_rl_overwrite_rubout (count, key));

  return (_rl_rubout_char (count, key));
}

为什么ctrl-w也可以指定数量

通过stty命令可以看到,显示的tty终端中ctrl - w删除单词是内核的ntty驱动处理的,但是通过 esc + num ctrl - w还是可以删除num个单词,这说明这个单词删除是bash完成的。
也就是readline在读取输入前会先关掉(rl_prep_term_function)内核的ctrl-w功能,然后在读取完成(用户输入回车)之后在开启(rl_deprep_term_function)该功能。

/* Read a line of input.  Prompt with PROMPT.  An empty PROMPT means
   none.  A return value of NULL means that EOF was encountered. */
char *
readline (prompt)
     const char *prompt;
{
///....
  if (rl_prep_term_function)
    (*rl_prep_term_function) (_rl_meta_flag);

#if defined (HANDLE_SIGNALS)
  rl_set_signals ();
#endif

  value = readline_internal ();
  if (rl_deprep_term_function)
    (*rl_deprep_term_function) ();
///...
}

结论

在当前bash的默认配置下,没有办法直接指定获取前一个命令的倒数第二个参数。

posted on 2022-10-30 11:09  tsecer  阅读(77)  评论(0编辑  收藏  举报

导航