Podstawianie poleceń: dzielenie na nowy wiersz, ale nie na spację


30

Wiem, że mogę rozwiązać ten problem na kilka sposobów, ale zastanawiam się, czy można to zrobić tylko przy użyciu wbudowanych bashów, a jeśli nie, jaki jest najbardziej efektywny sposób.

Mam plik z zawartością typu

AAA
B C DDD
FOO BAR

przez co rozumiem tylko, że ma kilka linii, a każda linia może zawierać spacje. Chcę uruchomić polecenie takie jak

cmd AAA "B C DDD" "FOO BAR"

Jeśli użyję cmd $(< file), dostanę

cmd AAA B C DDD FOO BAR

a jeśli użyję cmd "$(< file)", dostanę

cmd "AAA B C DDD FOO BAR"

Jak uzyskać traktowanie każdej linii dokładnie jednego parametru?


Odpowiedzi:


26

Przenośny:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

Lub za pomocą podpowłoki, aby IFSzmiany opcji i były lokalne:

( set -f; IFS='
'; exec cmd $(cat <file) )

Powłoka wykonuje dzielenie pól i generowanie nazw plików na podstawie podstawienia zmiennej lub polecenia, które nie jest w podwójnych cudzysłowach. Musisz więc wyłączyć generowanie nazw plików set -fi skonfigurować podział pól za pomocą, IFSaby tylko nowe linie były oddzielnymi polami.

W konstruktach bash i ksh niewiele można zyskać. Możesz ustawić IFSlokalnie dla funkcji, ale nie set -f.

W bash lub ksh93 możesz przechowywać pola w tablicy, jeśli chcesz przekazać je do wielu poleceń. Musisz kontrolować rozszerzanie w momencie budowania tablicy. Następnie "${a[@]}"rozwija się do elementów tablicy, po jednym na słowo.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

10

Możesz to zrobić za pomocą tablicy tymczasowej.

Ustawiać:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Wypełnij tablicę:

$ IFS=$'\n'; set -f; foo=($(<input))

Użyj tablicy:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

Nie można znaleźć sposobu na zrobienie tego bez tej zmiennej tymczasowej - chyba że IFSzmiana nie jest ważna cmd, w takim przypadku:

$ IFS=$'\n'; set -f; cmd $(<input) 

powinien to zrobić.


IFSzawsze wprawia mnie w zakłopotanie. IFS=$'\n' cmd $(<input)nie działa IFS=$'\n'; cmd $(<input); unset IFSdziała. Czemu? Myślę, że użyję(IFS=$'\n'; cmd $(<input))
Old Pro

6
@OldPro IFS=$'\n' cmd $(<input)nie działa, ponieważ ustawia się tylko IFSw środowisku cmd. $(<input)jest rozwijany, aby utworzyć polecenie przed wykonaniem przypisania do IFS.
Gilles 'SO - przestań być zły'

8

Wygląda na to, że kanoniczny sposób na zrobienie tego bashjest podobny

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

lub, jeśli twoja wersja bash ma mapfile:

mapfile -t args < filename
cmd "${args[@]}"

Jedyną różnicę, którą mogę znaleźć między plikiem mapy a pętlą podczas odczytu w porównaniu z linią jednowierszową

(set -f; IFS=$'\n'; cmd $(<file))

polega na tym, że ten pierwszy przekształci pustą linię w pusty argument, podczas gdy jednowierszowy zignoruje pustą linię. W tym przypadku i tak wolę liniowe zachowanie i tak wolę, więc podwójny bonus za to, że jest kompaktowy.

Chciałbym użyć, IFS=$'\n' cmd $(<file)ale to nie działa, ponieważ $(<file)jest interpretowane jako wiersz poleceń, zanim zacznie IFS=$'\n'obowiązywać.

Chociaż w moim przypadku to nie działa, nauczyłem się, że wiele narzędzi obsługuje linie kończące, null (\000)zamiast newline (\n)których znacznie ułatwia to, na przykład, nazwy plików, które są częstymi źródłami takich sytuacji :

find / -name '*.config' -print0 | xargs -0 md5

wysyła listę w pełni kwalifikowanych nazw plików jako argumentów do md5 bez globowania, interpolacji lub czegokolwiek. To prowadzi do niewbudowanego rozwiązania

tr "\n" "\000" <file | xargs -0 cmd

chociaż to również ignoruje puste linie, chociaż przechwytuje linie, które mają tylko białe znaki.


Używanie cmd $(<file)wartości bez cytowania (używanie zdolności bash do dzielenia słów) jest zawsze ryzykownym zakładem. Jeśli którykolwiek wiersz *, zostanie rozwinięty przez powłokę do listy plików.

3

Możesz użyć wbudowanego bash, mapfileaby odczytać plik do tablicy

mapfile -t foo < filename
cmd "${foo[@]}"

lub niesprawdzony xargsmoże to zrobić

xargs cmd < filename

Z dokumentacji pliku map: „plik map nie jest powszechną ani przenośną funkcją powłoki”. I rzeczywiście nie jest obsługiwany w moim systemie. xargsteż nie pomaga.
Old Pro

Będziesz potrzebować xargs -dlubxargs -L
James Youngman

@James, nie, nie mam -dopcji i xargs -L 1uruchamia polecenie raz na linię, ale wciąż dzieli argumenty na białe znaki.
Old Pro

1
@OldPro, cóż, poprosiłeś o „sposób na zrobienie tego przy użyciu tylko wbudowanych bashów” zamiast „wspólnej lub przenośnej funkcji powłoki”. Jeśli twoja wersja bash jest za stara, czy możesz ją zaktualizować?
glenn jackman

mapfilejest dla mnie bardzo przydatny, ponieważ pobiera puste wiersze jako elementy tablicy, czego IFSnie robi metoda. IFStraktuje ciągłe znaki nowej linii jako pojedynczy separator ... Dzięki za przedstawienie, ponieważ nie byłem świadomy polecenia (chociaż na podstawie danych wejściowych OP i oczekiwanej linii poleceń wydaje się, że naprawdę chce zignorować puste wiersze).
Peter.O

0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

Najlepszy sposób, jaki mogłem znaleźć. Po prostu działa.


I dlaczego to działa, jeśli ustawisz IFSdo przestrzeni, ale chodzi o to, aby nie podzielić na przestrzeni?
RalfFriedl

0

Plik

Najbardziej podstawową pętlą (przenośną) do dzielenia pliku na nowe wiersze jest:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Który wydrukuje:

$ ./script
<AAA><A B C><DE F>

Tak, z domyślnym IFS = spacetabnewline.

Dlaczego to działa?

  • IFS zostanie wykorzystany przez powłokę do podzielenia danych wejściowych na kilka zmiennych. Ponieważ istnieje tylko jedna zmienna, powłoka nie wykonuje podziału. Więc nie ma IFSpotrzeby zmiany .
  • Tak, początkowe i końcowe spacje / tabulatory są usuwane, ale w tym przypadku nie stanowi to problemu.
  • Nie, nie masek odbywa się jako ekspansja nie jest nienotowanych . Więc nie set -fpotrzebne.
  • Jedyną używaną (lub potrzebną) tablicą są parametry pozycyjne podobne do tablicy.
  • Opcja -r(raw) polega na uniknięciu usuwania większości ukośników odwrotnych.

To nie zadziała, jeśli konieczne jest dzielenie i / lub globowanie. W takich przypadkach potrzebna jest bardziej złożona struktura.

Jeśli potrzebujesz (nadal przenośny), aby:

  • Unikaj usuwania początkowych i końcowych spacji / tabulatorów, użyj: IFS= read -r line
  • Podział na linii do Vars na jakiś znak, użytkowania: IFS=':' read -r a b c.

Podziel plik na inny znak (nieprzenośny, działa z ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

Ekspansja

Oczywiście tytuł twojego pytania dotyczy podziału wykonania polecenia na nowej linii, unikając podziału na spacje.

Jedynym sposobem na uzyskanie podziału od powłoki jest pozostawienie rozszerzenia bez cudzysłowów:

echo $(< file)

Jest to kontrolowane przez wartość IFS, a przy niekwotowanych rozszerzeniach stosowane jest również globowanie. Aby wykonać tę pracę, potrzebujesz:

  • Zestaw IFS do nowej linii tylko , aby uzyskać podział tylko na nowej linii.
  • Usuń zaznaczenie opcji powłoki globbing set +f:

    set + f IFS = '' cmd $ (<plik)

Oczywiście zmienia to wartość IFS i globowania dla pozostałej części skryptu.

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.