Отступы heredocs с пробелами

Для личного развития и проектов, над которыми я работаю, мы используем четыре пробела вместо вкладок. Тем не менее, мне нужно использовать heredoc, и я не могу этого сделать, не нарушая поток отступа.

Единственный способ сделать это, о котором я могу думать, будет следующим:

usage() {
    cat << '    EOF' | sed -e 's/^    //';
    Hello, this is a cool program.
    This should get unindented.
    This code should stay indented:
        something() {
            echo It works, yo!;
        }
    That all.
    EOF
}

Есть ли лучший способ сделать это?

Сообщите мне, если это принадлежит Unix/Linux Stack Exchange.

Ответы

Ответ 1

(Если вы используете bash 4, прокрутите до конца, чтобы я считал, что это лучшая комбинация чистой оболочки и читаемости.)

Для сценариев оболочки использование вкладок не является вопросом предпочтения или стиля; как определить язык.

usage () {
    # Lines between EOF are each indented with the same number of tabs
    # Spaces can follow the tabs for in-document indentation
    cat <<-EOF
        Hello, this is a cool program.
        This should get unindented.
        This code should stay indented:
            something() {
                echo It works, yo!;
            }
        That all.
    EOF
}

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

usage () {
    printf '%s\n' \
        "Hello, this is a cool program." \
        "This should get unindented." \
        "This code should stay indented:" \
        "    something() {" \
        "        echo It works, yo!" \
        "    }" \
        "That all."
}

Если вы хотите отказаться от совместимости с POSIX, вы можете использовать массив, чтобы избежать явного продолжения строки:

usage () {
    message=(
        "Hello, this is a cool program."
        "This should get unindented."
        "This code should stay indented:"
        "    something() {"
        "        echo It works, yo!"
        "    }"
        "That all."
    )
    printf '%s\n' "${message[@]}"
}

В следующем документе снова используется документ, но на этот раз с командой bash 4 readarray для заполнения массива. Расширение параметров заботится об удалении фиксированного числа пробелов с начала каждой ложь.

usage () {
    # No tabs necessary!
    readarray message <<'    EOF'
        Hello, this is a cool program.
        This should get unindented.
        This code should stay indented:
            something() {
                echo It works, yo!;
            }
        That all.
    EOF
    # Each line is indented an extra 8 spaces, so strip them
    printf '%s' "${message[@]#        }"
}

Один из последних вариантов: вы можете использовать расширенный шаблон для упрощения расширения параметров. Вместо того, чтобы подсчитывать, сколько пробелов используется для отступов, просто закончите отступ выбранным непространственным символом, а затем сопоставьте фиксированный префикс. Я использую :. (Пространство, следующее двоеточие - для удобочитаемости; его можно отбросить с незначительным изменением шаблона префикса.)

(Кроме того, в качестве недостатка, один из недостатков вашего замечательного трюка использования разделителя здесь-документа, начинающегося с пробела, заключается в том, что он мешает вам выполнять расширения внутри этого документа. Если вы хотите сделать это, вы 'd необходимо либо оставить разделитель незакрепленным, либо сделать одно небольшое исключение из вашего правила без табуляции, а использовать <<-EOF и разделитель закрытия с табуреткой.)

usage () {
    # No tabs necessary!
    closing="That all"
    readarray message <<EOF
       : Hello, this is a cool program.
       : This should get unindented.
       : This code should stay indented:
       :      something() {
       :          echo It works, yo!;
       :      }
       : $closing
EOF
    shopt -s extglob
    printf '%s' "${message[@]#+( ): }"
    shopt -u extglob
}

Ответ 2

geta() {
  local _ref=$1
  local -a _lines
  local _i
  local _leading_whitespace
  local _len

  IFS=$'\n' read -rd '' -a _lines ||:
  _leading_whitespace=${_lines[0]%%[^[:space:]]*}
  _len=${#_leading_whitespace}
  for _i in "${!_lines[@]}"; do
    printf -v "$_ref"[$_i] '%s' "${_lines[$_i]:$_len}"
  done
}

gets() {
  local _ref=$1
  local -a _result
  local IFS

  geta _result
  IFS=$'\n'
  printf -v "$_ref" '%s' "${_result[*]}"
}

Это немного другой подход, который требует Bash 4.1 из-за того, что printf присваивается элементам массива. (для предыдущих версий замените функцию geta ниже). Он имеет дело с произвольными ведущими пробелами, а не только с заранее определенной суммой.

Первая функция, geta, считывает из stdin, strips ведущее пробелы и возвращает результат в массиве, имя которого было передано.

Вторая, gets, делает то же самое, что и geta, но возвращает единственную строку с новыми строками (кроме последнего).

Если вы передадите имя существующей переменной в geta, убедитесь, что она уже пуста.

Вызвать geta следующим образом:

$ geta hello <<'EOS'
>    hello
>    there
>EOS
$ declare -p hello
declare -a hello='([0]="hello" [1]="there")'

gets:

$ unset -v hello
$ gets hello <<'EOS'
>     hello
>     there
> EOS
$ declare -p hello
declare -- hello="hello
there"

Этот подход должен работать для любой комбинации ведущих символов пробелов, если они являются одинаковыми символами для всех последующих строк. Функция пропускает одинаковое количество символов с фронта каждой строки, в зависимости от количества ведущих символов пробелов в первой строке.

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

Для использования в функции OP:

gets usage_message <<'EOS'
    Hello, this is a cool program.
    This should get unindented.
    This code should stay indented:
        something() {
            echo It works, yo!;
        }
    That all.
EOS

usage() {
    printf '%s\n' "$usage_message"
}

Как уже упоминалось, для Bash старше 4.1:

geta() {
  local _ref=$1
  local -a _lines
  local _i
  local _leading_whitespace
  local _len

  IFS=$'\n' read -rd '' -a _lines ||:
  _leading_whitespace=${_lines[0]%%[^[:space:]]*}
  _len=${#_leading_whitespace}
  for _i in "${!_lines[@]}"; do
    eval "$(printf '%s+=( "%s" )' "$_ref" "${_lines[$_i]:$_len}")"
  done
}