Як виконати цикл for для кожного символу в рядку в Bash?


83

У мене є така змінна:

words="这是一条狗。"

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

Єдиний спосіб, який я знаю, - це вивести кожен символ у окремий рядок у файлі, а потім використовувати while read line, але це видається дуже неефективним.

  • Як я можу обробити кожен символ у рядку через цикл for?

3
Можливо, варто згадати, що ми бачимо багато запитань для початківців, де ОП вважає, що це те, що вони хочуть зробити. Дуже часто можливо краще рішення, яке не вимагає обробки кожного символу окремо. Це відоме як проблема XY, і правильним рішенням є пояснити, чого ви насправді хочете досягти у своєму запитанні, а не просто як виконати кроки, які, на вашу думку, допоможуть вам досягти цього.
триплеє

Відповіді:


45

З sedon dashshell 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. Мені це не потрібно було, але ти все одно отримаєш мою підтримку.
Йорданія

+1 Ви можете використовувати цикл for на отриманому рядку від sed.
Tyzoid

236

Ви можете використовувати forцикл у стилі С :

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
а як щодо пробілів?
Леандро

А як щодо пробілів? Пробіл - це символ, який перемикається на всі символи. (Хоча вам слід подбати про використання подвійних лапок навколо будь-якої змінної чи рядка, що містить значні пробіли.
Загальніше

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 .


Продуктивність

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

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"), в кінці додається зайвий символ нового рядка. Крім того, передача рядка через трубу ( 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 - буква r - буква M - буква e - буква e - буква s - буква s - буква a - буква g - буква e - буква


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 працює з Unicode

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

-1

Я ділюсь своїм рішенням:

read word

for char in $(grep -o . <<<"$word") ; do
    echo $char
done

Це дуже глючне - спробуйте ввести рядок, який містить a *, ви отримаєте файли в поточному каталозі.
Чарльз Даффі,

-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.