Как именно обратная косая черта работает внутри обратных кавычек?

Из часто задаваемых вопросов по Bash:

Обратная косая черта (\) внутри обратных кавычек обрабатывается неочевидным способом:

 $ echo "'echo \\a'" "$(echo \\a)"
 a \a
 $ echo "'echo \\\\a'" "$(echo \\\\a)"
 \a \\a

Но FAQ не нарушает правила разбора, которые приводят к этой разнице. Единственная релевантная цитата из man bash, которую я нашел, была:

Когда используется форма замены в стиле кавычек в старом стиле, обратный слеш сохраняет свое буквальное значение, за исключением случаев, когда следует $, 'или.

Случаи "$(echo \\a)" и "$(echo \\\\a)" достаточно просты: обратная косая черта, escape-символ, превращается в буквальную обратную реакцию. Таким образом, каждый экземпляр \\ становится \ на выходе. Но я изо всех сил пытаюсь понять аналогичную логику для случаев backtick. Что является основным правилом и как из этого вытекает наблюдаемый результат?

Наконец, связанный вопрос... Если вы не заключите в кавычки обратные ссылки, вы получите сообщение об ошибке "нет совпадения":

$ echo 'echo \\\\a'
-bash: no match: \a

Что происходит в этом случае?

обновление

Re: мой главный вопрос, у меня есть теория для набора правил, который объясняет все поведение, но все еще не понимаю, как это следует из каких-либо документированных правил в bash. Вот мои предложенные правила....

Внутри обратной косой черты обратный слеш перед персонажем просто возвращает этот символ. То есть, один обратный слеш не имеет никакого эффекта. И это верно для всех персонажей, кроме самой обратной реакции и обратных галочек. В случае самого обратного слэша \\ становится экранирующим обратным слэшем. Он избежит своего следующего персонажа.

Давайте посмотрим, как это закончится на примере:

a=xx
echo "'echo $a'"      # prints the value of $a
echo "'echo \$a'"     # single backslash has no effect: equivalent to above
echo "'echo \\$a'"    # escaping backslash make $ literal

принтами:

xx
xx
$a

Попробуйте онлайн!

Давайте проанализируем оригинальные примеры с этой точки зрения:

echo "'echo \\a'"

Здесь \\ создает экранирующую обратную косую черту, но когда мы "убегаем" a, мы просто возвращаемся a, поэтому он печатает a.

echo "'echo \\\\a'"

Здесь первая пара \\ создает экранирующую обратную косую черту, которая применяется к \, создавая буквальную обратную косую черту. То есть первые 3 \\\ становятся одним литералом \ в выходных данных. Оставшийся \a просто производит a. Конечный результат - \a.

Ответ 1

Логика довольно проста как таковая. Итак, мы смотрим на исходный код bash (4.4) сам

subst.c: 9273

case ''': /* Backquoted command substitution. */
{
    t_index = sindex++;

    temp = string_extract(string, &sindex, "'", SX_REQMATCH);
    /* The test of sindex against t_index is to allow bare instances of
        ' to pass through, for backwards compatibility. */
    if (temp == &extract_string_error || temp == &extract_string_fatal)
    {
    if (sindex - 1 == t_index)
    {
        sindex = t_index;
        goto add_character;
    }
    last_command_exit_value = EXECUTION_FAILURE;
    report_error(_("bad substitution: no closing \"'\" in %s"), string + t_index);
    free(string);
    free(istring);
    return ((temp == &extract_string_error) ? &expand_word_error
                                            : &expand_word_fatal);
    }

    if (expanded_something)
    *expanded_something = 1;

    if (word->flags & W_NOCOMSUB)
    /* sindex + 1 because string[sindex] == ''' */
    temp1 = substring(string, t_index, sindex + 1);
    else
    {
    de_backslash(temp);
    tword = command_substitute(temp, quoted);
    temp1 = tword ? tword->word : (char *)NULL;
    if (tword)
        dispose_word_desc(tword);
    }
    FREE(temp);
    temp = temp1;
    goto dollar_add_string;
}

Как видите, в строке вызывается функция de_backslash(temp);, которая обновляет строку в c. Код той же функции находится ниже

subst.c: 1607

/* Remove backslashes which are quoting backquotes from STRING.  Modifies
   STRING, and returns a pointer to it. */
char *
    de_backslash(string) char *string;
{
  register size_t slen;
  register int i, j, prev_i;
  DECLARE_MBSTATE;

  slen = strlen(string);
  i = j = 0;

  /* Loop copying string[i] to string[j], i >= j. */
  while (i < slen)
  {
    if (string[i] == '\\' && (string[i + 1] == ''' || string[i + 1] == '\\' ||
                              string[i + 1] == '$'))
      i++;
    prev_i = i;
    ADVANCE_CHAR(string, slen, i);
    if (j < prev_i)
      do
        string[j++] = string[prev_i++];
      while (prev_i < i);
    else
      j = i;
  }
  string[j] = '\0';

  return (string);
}

Вышеприведенное просто делает простую вещь, если есть символ \ и следующий символ - \ или обратный тик или $, затем пропустите этот символ \ и скопируйте следующий символ

Так что если преобразовать его в Python для простоты

text = r"\\\\$a"

slen = len(text)
i = 0
j = 0
data = ""
while i < slen:
    if (text[i] == '\\' and (text[i + 1] == ''' or text[i + 1] == '\\' or
                             text[i + 1] == '$')):
        i += 1
    data += text[i]
    i += 1

print(data)

Выход того же - \\$a. А теперь давайте протестируем то же самое в bash

$ a=xxx

$ echo "$(echo \\$a)"
\xxx

$ echo "'echo \\\\$a'"
\xxx

Ответ 2

Сделал еще несколько исследований, чтобы найти ссылку и правило того, что происходит. В Справочном руководстве по GNU Bash говорится

Когда используется форма замещения в старом стиле, обратная косая черта сохраняет свое буквальное значение, за исключением случаев, когда следуют ‘$,‘ 'или ‘\. Первая обратная кавычка, которой не предшествует обратная косая черта, завершает команду замена. При использовании формы $ (команда) все символы между скобки составляют команду; никто не лечится специально.

Другими словами \,\$ и 'inside of' 'обрабатываются синтаксическим анализатором CLI перед заменой команды. Все остальное передается команде подстановки для обработки.

Давайте рассмотрим каждый пример из вопроса. После # я указываю, как подстановка команд была обработана синтаксическим анализатором CLI перед выполнением '' или $().

Ваш первый пример объяснил.

$ echo "'echo \\a'"   # echo \a
 a 
$ echo "$(echo \\a)"  # echo \\a
 \a

Ваш второй пример объяснил:

$ echo "'echo \\\\a'"   # echo \\a
 \a 
$ echo "$(echo \\\\a)"  # echo \\\\a
 \\a

Ваш третий пример:

a=xx
$ echo "'echo $a'"    # echo xx 
xx
$ echo "'echo \$a'"   # echo $a
xx
echo "'echo \\$a'"    # echo \$a
$a

Ваш третий пример с использованием $()

$ echo "$(echo $a)"     # echo $a
xx
$ echo "$(echo \$a)"    # echo \$a
$a
$ echo "$(echo \\$a)"   # echo \\$a
\xx