Как разбить одну строку на несколько строк, разделенных хотя бы одним пробелом в оболочке bash?


224

У меня есть строка, содержащая много слов, по крайней мере, один пробел между каждыми двумя. Как я могу разбить строку на отдельные слова, чтобы я мог проходить через них?

Строка передается в качестве аргумента. Например ${2} == "cat cat file". Как я могу пройти через это?

Кроме того, как я могу проверить, содержит ли строка пробелы?


1
Что за оболочка? Bash, cmd.exe, powershell ...?
— Алексей Свиридов

Вам просто нужно зациклить (например, выполнить команду для каждого из слов)? Или вам нужно сохранить список слов для последующего использования?
— ДВК,

Ответы:


281

Вы пытались просто передать строковую переменную в forцикл? Bash, например, будет автоматически разделяться на пустые места.

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

 

This
is
a
sentence.

1
@MobRule - единственный недостаток этого заключается в том, что вы не можете легко захватить (по крайней мере, я не помню способ) вывод для дальнейшей обработки. См. Мое решение "tr" ниже для чего-то, что отправляет материал в STDOUT
— DVK

4
Вы можете просто добавить его в переменной: A=${A}${word}).
— Лукас Джонс

1
установить $ text [это поместит слова в $ 1, $ 2, $ 3 ... и т. д.]
— Раджеш

32
На самом деле этот трюк является не только неправильным решением, но и чрезвычайно опасным из-за разболтанности оболочки. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; doneвыходные данные [NOPE] [a] [NOPE]вместо ожидаемых [*] [a] [*](LFs заменены на SPC для удобства чтения).
— Тино

@mob Что мне делать, если я хочу разбить строку на основе какой-то конкретной строки? пример разделителя ".xlsx" .

296

Мне нравится преобразование в массив, чтобы иметь возможность доступа к отдельным элементам:

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

теперь вы можете получить доступ к отдельным элементам напрямую (начинается с 0):

echo ${stringarray[0]}

или преобразовать обратно в строку для цикла:

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

Конечно, раньше было получено ответное сообщение о том, что цикл проходил напрямую, но недостатком этого ответа было то, что он не отслеживал отдельные элементы для последующего использования:

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

Смотрите также Bash Array Reference .


26
К сожалению, не совсем идеально, из-за срыва оболочки: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=результаты arr=([0]="NOPE" [1]="a" [2]="NOPE")вместо ожидаемыхarr=([0]="*" [1]="a" [2]="*")
— Тино

@Tino: если вы не хотите, чтобы шатание мешало, просто выключите его. Тогда решение будет отлично работать и с подстановочными знаками. Это лучший подход на мой взгляд.
— Александрос

3
@Alexandros Мой подход состоит в том, чтобы использовать только шаблоны, которые по умолчанию безопасны и прекрасно работают в любом контексте. Требование изменить глобализацию оболочки для получения безопасного решения - это не просто очень опасный путь, это уже темная сторона. Поэтому я советую никогда не привыкать использовать такой шаблон здесь, потому что рано или поздно вы забудете о некоторых деталях, а затем кто-нибудь воспользуется вашей ошибкой. Вы можете найти доказательства для таких подвигов в прессе. Каждый. Не замужем. День.
— Тино

86

Просто используйте встроенные оболочки "set". Например,

установить $ текст

После этого отдельные слова в $ text будут в $ 1, $ 2, $ 3 и т. Д. Для устойчивости обычно делают

set - мусорный текст
сдвиг

обработать случай, когда $ text пуст или начать с тире. Например:

text = "Это тест"
set - мусорный текст
сдвиг
для слова; делать
  echo "[$ word]"
сделано

Это печатает

[Это]
[является]
[А]
[тест]

5
Это отличный способ разделить переменную так, чтобы к отдельным частям можно было обращаться напрямую. +1; решил мою проблему
— Cheekysoft

Я собирался предложить использовать, awkно setгораздо проще. Теперь я setфанат. Спасибо @ Идеально!
— Измир Рамирес

22
Пожалуйста, будьте внимательны, если вы делаете такие вещи: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; doneвывод [NOPE] [a] [NOPE]вместо ожидаемого [*] [a] [*]. Используйте его, только если вы на 101% уверены, что в разделенной строке метасимволы SHELL отсутствуют!
— Тино

4
@Tino: Эта проблема применяется везде, не только здесь, но в этом случае вы могли бы просто set -fдо set -- $varи set +fпосле отключить сглаживание.
— Идеально

3
@ Идеально: Хороший улов. С set -fтвоим решением тоже безопасно. Но set +fэто значение по умолчанию для каждой оболочки, поэтому это важная деталь, на которую следует обратить внимание, потому что другие, вероятно, не знают об этом (как я тоже).
— Тино

81

Вероятно, наиболее простой и безопасный способ в BASH 3 и выше:

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

(где arrмассив, который принимает разделенные части строки) или, если во вводе могут быть символы новой строки, и вам нужно больше, чем просто первая строка:

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

(обратите внимание на пробел -d '', он не может быть пропущен), но это может дать вам неожиданный символ новой строки от <<<"$var"(поскольку это неявно добавляет LF в конце).

Пример:

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

Выходит ожидаемый

[*]
[a]
[*]

так как это решение (в отличие от всех предыдущих решений здесь) не подвержено неожиданному и часто неконтролируемому выкалыванию оболочки.

Также это дает вам всю мощь IFS, как вы, вероятно, хотите:

Пример:

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

Выводит что-то вроде:

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

Как видите, пробелы можно сохранить и таким образом:

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

выходы

[ split  ]
[   this    ]

Обратите внимание, что обработка IFSв BASH сама по себе является предметом, поэтому проведите тесты, некоторые интересные темы на эту тему:

  • unset IFS: Игнорирует прогоны SPC, TAB, NL и начинается и заканчивается на линии
  • IFS='': Без разделения полей, просто все читает
  • IFS=' ': Запускает SPC (и только SPC)

Последний пример

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

выходы

1 [this is]
2 [a test]

пока

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

выходы

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

КСТАТИ:

  • Если вы не привыкли к $'ANSI-ESCAPED-STRING'этому, это экономит время.

  • Если вы не включаете -r(как в read -a arr <<<"$var"), тогда read выполняет обратную косую черту. Это оставлено как упражнение для читателя.


По второму вопросу:

Чтобы проверить что-то в строке, я обычно придерживаюсь case, так как это может проверять сразу несколько случаев (примечание: case выполняет только первое совпадение, если вам нужно упасть, используйте caseоператоры multiplce ), и это часто случается (pun предназначена):

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

Таким образом, вы можете установить возвращаемое значение для проверки SPC следующим образом:

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

Почему case? Поскольку он обычно более читабелен, чем последовательности регулярных выражений, и благодаря метасимволам Shell он хорошо обрабатывает 99% всех потребностей.


2
Этот ответ заслуживает большего числа голосов, из-за выделенных проблем и его всесторонности
— Брайан Агнью

@ Брайан Спасибо. Обратите внимание, что вы можете использовать set -fили set -o noglobдля переключения глобализации, чтобы метасимволы оболочки больше не наносили вреда в этом контексте. Но я на самом деле не дружу с этим, так как это оставляет много возможностей оболочки / очень подвержено ошибкам при переключении назад и вперед в этом параметре.
— тино

2
Замечательный ответ, действительно заслуживает большего количества голосов. Дополнительное замечание о падении дела - вы можете ;&добиться этого. Не совсем уверен, в какой версии bash это появилось. Я пользователь 4.3
— Сергей Колодяжный

2
@Serg спасибо за замечание, так как я этого еще не знал! Так что я посмотрел, он появился в Bash4 . ;&это принудительное падение без проверки шаблона, как в C. И есть также то, ;;&что просто продолжает делать дальнейшие проверки шаблона. Так ;;это как if ..; then ..; else if ..и ;;&как if ..; then ..; fi; if .., где ;&это похоже m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- никто не перестает учиться (у других);)
— Тино

@Tino Это абсолютно верно - обучение - это непрерывный процесс. На самом деле, я не знал, ;;&прежде чем вы прокомментировали: D Спасибо, и пусть снаряд будет с вами;)
— Сергей Колодяжный

43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Для проверки пробелов используйте grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1

1
В BASH echo "X" |обычно может быть заменен <<<"X", например: grep -s " " <<<"This contains SPC". Вы можете заметить разницу, если вы делаете что-то вроде echo X | read varв отличие от read var <<< X. Только последняя импортирует переменную varв текущую оболочку, а для доступа к ней в первом варианте необходимо сгруппировать ее так:echo X | { read var; handle "$var"; }
— Tino

17

(A) Чтобы разделить предложение на слова (разделенные пробелами), вы можете просто использовать IFS по умолчанию, используя

array=( $string )


Пример выполнения следующего фрагмента

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

будет выводить

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

Как вы можете видеть, вы можете использовать одинарные или двойные кавычки тоже без проблем.

Примечания:
- это в основном то же самое, что и ответ моба , но таким образом вы сохраняете массив для любых дальнейших нужд. Если вам нужен только один цикл, вы можете использовать его ответ, который на одну строку короче :)
- пожалуйста, обратитесь к этому вопросу для поиска альтернативных методов разделения строки на основе разделителя.


(B) Чтобы проверить наличие символа в строке, вы также можете использовать совпадение регулярного выражения.
Пример для проверки наличия пробела вы можете использовать:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi

Для подсказки регулярного выражения (B) +1, но -1 для неправильного решения (A), так как это подвержено ошибкам, вызывающим сбои оболочки. ;)
— Тино


1
echo $WORDS | xargs -n1 echo

Это выводит каждое слово, вы можете обработать этот список так, как считаете нужным.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.