Jak oddzielić dane wyjściowe polecenia do poszczególnych linii

12
list=`ls -a R*`
echo $list

Wewnątrz skryptu powłoki to polecenie echo wyświetli wszystkie pliki z bieżącego katalogu zaczynające się na R, ale w jednym wierszu. Jak mogę wydrukować każdy element w jednym wierszu?

Muszę rodzajowe polecenia dla wszystkich scenariuszy dzieje z ls, du, find -type -d, itd.

Anony
źródło
6
Uwaga ogólna: nie rób tego z ls, spowoduje to uszkodzenie dowolnych dziwnych nazw plików . Zobacz tutaj .
terdon

Odpowiedzi:

12

Jeśli dane wyjściowe polecenia zawierają wiele wierszy, wówczas podaj zmienne, aby zachować te nowe wiersze podczas echa:

echo "$list"

W przeciwnym razie powłoka rozszerzy się i podzieli zawartość zmiennej na białe znaki (spacje, znaki nowej linii, tabulatory) i zostaną utracone.

muru
źródło
15

Zamiast niewłaściwie umieszczać lsdane wyjściowe w zmiennej, a następnie echoje przetwarzać, co usuwa wszystkie kolory, użyj

ls -a1

Od man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

Nie radzę nic robić z wyjściem ls, z wyjątkiem wyświetlania :)

Użyj na przykład globusów powłoki i forpętli, aby zrobić coś z plikami ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* To nie będzie zawierała bieżącego katalogu .lub jego rodzica ..choć

Zanna
źródło
1
Polecam wymienić: echo "$i"z printf "%s\n" "$i" (że nie będzie dusić jeśli nazwa pliku zaczyna się od „-”). Właściwie większość czasu echo somethingnależy zastąpić printf "%s\n" "something".
Olivier Dulac
2
@OlivierDulac Wszystkie zaczynają się Rtutaj, ale ogólnie tak. Jednak nawet dla skryptów, próbując zawsze pomieścić wiodącym -w ścieżkach powoduje kłopoty: ludzie zakładają --, które obsługują tylko niektóre polecenia, oznacza koniec opcji. Widziałem, find . [tests] -exec [cmd] -- {} \;gdzie [cmd]nie obsługuje --. W .każdym razie każda ścieżka zaczyna się od ! Ale to nie jest argument przeciwko zastąpieniu echogo printf '%s\n'. Tutaj można zastąpić całą pętlę printf '%s\n' R/*. Bash ma printfwbudowane narzędzie, więc nie ma ograniczenia liczby / długości argumentów.
Eliah Kagan
12

Chociaż umieszczanie go w cudzysłowie, jak sugeruje @muru , rzeczywiście zrobi to, o co prosiłeś, możesz również rozważyć użycie tablicy do tego. Na przykład:

IFS=$'\n' dirs=( $(find . -type d) )

IFS=$'\n'Mówi bash tylko podzielić wyjście na nowej linii characcters o dostać każdy element tablicy. Bez tego podzieli się na spacje, więc a file name with spaces.txtbędzie 5 osobnych elementów zamiast jednego. Takie podejście zostanie jednak przerwane, jeśli nazwy plików / katalogów mogą zawierać znaki nowej linii ( \n). Zapisuje każdy wiersz wyniku polecenia jako element tablicy.

Zauważ, że zmieniłem również stary styl, `command`na $(command)który jest preferowaną składnią.

Masz teraz tablicę o nazwie $dirs, każdy bof, którego elementy są wierszem wyjścia poprzedniego polecenia. Na przykład:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Teraz, z powodu pewnej dziwności bashu (patrz tutaj ), IFSpo wykonaniu tej czynności konieczne będzie przywrócenie pierwotnej wartości. Więc albo zapisz to i ponownie przypisz:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

Lub zrób to ręcznie:

IFS=" "$'\t\n '

Alternatywnie, po prostu zamknij obecny terminal. Twój nowy będzie miał ponownie ustawiony oryginalny IFS.

terdon
źródło
Mogłem zasugerować coś takiego, ale część o dutym, jak wygląda OP, jest bardziej zainteresowana zachowaniem formatu wyjściowego.
muru
Jak mogę to zrobić, jeśli nazwy katalogów zawierają spacje?
deser
@dessert whoops! Powinien teraz działać ze spacjami (ale nie nowymi liniami). Dzięki za zwrócenie na to uwagi.
terdon
1
Pamiętaj, że powinieneś zresetować lub rozbroić IFS po tym przypisaniu tablicy. unix.stackexchange.com/q/264635/70524
mur
@muru oh wow, dzięki, zapomniałem o tej dziwności.
terdon
2

Jeśli musisz przechowywać dane wyjściowe i chcesz mieć tablicę, mapfileułatwia to.

Najpierw zastanów się, czy w ogóle musisz przechowywać dane wyjściowe polecenia. Jeśli nie, po prostu uruchom polecenie.

Jeśli zdecydujesz, że chcesz odczytać wynik polecenia jako tablicę wierszy, prawdą jest, że jednym ze sposobów jest wyłączenie globowania, ustawienie IFSpodziału na linie, stosowanie podstawiania poleceń w ( )składni tworzenia tablicy, a IFSnastępnie resetowanie , co jest bardziej złożone niż się wydaje. Odpowiedź terdona obejmuje niektóre z tych podejść. Ale proponuję użyć zamiast tego wbudowanego polecenia mapfilepowłoki Bash do odczytu tekstu jako tablicy wierszy. Oto prosty przypadek, w którym czytasz z pliku:

mapfile < filename

To odczytuje wiersze do tablicy o nazwie MAPFILE. Bez przekierowania wejścia czytałbyś ze standardowego wejścia powłoki (zwykle z terminala) zamiast z pliku o nazwie . Aby wczytać tablicę inną niż domyślna , przekaż jej nazwę. Na przykład wczytuje to do tablicy :< filenamefilenameMAPFILElines

mapfile lines < filename

Innym domyślnym zachowaniem, które możesz zmienić, jest pozostawienie znaków nowej linii na końcach linii; pojawiają się jako ostatni znak w każdym elemencie tablicy (chyba że dane wejściowe zakończyły się bez znaku nowej linii, w którym to przypadku ostatni element nie ma). Aby przełamać te znaki nowej linii, aby nie pojawiły się w tablicy, przekaż -topcję do mapfile. Na przykład odczytuje to tablicę recordsi nie zapisuje końcowych znaków nowego wiersza do jej elementów tablicy:

mapfile -t records < filename

Możesz także używać -tbez przekazywania nazwy tablicy; to znaczy, działa również z niejawną nazwą tablicy MAPFILE.

mapfilePowłoka wbudowane podpór innych opcji, a to może alternatywnie być powoływane jako readarray. Uruchom help mapfile(i help readarray), aby uzyskać szczegółowe informacje.

Ale nie chcesz czytać z pliku, chcesz czytać z wyniku polecenia. Aby to osiągnąć, użyj substytucji procesu . Ta komenda czyta wiersze z komendy some-commandz arguments...jak jego argumenty wiersza polecenia i miejsc w nich w mapfile„s domyślnej tablicy MAPFILE, z ich zakończeń znaków nowej linii usunięte:

mapfile -t < <(some-command arguments...)

Podstawienie procesu zastępowane jest rzeczywistą nazwą pliku, z którego można odczytać wynik działania. Plik jest nazwanym potokiem, a nie zwykłym plikiem, a na Ubuntu zostanie nazwany jak (czasem z inną liczbą niż ), ale nie musisz zajmować się szczegółami, ponieważ dba o to powłoka wszystko za kulisami.<(some-command arguments...)some-command arguments.../dev/fd/6363

Możesz pomyśleć, że możesz zrezygnować z zastępowania procesów za pomocą zamiast tego, ale to nie zadziała, ponieważ gdy masz wiele potoków oddzielonych , Bash uruchamia wszystkie polecenia w podpowłokach . Zatem zarówno i uruchomić w swoich środowiskach , ale z zainicjowany oddzielić od środowiska powłoki, w którym uruchomiono gazociąg. W podpowłoce, w której działa, tablica jest zapełniana, ale tablica ta jest odrzucana po zakończeniu polecenia. nigdy nie jest tworzony ani modyfikowany dla osoby dzwoniącej.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Oto jak wygląda przykład w odpowiedzi terdona , jeśli użyjesz mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Otóż ​​to. Nie musisz sprawdzać, czy IFSzostał ustawiony , śledzić, czy nie został ustawiony i jaką wartością ustawić nową linię, a następnie zresetować lub zresetować później. Nie trzeba wyłączać masek (na przykład, z set -f) - co jest naprawdę potrzebne, jeśli chcesz używać tej metody poważnie, ponieważ nazwy plików mogą zawierać *, ?oraz [--then ponownie włączyć go ( set +f) potem.

Możesz także całkowicie zastąpić tę konkretną pętlę pojedynczym printfpoleceniem - choć tak naprawdę nie jest to zaletą mapfile, ponieważ możesz to zrobić niezależnie od tego, czy użyjesz mapfileinnej metody do zapełnienia tablicy. Oto krótsza wersja:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

Ważne jest, aby pamiętać, że, jak wspomina terdon , operacja linia po linii nie zawsze jest odpowiednia i nie będzie działać poprawnie z nazwami plików, które zawierają znaki nowej linii. Odradzam nazywanie plików w ten sposób, ale może się to zdarzyć, w tym przez przypadek.

Tak naprawdę nie ma jednego uniwersalnego rozwiązania.

Prosiłeś o „ogólne polecenie dla wszystkich scenariuszy” i podejście mapfilepolegające na tym, że w pewnym stopniu zbliżasz się do tego celu, ale zachęcam do ponownego rozważenia swoich wymagań.

Zadanie pokazane powyżej lepiej wykonać za pomocą jednego findpolecenia:

find . -type d -printf 'DIR: %p\n'

Możesz także użyć zewnętrznego polecenia, takiego jak seddodać DIR: na początku każdej linii. Jest to prawdopodobnie nieco brzydkie i w przeciwieństwie do polecenia find doda dodatkowe „przedrostki” w nazwach plików zawierających znaki nowej linii, ale działa niezależnie od danych wejściowych, więc spełnia twoje wymagania i nadal lepiej jest czytać dane wyjściowe w pliku zmienna lub tablica :

find . -type d | sed 's/^/DIR: /'

Jeśli chcesz wyświetlić listę, a także wykonać akcję na każdym znalezionym katalogu, na przykład uruchomić some-commandi przekazać ścieżkę katalogu jako argument, findmożesz to zrobić również:

find . -type d -print -exec some-command {} \;

Jako kolejny przykład wróćmy do ogólnego zadania dodawania prefiksu do każdej linii. Załóżmy, że chcę zobaczyć wynik, help mapfileale numeruje linie. Tak naprawdę nie użyłbym mapfiledo tego ani żadnej innej metody, która odczytuje go do zmiennej powłoki lub tablicy powłoki. Załóżmy, help mapfile | cat -nże nie daje pożądanego formatowania, mogę użyć awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

Odczytywanie wszystkich danych wyjściowych polecenia do jednej zmiennej lub tablicy jest czasem przydatne i odpowiednie, ale ma poważne wady. Nie tylko musisz poradzić sobie z dodatkową złożonością korzystania z powłoki, gdy istniejące polecenie lub kombinacja istniejących poleceń może już robić to, czego potrzebujesz, lub lepiej, ale całe wyjście polecenia musi być przechowywane w pamięci. Czasami możesz wiedzieć, że to nie jest problem, ale czasami możesz przetwarzać duży plik.

Alternatywą, która jest często podejmowana - a czasem wykonywana właściwie - jest odczytywanie wejścia wiersz po wierszu za read -rpomocą pętli. Jeśli nie musisz zapisywać poprzednich wierszy podczas pracy na późniejszych wierszach i musisz użyć długich danych wejściowych, może to być lepsze niż mapfile. Ale również tego należy unikać w przypadkach, gdy można po prostu potokować go do polecenia, które może wykonać pracę, co jest w większości przypadków .

Eliah Kagan
źródło