Как выполнить цикл for для каждого символа в строке в Bash?


83

У меня есть такая переменная:

words="这是一条狗。"

Я хочу , чтобы сделать цикл по каждому из персонажей, один в то время, например , первый character="这", а затем character="是", character="一"и т.д.

Единственный известный мне способ - вывести каждый символ в отдельную строку в файле, а затем использовать while read line, но это кажется очень неэффективным.

  • Как я могу обработать каждый символ в строке с помощью цикла for?

3
Возможно, стоит упомянуть, что мы видим много вопросов новичков, когда ОП думает, что это именно то, что они хотят сделать. Очень часто возможно лучшее решение, которое не требует индивидуальной обработки каждого символа. Это известно как проблема XY, и правильное решение - объяснить, чего вы действительно хотите достичь в своем вопросе, а не только то, как выполнить шаги, которые, по вашему мнению, помогут вам достичь этого.
tripleee

Ответы:


45

С sedна dashоболочке LANG=en_US.UTF-8, я получил в следующем режиме работает правильно:

$ echo "你好嗎 新年好。全型句號" | sed -e 's/\(.\)/\1\n/g'
你
好
嗎

新
年
好
。
全
型
句
號

и

$ echo "Hello world" | sed -e 's/\(.\)/\1\n/g'
H
e
l
l
o

w
o
r
l
d

Таким образом, вывод можно зациклить с помощью while read ... ; do ... ; done

отредактированный для образца текста перевод на английский язык:

"你好嗎 新年好。全型句號" is zh_TW.UTF-8 encoding for:
"你好嗎"     = How are you[ doing]
" "         = a normal space character
"新年好"     = Happy new year
"。全型空格" = a double-byte-sized full-stop followed by text description

4
Хорошая работа с UTF-8. Мне это не нужно, но вы все равно получите мой голос.
Jordan

+1 Вы можете использовать цикл for для результирующей строки из sed.
Tyzoid

236

Вы можете использовать forцикл в стиле C :

foo=string
for (( i=0; i<${#foo}; i++ )); do
  echo "${foo:$i:1}"
done

${#foo}расширяется до длины foo. ${foo:$i:1}расширяется до подстроки, начиная с позиции $iдлины 1.


Зачем вам нужны две скобки вокруг оператора for, чтобы он работал?
tgun926

Это bashтребует синтаксиса .
chepner

3
Я знаю, что это устарело, но две круглые скобки необходимы, потому что они позволяют выполнять арифметические операции. См. Здесь => tldp.org/LDP/abs/html/dblparens.html
Ганнибал

8
@Hannibal Я просто хотел указать, что это конкретное использование двойных скобок на самом деле является конструкцией bash: for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; doneа не то же самое, что $ (( expr )) или (( expr )). Во всех трех конструкциях bash expr обрабатывается одинаково, а $ (( expr )) также является POSIX.
nabin-info

1
@codeforester Это не имеет ничего общего с массивами; это всего лишь одно из многих выражений, bashкоторые вычисляются в арифметическом контексте.
chepner

36

${#var} возвращает длину var

${var:pos:N}возвращает N символов posначиная с

Примеры:

$ words="abc"
$ echo ${words:0:1}
a
$ echo ${words:1:1}
b
$ echo ${words:2:1}
c

так что легко повторить.

по-другому:

$ grep -o . <<< "abc"
a
b
c

или же

$ grep -o . <<< "abc" | while read letter;  do echo "my letter is $letter" ; done 

my letter is a
my letter is b
my letter is c

1
как насчет пробелов?
Леандро

А как насчет пробелов? Пробельный символ - это символ, и он проходит по всем символам. (Хотя вам следует позаботиться о том, чтобы заключать в двойные кавычки любую переменную или строку, содержащую значимые пробелы. В целом, всегда цитируйте все, если вы не знаете, что делаете. )
tripleee

23

Я удивлен, что никто не упомянул очевидное bashрешение, использующее только whileи read.

while read -n1 character; do
    echo "$character"
done < <(echo -n "$words")

Обратите внимание на использование, echo -nчтобы избежать лишней новой строки в конце. printf- еще один хороший вариант, который может больше подойти для ваших нужд. Если вы хотите игнорировать пробелы, замените их "$words"на "${words// /}".

Другой вариант есть fold. Обратите внимание, однако, что он никогда не должен попадать в цикл for. Вместо этого используйте цикл while следующим образом:

while read char; do
    echo "$char"
done < <(fold -w1 <<<"$words")

Основным преимуществом использования внешней foldкоманды ( пакета coreutils ) будет краткость. Вы можете передать его вывод другой команде, такой как xargs(часть пакета findutils ), следующим образом:

fold -w1 <<<"$words" | xargs -I% -- echo %

Вы захотите заменить echoкоманду, используемую в приведенном выше примере, на команду, которую вы хотите запускать для каждого персонажа. Обратите внимание, что xargsпо умолчанию отбрасываются пробелы. Вы можете использовать, -d '\n'чтобы отключить это поведение.


Интернационализация

Я только что протестировал foldнекоторые азиатские символы и понял, что у них нет поддержки Unicode. Так что, хотя он подходит для нужд ASCII, он не сработает для всех. В этом случае есть несколько альтернатив.

Я бы, наверное, заменил fold -w1массивом awk:

awk 'BEGIN{FS=""} {for (i=1;i<=NF;i++) print $i}'

Или grepкоманда, упомянутая в другом ответе:

grep -o .


Спектакль

К вашему сведению, я проверил 3 вышеупомянутых варианта. Первые два были быстрыми, почти завязывающими, а петля сгиба была немного быстрее, чем петля while. Неудивительно, что он xargsбыл самым медленным ... в 75 раз медленнее.

Вот (сокращенный) тестовый код:

words=$(python -c 'from string import ascii_letters as l; print(l * 100)')

testrunner(){
    for test in test_while_loop test_fold_loop test_fold_xargs test_awk_loop test_grep_loop; do
        echo "$test"
        (time for (( i=1; i<$((${1:-100} + 1)); i++ )); do "$test"; done >/dev/null) 2>&1 | sed '/^$/d'
        echo
    done
}

testrunner 100

Вот результаты:

test_while_loop
real    0m5.821s
user    0m5.322s
sys     0m0.526s

test_fold_loop
real    0m6.051s
user    0m5.260s
sys     0m0.822s

test_fold_xargs
real    7m13.444s
user    0m24.531s
sys     6m44.704s

test_awk_loop
real    0m6.507s
user    0m5.858s
sys     0m0.788s

test_grep_loop
real    0m6.179s
user    0m5.409s
sys     0m0.921s

characterпуст для пробелов с помощью простого while readрешения, что может быть проблематичным, если нужно различать разные типы пробелов.
pkfm

Хорошее решение. Я обнаружил, что изменение read -n1на read -N1необходимо для правильной обработки пробелов.
nielsen

16

Я считаю, что до сих пор нет идеального решения, которое бы правильно сохраняло все символы пробелов и было достаточно быстрым, поэтому я отправлю свой ответ. Использование ${foo:$i:1}работает, но очень медленно, что особенно заметно на больших строках, как я покажу ниже.

Моя идея - это расширение метода, предложенного Six , который включает в себя read -n1некоторые изменения для сохранения всех символов и правильной работы для любой строки:

while IFS='' read -r -d '' -n 1 char; do
        # do something with $char
done < <(printf %s "$string")

Как это устроено:

  • IFS=''- Переопределение внутреннего разделителя полей на пустую строку предотвращает удаление пробелов и табуляции. Выполнение этого в той же строке readозначает, что это не повлияет на другие команды оболочки.
  • -r- Означает «сырой», что предотвращает readобработку \конца строки как специального символа конкатенации строк.
  • -d ''- Передача пустой строки в качестве разделителя предотвращает readудаление символов новой строки. Фактически означает, что в качестве разделителя используется нулевой байт. -d ''равно -d $'\0'.
  • -n 1 - Означает, что будет читаться по одному символу.
  • printf %s "$string"- Использование printfвместо echo -nбезопаснее, потому что echoотносится -nи -eк опциям. Если вы передадите «-e» в виде строки, echoничего не напечатает.
  • < <(...)- Передача строки в цикл с помощью подстановки процесса. Если вы используете здесь-строки вместо ( done <<< "$string"), в конце добавляется дополнительный символ новой строки. Кроме того, передача строки через pipe ( printf %s "$string" | while ...) заставит цикл выполняться в подоболочке, что означает, что все операции с переменными являются локальными внутри цикла.

Теперь давайте проверим производительность с огромной строкой. В качестве источника я использовал следующий файл:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Следующий скрипт был вызван timeкомандой:

#!/bin/bash

# Saving contents of the file into a variable named `string'.
# This is for test purposes only. In real code, you should use
# `done < "filename"' construct if you wish to read from a file.
# Using `string="$(cat makefiles.txt)"' would strip trailing newlines.
IFS='' read -r -d '' string < makefiles.txt

while IFS='' read -r -d '' -n 1 char; do
        # remake the string by adding one character at a time
        new_string+="$char"
done < <(printf %s "$string")

# confirm that new string is identical to the original
diff -u makefiles.txt <(printf %s "$new_string")

И вот результат:

$ time ./test.sh

real    0m1.161s
user    0m1.036s
sys     0m0.116s

Как видим, довольно быстро.
Затем я заменил цикл на цикл, в котором используется расширение параметров:

for (( i=0 ; i<${#string}; i++ )); do
    new_string+="${string:$i:1}"
done

Выходные данные показывают, насколько велика потеря производительности:

$ time ./test.sh

real    2m38.540s
user    2m34.916s
sys     0m3.576s

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


13

Я тестировал это только со строками ascii, но вы могли бы сделать что-то вроде:

while test -n "$words"; do
   c=${words:0:1}     # Get the first character
   echo character is "'$c'"
   words=${words:1}   # trim the first character
done

8

Цикл стиля C в ответе @chepner находится в функции оболочки update_terminal_cwd, и grep -o .решение умное, но я был удивлен, не увидев решения, использующего seq. Вот мой:

read word
for i in $(seq 1 ${#word}); do
  echo "${word:i-1:1}"
done

6

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

for char in `echo "这是一条狗。" | fold -w1`; do
    echo $char
done

1
#!/bin/bash

word=$(echo 'Your Message' |fold -w 1)

for letter in ${word} ; do echo "${letter} is a letter"; done

Вот результат:

Y - это буква o - это буква u - это буква r - это буква M - это буква e - это буква s - это буква s - это буква a - это буква g - это буква е - это буква


1

Чтобы перебирать символы ASCII в POSIX-совместимой оболочке, вы можете избежать внешних инструментов, используя расширение параметров:

#!/bin/sh

str="Hello World!"

while [ ${#str} -gt 0 ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done

или же

str="Hello World!"

while [ -n "$str" ]; do
    next=${str#?}
    echo "${str%$next}"
    str=$next
done

1

sed работает с юникодом

IFS=$'\n'
for z in $(sed 's/./&\n/g' <(printf '你好嗎')); do
 echo hello: "$z"
done

выходы

hello: 你
hello: 好
hello: 嗎

0

Другой подход, если вас не волнует игнорирование пробелов:

for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do
    # Handle $char here
done

0

Другой способ:

Characters="TESTING"
index=1
while [ $index -le ${#Characters} ]
do
    echo ${Characters} | cut -c${index}-${index}
    index=$(expr $index + 1)
done


-3
TEXT="hello world"
for i in {1..${#TEXT}}; do
   echo ${TEXT[i]}
done

где {1..N}включающий диапазон

${#TEXT} это количество букв в строке

${TEXT[i]} - вы можете получить char из строки, как элемент из массива


5
Shellcheck сообщает: «Bash не поддерживает переменные в расширениях диапазона фигурных скобок». Так что это не будет работать в Bash
Брен

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