Как выполнить цикл for для каждого символа в строке в Bash?
У меня есть такая переменная:
words="这是一条狗。"
Я хочу сделать цикл for для каждого из символов, по одному за раз, например. сначала character="这"
, затем character="是"
, character="一"
и т.д.
Единственный способ, которым я знаю, - вывести каждый символ для разделения строки в файле, а затем использовать while read line
, но это кажется очень неэффективным.
- Как обрабатывать каждый символ в строке через цикл for?
Ответы
Ответ 1
С 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
Ответ 2
Вы можете использовать цикл for
C-стиля:
foo=string
for (( i=0; i<${#foo}; i++ )); do
echo "${foo:$i:1}"
done
${#foo}
расширяется до длины foo
. ${foo:$i:1}
расширяется до подстроки, начиная с позиции $i
длины 1.
Ответ 3
${#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
Ответ 4
Я удивлен, что никто не упомянул очевидное решение 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 .
Производительность
FYI, я сравнил 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
Ответ 5
Я тестировал это только со строками 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
Ответ 6
Я считаю, что до сих пор нет идеального решения, которое бы правильно сохраняло все символы пробелов и было достаточно быстрым, поэтому я отправлю свой ответ. Использование ${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
- означает "raw", что предотвращает read
от обработки \
в конце строки в качестве специального символа конкатенации линии.
-
-d ''
- Передача пустой строки в качестве разделителя предотвращает удаление read
символов новой строки. Фактически это означает, что нулевой байт используется как разделитель. -d ''
равно -d $'\0'
.
-
-n 1
- означает, что будет отображаться один символ за раз.
-
printf %s "$string"
- Использование printf
вместо echo -n
безопаснее, потому что echo
рассматривает -n
и -e
как опции. Если вы передаете "-e" в виде строки, echo
ничего не печатает.
-
< <(...)
- Передача строки в цикл с использованием замещения процесса. Если вместо этого вы используете здесь строки (done <<< "$string"
), в конце добавляется дополнительный символ новой строки. Кроме того, передача строки через канал (printf %s "$string" | while ...
) заставит цикл работать в подоболочке, что означает, что все переменные операции локальны в цикле.
Теперь давайте протестируем производительность с помощью огромной строки.
Я использовал следующий файл в качестве источника:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Следующий script был вызван командой 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
Точные цифры могут быть очень разными для разных систем, но общая картина должна быть одинаковой.
Ответ 7
Также можно разбить строку на массив символов с помощью fold
, а затем перебрать этот массив:
for char in `echo "这是一条狗。" | fold -w1`; do
echo $char
done
Ответ 8
Другой подход, если вы не заботитесь о игнорировании пробелов:
for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do
# Handle $char here
done
Ответ 9
Другой способ:
Characters="TESTING"
index=1
while [ $index -le ${#Characters} ]
do
echo ${Characters} | cut -c${index}-${index}
index=$(expr $index + 1)
done