оболонка: зберігайте сліди нових рядків ('\ 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
Newline є частиною $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.

rc / es / akanaga

Для повноти зауважте, що rc/ es/ для akangaцього є оператор. У них підстановка команд, виражена у вигляді `cmd(або `{cmd}для більш складних команд), повертає список (шляхом розділення на $ifsпробіл табл-новий рядок за замовчуванням). У цих оболонках (на відміну від Борн-подібних снарядів) зачистка нової лінії проводиться лише як частина цього $ifsрозщеплення. Таким чином, ви можете або порожній, $ifsабо використовувати ``(seps){cmd}форму, де ви вказали роздільники:

ifs = ''; output = `cmd

або:

output = ``()cmd

У будь-якому випадку статус виходу команди втрачається. Вам потрібно буде вбудувати його у висновок та витягти його згодом, що стане некрасивим.

риба

У рибі підміна команд відбувається з (cmd)і не включає підпрограму.

set var (cmd)

Створює $varмасив з усіх рядків у висновку, cmdякщо $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 збереження нових рядків у підстановці команд `...`

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


Спасибі - і особливо за підказку на різних графіках. Якщо zshможна зберігати NULзмінну, чому б це не IFS= read -rd '' output < <(cmd)працювало? Потрібно вміти зберігати довжину рядка ... чи кодує це ''як 1-байтовий рядок, \0а не 0-байтовий рядок?
Том Хейл

1
@TomHale, так, read -d ''трактується як read -d $'\0'bashтому $'\0'ж випадку, як і ''скрізь).
Стефан Шазелас

Ви поєднуєте символи та байти. Будь ласка, розумійте, що якщо ми видалимо саме те, що було додано, оригінальна сутність не повинна змінюватися. Видалити один байт, який називається, це не важко, xякщо саме це було додано. Будь ласка, подивіться на мою відредаговану відповідь.
Ісаак

@Arrow, так, var=value command evalфокус обговорювався тут ( також ) і в списку розсилки австинської групи раніше. Ви побачите, що він не є портативним (і це цілком очевидно, коли ви намагаєтеся такі речі, a=1 command eval 'unset a; a=2'або ще гірше, що його не передбачали використовувати таким чином). Те саме, savedVAR=$VAR;...;VAR=$savedVARщо не робить те, що ви хочете, коли $VARспочатку не було встановлено. Якщо це вирішує лише теоретичну проблему (помилка, яку неможливо вдарити на практиці), IMO, турбуватися не варто. Все-таки я підтримаю вас у спробі.
Стефан Шазелас

Чи є у вас посилання на те, де ви відмовились і остаточно відмовились від використання LANG=Cдля видалення байта з рядка? Ви викликаєте занепокоєння навколо реальної точки, вирішити все легко. (1) не використовується використане скидання (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. Дозволяє вибрати будь-який із них, можливо, x (дійсно байт значення 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

Але не ksh чи zsh.

Однак це дуже легко вирішити, давайте скажемо всім цих оболонок зробити видалення байтів:

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

це все, всі перевірені оболонки працюють (крім яшму) (для останньої частини рядка):

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 характер, який точно один байт для всіх значень байтів від 0x00до 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(або вдатися до особливостей, що не стосуються POSIX local) 3 - зміна локалі посередині через сценарій не підтримується повністю в деяких оболонках, як яш. З іншого боку, на практиці .ніколи не виникає проблем у графіках реального життя, тому його використання дозволяє уникнути змішування з 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 рішення.


Виходячи з відповідей, я бачу, що моє питання було незрозумілим. Я просто оновив його.
Том Хейл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.