shell: zachowaj końcowe znaki nowej linii ('\ n') w zastępstwie poleceń


14

Chcę być w stanie uchwycić dokładny wynik podstawienia polecenia, w tym końcowe znaki nowego wiersza .

Zdaję sobie sprawę, że są one domyślnie usuwane, więc może być wymagana pewna manipulacja, aby je zachować, i chcę zachować oryginalny kod wyjścia .

Na przykład, biorąc pod uwagę polecenie ze zmienną liczbą końcowych znaków nowej linii i kodem wyjścia:

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

Chcę uruchomić coś takiego:

exact_output f

I niech wynik będzie:

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

Interesuje mnie zarówno bashPOSIX sh.


1
Newline jest częścią $IFS, więc nie zostanie przechwycony jako argument.
Deathgrip,

4
@Deathgrip Nie ma to nic wspólnego IFS(spróbuj ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). Tylko nowe linie są usuwane. \tI `` nie, i IFSnie ma na to wpływu.
PSkocik



Odpowiedzi:


17

Pociski POSIX

Zwykłą ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) sztuczką, aby uzyskać kompletny krok polecenia, jest:

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

Chodzi o to, aby dodać i dodatkowo .\n. Zastąpienie polecenia spowoduje tylko usunięcie tego \n . I rozebrać .z ${output%.}.

Zauważ, że w powłokach innych niż zsh, to nadal nie będzie działać, jeśli wyjście ma NUL bajtów. Dzięki yashnie będzie to działać, jeśli wynik nie jest tekstem.

Pamiętaj też, że w niektórych lokalizacjach ma znaczenie to, jakiego znaku użyjesz na końcu. .ogólnie powinno być w porządku, ale niektóre inne mogą nie. Na przykład x(jak w niektórych innych odpowiedziach) lub @nie działałby w ustawieniach regionalnych przy użyciu zestawów znaków BIG5, GB18030 lub BIG5HKSCS. W tych zestawach znaków kodowanie wielu znaków kończy się tym samym bajtem, co kodowanie xlub @(0x78, 0x40)

Na przykład ūw BIG5HKSCS wynosi 0x88 0x78 (i x0x78 jak w ASCII, wszystkie zestawy znaków w systemie muszą mieć to samo kodowanie dla wszystkich znaków przenośnego zestawu znaków zawierającego litery angielskie @i .). Gdyby tak cmdbyło printf '\x88'i wstawiliśmy xpo nim, ${output%x}nie usunęłoby tego, xco $outputfaktycznie zawierałoby ū.

Używanie .zamiast tego może prowadzić do tego samego problemu teoretycznie, jeśli byłyby jakieś znaki, których kodowanie kończy się takim samym kodowaniem jak ., ale po sprawdzeniu jakiś czas temu mogę powiedzieć, że żaden z zestawów znaków, które mogą być dostępne do użycia w ustawieniach regionalnych w Systemy Debian, FreeBSD lub Solaris mają takie znaki, które są dla mnie wystarczająco dobre (i dlaczego zdecydowałem się na .to, że jest to również symbol oznaczający koniec zdania w języku angielskim, więc wydaje się to właściwe).

Bardziej poprawnym podejściem omawianym przez @Arrow byłaby zmiana ustawień regionalnych na C tylko dla usuwania ostatniego znaku ( ${output%.}), co zapewniłoby usunięcie tylko jednego bajtu, ale to znacznie skomplikowałoby kod i potencjalnie wprowadziłoby problemy z kompatybilnością jego.

alternatywy bash / zsh

Za pomocą bashi zsh, zakładając, że dane wyjściowe nie mają wartości NUL, możesz także:

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

Aby uzyskać status wyjścia cmd, możesz zrobić wait "$!"; ret=$?w, bashale nie w zsh.

rc / es / akanaga

Dla kompletności zwróć uwagę, że rc/ es/ akangamają do tego operatora. W nich podstawienie polecenia wyrażone jako `cmd(lub w `{cmd}przypadku bardziej złożonych poleceń) zwraca listę ( $ifsdomyślnie dzieląc klawisz spacja-tab-nowa linia). W tych powłokach (w przeciwieństwie do powłok podobnych do Bourne'a) usuwanie nowej linii odbywa się tylko w ramach tego $ifspodziału. Możesz więc opróżnić $ifslub użyć ``(seps){cmd}formularza, w którym określasz separatory:

ifs = ''; output = `cmd

lub:

output = ``()cmd

W każdym razie status wyjścia polecenia zostanie utracony. Będziesz musiał osadzić go w wyjściu i wyodrębnić później, co stałoby się brzydkie.

ryba

W przypadku ryb zastępowanie poleceń odbywa się za (cmd)pomocą podpowłoki.

set var (cmd)

Tworzy $vartablicę ze wszystkimi wierszami na wyjściu cmdif $IFSjest niepustym lub z wynikiem cmdpozbawionym do jednego (w przeciwieństwie do wszystkich w większości innych powłok) znaku nowej linii, jeśli $IFSjest pusty.

Więc wciąż jest w tym problem (printf 'a\nb')i (printf 'a\nb\n')rozwinąć się do tego samego nawet z pustym $IFS.

Aby obejść ten problem, najlepiej wymyślić:

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

Alternatywą jest wykonanie:

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

Skorupa Bourne'a

Powłoka Bourne'a nie obsługiwała $(...)ani formy, ani ${var%pattern}operatora, więc może być tam dość trudno go osiągnąć. Jednym z podejść jest użycie eval i cytowania:

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\"
  `"

Tutaj generujemy

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

zostać przekazanym eval. Jeśli chodzi o podejście POSIX, jeśli 'byłby to jeden z tych znaków, którego kodowanie można znaleźć na końcu innych znaków, mielibyśmy problem (znacznie gorszy, ponieważ stałby się podatny na iniekcję poleceń), ale na szczęście, jak .: to nie jest jedna z nich, a ta technika cytowania jest generalnie stosowana przez wszystko, co cytuje kod powłoki (uwaga, że \ma problem, więc nie powinna być używana (wyklucza również "..."wewnątrz, w których trzeba używać odwrotnych ukośników dla niektórych znaków) Tutaj używamy go tylko po tym, 'co jest w porządku).

tcsh

Zobacz tcsh zachowaj znaki nowej linii w podstawianiu poleceń `...`

(nie dbając o status wyjścia, który można rozwiązać, zapisując go w pliku tymczasowym ( echo $status > $tempfile:qpo poleceniu))


Dzięki - a zwłaszcza za wskazówkę dotyczącą różnych zestawów znaków. Jeśli zshmożna przechowywać NULw zmiennej, dlaczego nie miałby IFS= read -rd '' output < <(cmd)działać? Musi mieć możliwość przechowywania długości ciągu ... czy koduje go ''jako ciąg 1-bajtowy, \0a nie ciąg 0-bajtowy?
Tom Hale,

1
@TomHale, tak, read -d ''jest traktowany jako read -d $'\0'(w bashtym, że $'\0'jest tak samo jak ''wszędzie).
Stéphane Chazelas

Łączymy znaki i bajty. Proszę zrozumieć, że jeśli usuniemy dokładnie to, co zostało dodane, oryginalny podmiot nie może się zmienić. Usunięcie jednego bajtu o nazwie, xjeśli tak zostało dodane, nie jest trudne . Proszę spojrzeć na moją zredagowaną odpowiedź.
Izaak

@ Strzałka, tak, var=value command evalsztuczka była omawiana tutaj ( także ) i na liście mailingowej grupy austin wcześniej. Przekonasz się, że nie jest przenośny (i jest całkiem oczywiste, gdy próbujesz czegoś takiego a=1 command eval 'unset a; a=2'lub gorzej, że nie miał być tak używany). To samo dotyczy tego, savedVAR=$VAR;...;VAR=$savedVARże nie robi tego, co chcesz, gdy $VARpoczątkowo był rozbrojony. Jeśli ma to obejść tylko problem teoretyczny (błąd, którego nie można w praktyce trafić), IMO, nie warto się tym przejmować. Mimo to będę cię wspierać za próbę.
Stéphane Chazelas

Czy masz link do miejsca, w którym usunąłeś i ostatecznie odrzuciłeś użycie LANG=Cdo usunięcia bajtu z ciągu? Podnosisz obawy wokół rzeczywistego punktu, wszystkie są łatwe do rozwiązania. (1) nie zastosowano nieuzbrojonego (2) Przetestuj zmienną przed jej zmianą. @ StéphaneChazelas
Isaac

3

W przypadku nowego pytania skrypt działa:

#!/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

Po wykonaniu:

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

Dłuższy opis

Zwykłą mądrością dla powłok POSIX do radzenia sobie z usuwaniem \njest:

dodaj x

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

Jest to wymagane, ponieważ ostatnia nowa linia ( S ) jest usuwana przez rozszerzenie komend zgodnie ze specyfikacją POSIX :

usuwanie sekwencji jednego lub więcej znaków na końcu podstawienia.


O wleczeniu x.

W tym pytaniu powiedziano, że xmożna pomylić z końcowym bajtem jakiegoś znaku w niektórych kodowaniach. Ale jak zgadniemy, która lub która postać jest lepsza w jakimś języku w jakimś możliwym kodowaniu, co jest co najmniej trudną propozycją.

Jednak; To jest po prostu nieprawidłowe .

Jedyną zasadą, którą musimy przestrzegać, jest dodawanie dokładnie tego , co usuwamy.

Powinno być łatwe do zrozumienia, że ​​jeśli dodamy coś do istniejącego ciągu (lub sekwencji bajtów), a później usuniemy dokładnie to samo, oryginalny ciąg (lub sekwencja bajtów) musi być taki sam.

Gdzie popełniamy błąd? Kiedy mieszamy znaki i bajty .

Jeśli dodamy bajt, musimy usunąć bajt, jeśli dodamy znak, musimy usunąć dokładnie ten sam znak .

Druga opcja, dodawanie znaku (a później usunięcie dokładnie tego samego znaku) może stać się skomplikowane i złożone, i tak, strony kodowe i kodowanie mogą przeszkadzać.

Jednak pierwsza opcja jest całkiem możliwa, a po jej wyjaśnieniu stanie się po prostu prosta.

Dodajmy bajt, bajt ASCII (<127), i aby zachować jak najmniej skomplikowaną sytuację, powiedzmy znak ASCII w zakresie az. Albo jak należy go mówiąc, bajt w zakresie hex 0x61- 0x7a. Wybierzmy dowolny z nich, może x (naprawdę bajt wartości 0x78). Możemy dodać taki bajt, łącząc x z ciągiem (załóżmy, że é):

$ a
$ b=${a}x

Jeśli spojrzymy na ciąg jako sekwencję bajtów, zobaczymy:

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

Ciąg znaków kończący się na x.

Jeśli usuniemy ten x (wartość bajtu 0x78), otrzymamy:

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

Działa bez problemu.

Trochę trudniejszy przykład.

Powiedzmy, że ciąg, który nas interesuje, kończy się bajtem 0xc3:

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

I dodajmy bajt wartości 0xa9

$ b=$a$'\xa9'

Ciąg stał się teraz taki:

$ echo "$b"
a test string é

Dokładnie to, czego chciałem, ostatnie dwa bajty to jeden znak w utf8 (aby każdy mógł odtworzyć te wyniki w swojej konsoli utf8).

Jeśli usuniemy znak, oryginalny ciąg zostanie zmieniony. Ale to nie to, co dodaliśmy, dodaliśmy wartość bajtu, która przypadkowo jest zapisywana jako x, ale bajt i tak.

Czego potrzebujemy, aby uniknąć błędnej interpretacji bajtów jako znaków. Potrzebujemy działania, które usuwa użyty bajt 0xa9. W rzeczywistości ash, bash, lksh i mksh wydają się robić dokładnie to:

$ 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

Ale nie ksh ani zsh.

Jest to jednak bardzo łatwe do rozwiązania, powiedzmy wszystkim tym powłokom, aby usunęły bajty:

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

to wszystko, wszystkie testowane powłoki działają (oprócz yash) (dla ostatniej części łańcucha):

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

Po prostu powiedz powłoce, aby usunęła znak LC_ALL = C, który jest dokładnie jednym bajtem dla wszystkich wartości bajtów od 0x00do 0xff.

Rozwiązanie dla komentarzy:

Dla przykładu omówionego w komentarzach jednym z możliwych rozwiązań (które nie działa w Zsh) jest:

#!/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

To usunie problem z kodowaniem.


Dobrze wiedzieć, że można usunąć więcej niż jeden końcowy znak nowej linii.
Tom Hale,


Zgadzam się, że ustalenie ustawienia narodowego na C, aby mieć pewność, że ${var%?}zawsze usunie jeden bajt, jest bardziej poprawne teoretycznie, ale: 1 LC_ALLi LC_CTYPEzastąp $LANG, więc musisz ustawić LC_ALL=C2, nie możesz zrobić var=${var%?}w podpowłoce, ponieważ zmiana zgubić się, więc trzeba zapisać i przywrócić wartość i stan LC_ALL(lub uciekać się do funkcji spoza localzakresu POSIX ) 3- zmiana ustawień regionalnych w połowie skryptu nie jest w pełni obsługiwana w niektórych powłokach, takich jak yash. Z drugiej strony w praktyce .nigdy nie stanowi problemu w prawdziwych zestawach znaków, więc używanie go pozwala uniknąć mieszania się z LC_ALL.
Stéphane Chazelas,

2

Możesz wypisać znak po normalnym wyjściu, a następnie usunąć go:

#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}
}

Jest to rozwiązanie zgodne z POSIX.


Na podstawie odpowiedzi widzę, że moje pytanie było niejasne. Właśnie to zaktualizowałem.
Tom Hale,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.