Jak podzielić jeden ciąg na wiele ciągów oddzielonych co najmniej jedną spacją w powłoce bash?

224

Mam ciąg zawierający wiele słów z co najmniej jedną spacją między nimi. Jak mogę podzielić ciąg na poszczególne słowa, aby móc je przewijać?

Ciąg jest przekazywany jako argument. Np ${2} == "cat cat file". Jak mogę przez to przejść?

Jak mogę sprawdzić, czy ciąg znaków zawiera spacje?

derrdji
źródło
1
Jaki rodzaj skorupy? Bash, cmd.exe, powershell ...?
Alexey Sviridov
Czy wystarczy zapętlić (np. Wykonać polecenie dla każdego słowa)? A może musisz przechowywać listę słów do późniejszego wykorzystania?
DVK

Odpowiedzi:

281

Czy próbowałeś po prostu przekazać zmienną łańcuchową do forpętli? Na przykład Bash automatycznie podzieli się na białe znaki.

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.
tłum
źródło
1
@MobRule - jedyną wadą tego jest to, że nie można łatwo przechwycić (przynajmniej nie pamiętam sposobu) wyników do dalszego przetwarzania. Zobacz moje rozwiązanie „tr” poniżej, aby wysłać coś do STDOUT
DVK
4
Można po prostu dołączyć go do zmiennej: A=${A}${word}).
Lucas Jones
1
ustaw $ text [wstawi słowa do 1 $, 2 $, 3 $ ... itd.]
Rajesh
32
W rzeczywistości ta sztuczka jest nie tylko złym rozwiązaniem, ale jest również bardzo niebezpieczna z powodu globowania powłoki. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; donewyjścia [NOPE] [a] [NOPE]zamiast oczekiwanych [*] [a] [*](LF zastąpione przez SPC dla czytelności).
Tino
@mob co powinienem zrobić, jeśli chcę podzielić ciąg na podstawie określonego ciągu? przykład separatora „.xlsx” .
296

Podoba mi się konwersja na tablicę, aby mieć dostęp do poszczególnych elementów:

sentence="this is a story"
stringarray=($sentence)

teraz możesz uzyskać bezpośredni dostęp do poszczególnych elementów (zaczyna się od 0):

echo ${stringarray[0]}

lub przekonwertować z powrotem na ciąg, aby wykonać pętlę:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

Oczywiście już wcześniej odpowiedziano bezpośrednio na pętlę ciągu, ale ta wada miała tę wadę, że nie śledziła poszczególnych elementów do późniejszego użycia:

for i in $sentence
do
  :
  # do whatever on $i
done

Zobacz także Odwołanie do tablicy Bash .

Silny wiatr
źródło
26
Niestety nie do końca idealny, z powodu globowania powłoki: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=wyniki arr=([0]="NOPE" [1]="a" [2]="NOPE")zamiast oczekiwanycharr=([0]="*" [1]="a" [2]="*")
Tino
@Tino: jeśli nie chcesz, aby globowanie przeszkadzało, po prostu wyłącz je. Rozwiązanie będzie wtedy działać dobrze również z symbolami wieloznacznymi. Moim zdaniem jest to najlepsze podejście.
Alexandros,
3
@Alexandros Moje podejście polega na stosowaniu wyłącznie wzorców, które są domyślnie bezpieczne i działają doskonale w każdym kontekście. Wymóg zmiany globowania powłoki w celu uzyskania bezpiecznego rozwiązania to coś więcej niż bardzo niebezpieczna ścieżka, to już ciemna strona. Tak więc radzę, aby nigdy nie przyzwyczajać się do używania takiego wzoru tutaj, ponieważ prędzej czy później zapomnisz o niektórych szczegółach, a następnie ktoś wykorzysta twój błąd. W prasie można znaleźć dowód na takie wyczyny. Każdy. Pojedynczy. Dzień.
Tino
86

Wystarczy użyć wbudowanego „zestawu” powłok. Na przykład,

ustaw $ text

Następnie poszczególne słowa w tekście $ będą w 1 $, 2 $, 3 $ itd. Aby uzyskać solidność, zwykle robi się

set - śmieciowy tekst
Zmiana

aby obsłużyć przypadek, w którym $ text jest pusty lub rozpocząć od myślnika. Na przykład:

text = "To jest test"
set - śmieciowy tekst
Zmiana
za słowo; robić
  echo „[$ słowo]”
Gotowe

To drukuje

[To]
[jest]
[za]
[test]
Ideliczny
źródło
5
Jest to doskonały sposób na podzielenie var, aby można było uzyskać bezpośredni dostęp do poszczególnych części. +1; rozwiązał mój problem
Cheekysoft
Chciałem zasugerować użycie, awkale setjest o wiele łatwiejsze. Jestem teraz setfanboyem. Dzięki @Idelic!
Yzmir Ramirez
22
Należy pamiętać o globowaniu powłoki, jeśli wykonujesz takie rzeczy: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; donewyniki [NOPE] [a] [NOPE]zamiast oczekiwanych [*] [a] [*]. Używaj go tylko wtedy, gdy masz 101% pewności, że w podzielonym ciągu nie ma metaznaków SHELL!
Tino
4
@Tino: Ten problem dotyczy wszędzie, nie tylko tutaj, ale w tym przypadku można tuż set -fprzed set -- $vari set +fpo nim wyłączyć globowanie.
Idelic
3
@Idelic: Dobry połów. Z set -frozwiązania jest bezpieczny, zbyt. Ale set +fjest domyślną wartością każdej powłoki, więc jest to istotny szczegół, który należy zauważyć, ponieważ inni prawdopodobnie nie są tego świadomi (tak jak ja też).
Tino
81

Prawdopodobnie najłatwiejszym i najbezpieczniejszym sposobem w BASH 3 i nowszych jest:

var="string    to  split"
read -ra arr <<<"$var"

(gdzie arrjest tablica, która pobiera podzielone części łańcucha) lub, jeśli na wejściu mogą znajdować się znaki nowej linii i potrzebujesz więcej niż tylko pierwszego wiersza:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(zwróć uwagę na miejsce w środku -d '', nie można go zostawić), ale może to dać nieoczekiwany znak nowej linii <<<"$var"(ponieważ domyślnie dodaje to LF na końcu).

Przykład:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

Wyprowadza oczekiwane

[*]
[a]
[*]

ponieważ to rozwiązanie (w przeciwieństwie do wszystkich poprzednich rozwiązań tutaj) nie jest podatne na nieoczekiwane i często niekontrolowane globowanie powłoki.

Daje to również pełną moc IFS, jak zapewne chcesz:

Przykład:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

Wyprowadza coś takiego:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

Jak widać, spacje można również zachować w ten sposób:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

wyjścia

[ split  ]
[   this    ]

Należy pamiętać, że obsługa IFSw BASH jest przedmiotem sama w sobie, podobnie jak testy, kilka interesujących tematów na ten temat:

  • unset IFS: Ignoruje przebiegi SPC, TAB, NL oraz on-line start i end
  • IFS='': Bez separacji pola, wszystko czyta
  • IFS=' ': Uruchamia SPC (i tylko SPC)

Ostatni przykład

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

wyjścia

1 [this is]
2 [a test]

podczas

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

wyjścia

1 [this]
2 [is]
3 [a]
4 [test]

BTW:

  • Jeśli nie jesteś $'ANSI-ESCAPED-STRING'przyzwyczajony do tego, oznacza to oszczędność czasu.

  • Jeśli nie podasz -r(jak w read -a arr <<<"$var"), wtedy read unika odwrotnego ukośnika. Pozostaje to jako ćwiczenie dla czytelnika.


W przypadku drugiego pytania:

Aby przetestować coś w ciągu, zwykle trzymam się tego case, ponieważ może to sprawdzić wiele przypadków jednocześnie (uwaga: case wykonuje tylko pierwsze dopasowanie, jeśli potrzebujesz przewrotnego użycia caseinstrukcji mnożenia ), a taka potrzeba jest dość często (pun zamierzony):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

Możesz więc ustawić wartość zwracaną w celu sprawdzenia SPC w następujący sposób:

case "$var" in (*' '*) true;; (*) false;; esac

Dlaczego case? Ponieważ zwykle jest nieco bardziej czytelny niż sekwencje wyrażeń regularnych, a dzięki metaznakom Shell bardzo dobrze radzi sobie z 99% wszystkich potrzeb.

Tino
źródło
2
Ta odpowiedź zasługuje na więcej głosów pozytywnych, ze względu na podkreślone problemy globbingowe i jej kompleksowość
Brian Agnew,
@brian Thanks. Pamiętaj, że możesz użyćset -f lub set -o noglobprzełączać globowanie, tak aby metaznaki powłoki nie wyrządzały więcej szkody w tym kontekście. Ale tak naprawdę nie jestem tego przyjacielem, ponieważ pozostawia to za sobą wiele mocy powłoki / jest bardzo podatne na błędy, aby przełączać się tam iz powrotem.
Tino
2
Wspaniała odpowiedź, zasługuje na więcej pochwał. Notatka boczna na temat upadku skrzynki - możesz użyć;& osiągnąć. Nie jestem pewien, w której wersji bash się pojawił. Jestem użytkownikiem 4.3
Sergiy Kolodyazhnyy 11.01.17
2
@Serg dziękuję za uwagę, ponieważ jeszcze tego nie wiedziałem! Więc spojrzałem na to, pojawił się w Bash4 . ;&jest wymuszone przewijanie bez sprawdzania wzorca, jak w C. I jest też taki, ;;&który kontynuuje dalsze sprawdzanie wzorca. Więc ;;jest jak if ..; then ..; else if ..i ;;&jest jak if ..; then ..; fi; if .., gdzie ;&jest m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- nigdy nie przestaje się uczyć (od innych);)
Tino
@Tino To absolutnie prawda - nauka jest procesem ciągłym. W rzeczywistości nie wiedziałem o tym, ;;&zanim skomentowałeś: D Dzięki, i niech skorupa będzie z tobą;)
Sergiy Kolodyazhnyy
43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Aby sprawdzić spacje, użyj grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1
DVK
źródło
1
W bashu echo "X" |można zwykle zastąpiony przez <<<"X"coś takiego: grep -s " " <<<"This contains SPC". Możesz zauważyć różnicę, jeśli zrobisz coś echo X | read varw przeciwieństwie do read var <<< X. Tylko ta ostatnia importuje zmienną vardo bieżącej powłoki, a aby uzyskać do niej dostęp w pierwszym wariancie, musisz pogrupować w następujący sposób:echo X | { read var; handle "$var"; }
Tino
17

(A) Aby podzielić zdanie na jego słowa (oddzielone spacją), możesz po prostu użyć domyślnego IFS, używając

array=( $string )


Przykład uruchomienia następującego fragmentu kodu

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

wyjdzie

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

Jak widać, możesz bez problemu używać pojedynczych lub podwójnych cudzysłowów.

Uwagi:
- jest to w zasadzie to samo co odpowiedź moba , ale w ten sposób przechowujesz tablicę na wszelkie dalsze potrzeby. Jeśli potrzebujesz tylko jednej pętli, możesz użyć jego odpowiedzi, która jest krótsza o jedną linię :)
- zapoznaj się z tym pytaniem, aby uzyskać alternatywne metody dzielenia łańcucha na podstawie ogranicznika.


(B) Aby sprawdzić znak w ciągu, możesz również użyć dopasowania wyrażenia regularnego.
Przykład sprawdzenia obecności znaku spacji, którego możesz użyć:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi
Luca Borrione
źródło
Dla wyrażenia regularnego (B) +1, ale -1 dla niewłaściwego rozwiązania (A), ponieważ jest to podatne na błąd powodujący globowanie powłoki. ;)
Tino
6

Aby sprawdzić spacje tylko za pomocą bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"
Glenn Jackman
źródło
1
echo $WORDS | xargs -n1 echo

Powoduje to wyświetlenie każdego słowa, które możesz przetworzyć na liście według własnego uznania.

Álex
źródło