shell: оставляйте завершающие символы новой строки ('\ n') в подстановке команд

14

Я хочу быть в состоянии захватить точный вывод подстановки команды, включая завершающие символы новой строки .

Я понимаю, что они удалены по умолчанию, поэтому для их сохранения могут потребоваться некоторые манипуляции, и я хочу сохранить исходный код выхода .

Например, дана команда с переменным числом завершающих строк новой строки и кодом выхода:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

Я хочу запустить что-то вроде:

exact_output f

И иметь вывод:

Output: $'\n\n'
Exit: 5

Я заинтересован в обоих bashи POSIX sh.

Том Хейл
источник
1
Новая строка является частью $IFS, поэтому она не будет использоваться в качестве аргумента.
Deathgrip
4
@Deathgrip Это не имеет ничего общего с IFS(попробуйте ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). Только символы новой строки удаляются. \tИ `` не делают, и IFSне влияют на это.
PSkocik

Ответы:

17

POSIX снаряды

Обычный ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) трюк для получения полного вывода команды состоит в следующем:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

Идея состоит в том, чтобы добавить и доп .\n. Подстановка команд только лишит этого \n . И ты раздеваешь .с ${output%.}.

Обратите внимание, что в других оболочках это не zshбудет работать, если выходные данные имеют байты NUL. С yashэтим не сработает, если вывод не текстовый.

Также обратите внимание, что в некоторых локалях важно, какой символ вы используете для вставки в конце. .как правило, должно быть хорошо, но некоторые другие не могут. Например x(как используется в некоторых других ответах) или @не будет работать в локали, использующей кодировки BIG5, GB18030 или BIG5HKSCS. В этих кодировках кодировка ряда символов заканчивается тем же байтом, что и кодировка xили @(0x78, 0x40)

Например, ūв BIG5HKSCS 0x88 0x78 (и x0x78, как в ASCII, все кодировки в системе должны иметь одинаковую кодировку для всех символов переносимого набора символов, который включает английские буквы, @и .). Так что, если cmdбыло printf '\x88'и мы вставили xпосле него, ${output%x}будет не в состоянии раздеться , что , xкак $outputбы на самом деле содержат ū.

Использование .вместо этого может привести к той же проблеме в теории, если есть какие-либо символы, кодировка которых заканчивается в той же кодировке, что и ., но для проверки некоторое время назад, я могу сказать, что ни одна из кодировок, которые могут быть доступны для использования в локали в в системах Debian, FreeBSD или Solaris есть такие символы, которые мне достаточно хороши (и почему я остановился на том, .что также является символом, обозначающим конец предложения на английском языке, поэтому кажется уместным).

Более правильный подход, как обсуждал @Arrow, состоял бы в том, чтобы изменить языковой стандарт на C только для удаления последнего символа ( ${output%.}), который обеспечил бы удаление только одного байта, но это значительно усложнило бы код и потенциально привело бы к проблемам совместимости свой.

альтернативы bash / zsh

С bashи zsh, предполагая, что вывод не имеет NUL, вы также можете сделать:

IFS= read -rd '' output < <(cmd)

Для того, чтобы получить статус выхода cmd, вы можете сделать wait "$!"; ret=$?в , bashно не в zsh.

гс / эс / akanaga

Для полноты, заметим , что rc/ es/ akangaесть оператор для этого. В них подстановка команд, выраженная как `cmd(или `{cmd}для более сложных команд), возвращает список (путем разделения на $ifsspace-tab-newline по умолчанию). В этих оболочках (в отличие от оболочек типа Борна) удаление новой строки выполняется только как часть этого $ifsразделения. Таким образом, вы можете либо очистить, $ifsлибо использовать ``(seps){cmd}форму, в которой вы указываете разделители:

ifs = ''; output = `cmd

или:

output = ``()cmd

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

рыбы

В рыбе подстановка команд выполняется с (cmd)использованием необолочки.

set var (cmd)

Создает $varмассив со всеми строками в выходных данных cmdif, не $IFSявляется пустым, или с выводом cmdразделенных до одного (в отличие от всех в большинстве других оболочек) символа новой строки, если $IFSон пуст.

Так что есть еще проблема в этом (printf 'a\nb')и (printf 'a\nb\n')расширяться до того же, даже с пустым $IFS.

Чтобы обойти это, лучшее, что я мог придумать, было:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

Альтернатива состоит в том, чтобы сделать:

read -z output < (begin; cmd; set ret $status; end | psub)

Оболочка Борна

Оболочка Bourne не поддерживала $(...)ни форму, ни ${var%pattern}оператора, поэтому ее может быть довольно сложно достичь. Один из подходов заключается в использовании eval и цитирования:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

Здесь мы генерируем

output='output of cmd
with the single quotes escaped as '\''
';ret=X

быть переданным eval. Что касается подхода POSIX, если 'был один из тех характеров, кодирование может быть найдена в конце других персонажей, мы должны были бы проблемы (гораздо хуже , так как он станет инъекцией команды уязвимостью), но , к счастью, как ., это не одна из тех, и эта техника цитирования, как правило, используется любой, которая заключает в кавычки шелл-код (обратите внимание, что \есть проблема, поэтому ее не следует использовать (кроме тех случаев, "..."внутри которых необходимо использовать обратную косую черту для некоторых символов) Здесь мы используем его только после того, как 'все в порядке).

Tcsh

Смотрите, что tcsh сохраняет новые строки в подстановке команд `...`

(без учета состояния выхода, к которому можно обратиться, сохранив его во временном файле ( echo $status > $tempfile:qпосле команды))

Стефан Шазелас
источник
Спасибо - и особенно за подсказку по различным кодировкам. Если zshможете хранить NULв переменной, почему бы не IFS= read -rd '' output < <(cmd)работать? Он должен иметь возможность хранить длину строки ... она кодируется ''как 1-байтовая строка, \0а не 0-байтовая строка?
Том Хейл,
1
@TomHale, да, read -d ''рассматривается как read -d $'\0'( bashхотя, $'\0'как и ''везде).
Стефан
Вы объединяете символы и байты. Пожалуйста, поймите, что если мы удалим именно то, что было добавлено, исходная сущность не должна измениться. Нетрудно удалить один вызванный байт,x если он был добавлен. Пожалуйста, посмотрите на мой отредактированный ответ.
Исаак
@ Стрелка, да, var=value command evalуловка обсуждалась здесь ( также ) и в списке рассылки Austin-Group ранее. Вы обнаружите, что он не переносимый (и когда вы пытаетесь сделать что-то вроде того a=1 command eval 'unset a; a=2'или хуже, совершенно очевидно, что он не предназначен для такого использования). То же самое для того, savedVAR=$VAR;...;VAR=$savedVARчто не делает то, что вы хотите, когда $VARбыл изначально не установлен. Если это обойти только теоретическую проблему (ошибка, которую невозможно устранить на практике), IMO, это не стоит беспокоиться. Тем не менее, я поддержу вас за попытку.
Стефан
У вас есть ссылка на то, где вы отказались и наконец отказались от использования LANG=Cдля удаления байта из строки? Вы поднимаете проблемы вокруг реальной точки зрения, все легко решить. (1) не используется unset (2) Проверьте переменную перед ее изменением. @ StéphaneChazelas
Исаак
3

Для нового вопроса этот скрипт работает:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

По исполнению:

Output:$'\n\n\n'
Exit :25
Done

Длинное описание

Обычная мудрость для оболочек POSIX для удаления \n:

добавить x

s=$(printf "%s" "${1}x"); s=${s%?}

Это необходимо, потому что последняя новая строка ( S ) удаляется расширением команды согласно спецификации POSIX :

удаление последовательностей из одного или нескольких символов в конце замены.


О трейлинге x.

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

Тем не мение; Это просто неверно .

Единственное правило, которому мы должны следовать, это добавлять именно то , что мы удаляем.

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

Куда мы пойдем не так? Когда мы смешиваем символы и байты .

Если мы добавляем байт, мы должны удалить байт, если мы добавляем символ, мы должны удалить точно такой же символ .

Второй вариант, добавление символа (а затем удаление точно такого же символа) может стать запутанным и сложным, и, да, кодовые страницы и кодировки могут помешать.

Однако первый вариант вполне возможен, и, после его объяснения, он станет простым.

Давайте добавим байт, байт ASCII (<127), и, чтобы сделать вещи как можно менее запутанными, скажем, символ ASCII в диапазоне az. Или, как мы должны сказать, байт в шестнадцатеричном диапазоне 0x61- 0x7a. Позволяет выбрать любой из них, возможно, х (на самом деле это байт значения 0x78). Мы можем добавить такой байт с помощью конкатенации x к строке (предположим, что é):

$ a
$ b=${a}x

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

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

Последовательность строк, которая заканчивается на х.

Если мы удалим это x (значение байта 0x78), мы получим:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

Работает без проблем.

Немного более сложный пример.

Допустим, интересующая нас строка заканчивается байтом 0xc3:

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

И давайте добавим байт значения 0xa9

$ b=$a$'\xa9'

Строка теперь стала такой:

$ echo "$b"
a test string é

Именно то, что я хотел, последние два байта - это один символ в utf8 (так что любой мог воспроизвести этот результат в своей консоли utf8).

Если мы удалим символ, исходная строка будет изменена. Но это не то, что мы добавили, мы добавили байтовое значение, которое в любом случае записывается как x, а как байт.

Что нам нужно, чтобы избежать неправильной интерпретации байтов как символов. Нам нужно действие, которое удаляет использованный нами байт 0xa9. Фактически, ash, bash, lksh и mksh, похоже, делают именно это:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

Но не кш или зш.

Однако это очень легко решить, давайте скажем всем этим оболочкам выполнить удаление байтов:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

вот и все, все снаряды протестировали работу (кроме yash) (для последней части строки):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

Просто скажи оболочке удалить символ LC_ALL = C, который является ровно одним байтом для всех байтовых значений от 0x00to 0xff.

Решение для комментариев:

Для примера, обсуждаемого в комментариях, одно из возможных решений (которое не работает в zsh):

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

Это устранит проблему кодирования.

Исаак
источник
Полезно знать, что может быть удалено более одного завершающего символа новой строки.
Том Хейл,
Я согласен, что исправление языкового стандарта C для обеспечения того, чтобы ${var%?}всегда обрезать один байт, теоретически более корректно, но: 1 - LC_ALLи LC_CTYPEпереопределение $LANG, поэтому вам нужно установить LC_ALL=C2 - вы не можете сделать это var=${var%?}в подоболочке, как изменение быть потерянным, поэтому вам нужно будет сохранить и восстановить значение и состояние LC_ALL(или прибегнуть к localфункциям, не относящимся к области POSIX ). 3 - изменение локали в середине сценария не полностью поддерживается в некоторых оболочках, таких как yash. С другой стороны, на практике .никогда не бывает проблем в реальных кодировках, поэтому его использование позволяет избежать смешения с LC_ALL.
Стефан
2

Вы можете вывести символ после обычного вывода и затем удалить его:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

Это POSIX-совместимое решение.

PSkocik
источник
Судя по ответам, я вижу, что мой вопрос был неясным. Я только что обновил его.
Том Хейл,