Jak poprawnie zebrać tablicę linii w Zsh

42

Myślałem, że następujące zgrupuje dane wyjściowe my_commandw tablicy wierszy:

IFS='\n' array_of_lines=$(my_command);

tak $array_of_lines[1]by odnosiło się do pierwszego wiersza na wyjściu my_command, $array_of_lines[2]do drugiego i tak dalej.

Jednak powyższe polecenie wydaje się nie działać dobrze. Wydaje się, że dzieli także wynik my_commandwokół znaku n, jak już sprawdziłem print -l $array_of_lines, co, jak sądzę, drukuje elementy tablicy linia po linii. Sprawdziłem to również za pomocą:

echo $array_of_lines[1]
echo $array_of_lines[2]
...

W drugiej próbie pomyślałem, że dodanie evalmoże pomóc:

IFS='\n' array_of_lines=$(eval my_command);

ale mam dokładnie taki sam wynik jak bez niego.

Wreszcie, podążając za odpowiedzią dotyczącą elementów List ze spacjami w zsh , próbowałem również użyć flag rozwijania parametrów zamiast IFSpowiedzieć zsh, jak podzielić dane wejściowe i zebrać elementy w tablicę, tj .:

array_of_lines=("${(@f)$(my_command)}");

Ale nadal mam ten sam wynik (dzielenie się dzieje n)

W związku z tym mam następujące pytania:

Pytanie 1 Jakie są „właściwe” sposoby gromadzenia danych wyjściowych polecenia w szeregu wierszy?

Q2 Jak mogę określić IFSpodział tylko na nowe wiersze?

Pytanie 3 Jeśli użyję flag rozszerzania parametrów, jak w mojej trzeciej próbie powyżej (tj. Przy użyciu @f), aby określić podział, czy zsh ignoruje wartość IFS? Dlaczego to nie zadziałało powyżej?

Amelio Vazquez-Reina
źródło

Odpowiedzi:

71

TL, DR:

array_of_lines=("${(@f)$(my_command)}")

Pierwszy błąd (→ Q2): IFS='\n'ustawia IFSna dwa znaki \i n. Aby ustawić IFSnową linię, użyj IFS=$'\n'.

Drugi błąd: ustawić zmienną na wartość tablicy, trzeba nawiasów wokół elementów: array_of_lines=(foo bar).

To działałoby, z wyjątkiem tego, że usuwa puste linie, ponieważ kolejne białe znaki są liczone jako pojedynczy separator:

IFS=$'\n' array_of_lines=($(my_command))

Możesz zachować puste linie z wyjątkiem samego końca, podwajając znak białych znaków w IFS:

IFS=$'\n\n' array_of_lines=($(my_command))

Aby zachować również puste linie, musisz dodać coś do wyniku polecenia, ponieważ dzieje się tak w przypadku podstawienia polecenia, a nie jego analizy.

IFS=$'\n\n' array_of_lines=($(my_command; echo .)); unset 'array_of_lines[-1]'

(zakładając, że dane wyjściowe my_commandnie kończą się nieokreśloną linią; pamiętaj również, że tracisz status wyjścia my_command)

Pamiętaj, że wszystkie powyższe fragmenty wychodzą IFSz wartością inną niż domyślna, więc mogą zepsuć kolejny kod. Aby zachować ustawienie IFSlokalne, ustaw całą funkcję na deklarację IFSlokalną (tutaj również dbaj o zachowanie statusu wyjścia polecenia):

collect_lines() {
  local IFS=$'\n\n' ret
  array_of_lines=($("$@"; ret=$?; echo .; exit $ret))
  ret=$?
  unset 'array_of_lines[-1]'
  return $ret
}
collect_lines my_command

Ale zalecam nie zadzierać IFS; zamiast tego użyj fflagi rozwijania, aby podzielić na nowe linie (→ Q1):

array_of_lines=("${(@f)$(my_command)}")

Lub, aby zachować końcowe puste linie:

array_of_lines=("${(@f)$(my_command; echo .)}")
unset 'array_of_lines[-1]'

Wartość IFSnie ma znaczenia. Podejrzewam, że użyłeś polecenia, które dzieli na IFSdrukowanie $array_of_linesw badaniach (→ Q3).

Gilles „SO- przestań być zły”
źródło
7
To takie skomplikowane! "${(@f)...}"jest taki sam jak ${(f)"..."}, ale w inny sposób. (@)wewnątrz podwójnych cudzysłowów oznacza „wydaj jedno słowo na element tablicy” i (f)„podziel się na tablicę znakiem nowej linii”. PS: Proszę zamieścić link do dokumentacji
latające owce
3
@flyingsheep, no ${(f)"..."}pomija puste linie, "${(@f)...}"zachowuje je. To jest to samo rozróżnienie między $argvi "$argv[@]". Ta "$@"rzecz polegająca na zachowaniu wszystkich elementów tablicy pochodzi z powłoki Bourne'a pod koniec lat 70.
Stéphane Chazelas
4

Dwa problemy: po pierwsze, najwyraźniej podwójne cudzysłowy również nie interpretują znaków ukośnika odwrotnego (przepraszam za to :). Użyj $'...'cytatów. I zgodnie z tym man zshparam, aby zebrać słowa w tablicy, musisz zawrzeć je w nawiasach. To działa:

% touch 'a b' c d 'e f'
% IFS=$'\n' arr=($(ls)); print -l $arr
a b
c
d
e f
% print $arr[1]
a b

Nie mogę odpowiedzieć na twoje pytanie 3. Mam nadzieję, że nigdy nie będę musiał wiedzieć tak ezoterycznych rzeczy :).

angus
źródło
-2

Możesz także użyć tr, aby zastąpić znak nowej linii spacją:

lines=($(mycommand | tr '\n' ' '))
select line in ("${lines[@]}"); do
  echo "$line"
  break
done
Jimchao
źródło
3
co jeśli linie zawierają spacje?
don_crissti
2
To nie ma sensu. Zarówno SPC, jak i NL mają domyślną wartość $IFS. Tłumaczenie jednego na drugi nie ma znaczenia.
Stéphane Chazelas
Czy moje zmiany były rozsądne? Nie mogłem sprawić, żeby działał tak, jak był
John P
(upłynął limit czasu) Przyznaję, że zredagowałem go, ale tak naprawdę nie rozumiem, o co chodzi, ale myślę, że tłumaczenie to dobry początek separacji opartej na manipulacji ciągami. Nie tłumaczyłbym na spacje i nie podzieliłbym ich, chyba że oczekiwane zachowanie było bardziej podobne echo, gdzie wkładem są mniej więcej stosy słów oddzielone tym, kogo to obchodzi.
John P