Làm thế nào để thực hiện một vòng lặp for trên mỗi ký tự trong một chuỗi trong Bash?


82

Tôi có một biến như thế này:

words="这是一条狗。"

Tôi muốn thực hiện một vòng lặp for trên mỗi ký tự, cùng một lúc, ví dụ đầu tiên character="这", sau đó character="是", character="一"vv

Cách duy nhất tôi biết là xuất từng ký tự ra từng dòng riêng biệt trong một tệp, sau đó sử dụng while read line, nhưng cách này có vẻ rất kém hiệu quả.

  • Làm cách nào để xử lý từng ký tự trong chuỗi thông qua vòng lặp for?

3
Có thể đáng nói là chúng tôi thấy rất nhiều câu hỏi dành cho người mới mà OP nghĩ rằng đây là những gì họ muốn làm. Rất thường xuyên, có thể có một giải pháp tốt hơn mà không yêu cầu từng ký tự được xử lý riêng lẻ. Đây được gọi là Vấn đề XY và giải pháp thích hợp là giải thích những gì bạn thực sự muốn đạt được trong câu hỏi của mình, chứ không chỉ cách thực hiện các bước bạn nghĩ sẽ giúp bạn đạt được điều đó.
tripleee

Câu trả lời:


45

Với sedtrên dashvỏ LANG=en_US.UTF-8, tôi có các nội dung sau làm việc phải:

$ 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

Do đó, đầu ra có thể được lặp lại với while read ... ; do ... ; done

đã chỉnh sửa để dịch văn bản mẫu sang tiếng Anh:

"你好嗎 新年好。全型句號" 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
Nỗ lực tốt trên UTF-8. Tôi không cần nó, nhưng bạn vẫn nhận được sự ủng hộ của tôi.
Jordan

+1 Bạn có thể sử dụng vòng lặp for trên chuỗi kết quả từ sed.
Tyzoid

233

Bạn có thể sử dụng forvòng lặp kiểu C :

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

${#foo}mở rộng đến độ dài của foo. ${foo:$i:1}mở rộng đến chuỗi con bắt đầu từ vị trí $icó độ dài 1.


Tại sao bạn cần hai bộ dấu ngoặc quanh câu lệnh for để nó hoạt động?
tgun926

Đó là cú pháp bashyêu cầu.
chepner

3
Tôi biết điều này là cũ, nhưng, hai dấu ngoặc đơn là bắt buộc vì chúng cho phép các phép toán số học. Xem tại đây => tldp.org/LDP/abs/html/dblparens.html
Hannibal

8
@Hannibal Tôi chỉ muốn chỉ ra rằng việc sử dụng dấu ngoặc kép cụ thể này thực sự là cấu trúc bash: for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; donevà không giống với $ (( expr )) nor (( expr )). Trong cả ba cấu trúc bash, expr được xử lý giống nhau và $ (( expr )) cũng là POSIX.
nabin-info

1
@codeforester Điều đó không liên quan gì đến mảng; nó chỉ là một trong nhiều biểu thức bashđược đánh giá trong ngữ cảnh số học.
chepner

36

${#var} trả về độ dài của var

${var:pos:N}trả về N ký tự từ postrở đi

Ví dụ:

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

vì vậy rất dễ lặp lại.

cách khác:

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

hoặc là

$ 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
còn khoảng trắng thì sao?
Leandro

Còn khoảng trắng thì sao? Một ký tự khoảng trắng là một ký tự và ký tự này lặp lại trên tất cả các ký tự. (Mặc dù bạn nên cẩn thận khi dùng dấu ngoặc kép xung quanh bất kỳ biến hoặc chuỗi có chứa khoảng trắng Đáng kể hơn thông thường, luôn luôn trích dẫn tất cả mọi thứ trừ. Bạn biết những gì bạn đang làm. )
tripleee

23

Tôi ngạc nhiên là không ai đề cập đến bashgiải pháp rõ ràng chỉ sử dụng whileread.

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

Lưu ý sử dụng echo -nđể tránh dòng mới không liên quan ở cuối. printflà một lựa chọn tốt khác và có thể phù hợp hơn cho các nhu cầu cụ thể của bạn. Nếu bạn muốn bỏ qua khoảng trắng thì hãy thay thế "$words"bằng "${words// /}".

Một lựa chọn khác là fold. Tuy nhiên, xin lưu ý rằng nó không bao giờ được đưa vào vòng lặp for. Thay vào đó, hãy sử dụng một vòng lặp while như sau:

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

Lợi ích chính của việc sử dụng foldlệnh bên ngoài (của gói coreutils ) sẽ là ngắn gọn. Bạn có thể cung cấp đầu ra của nó cho một lệnh khác chẳng hạn như xargs(một phần của gói findutils ) như sau:

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

Bạn sẽ muốn thay thế echolệnh được sử dụng trong ví dụ trên bằng lệnh bạn muốn chạy với từng ký tự. Lưu ý rằng xargssẽ loại bỏ khoảng trắng theo mặc định. Bạn có thể sử dụng -d '\n'để vô hiệu hóa hành vi đó.


Quốc tế hóa

Tôi vừa thử nghiệm foldvới một số ký tự châu Á và nhận ra rằng nó không hỗ trợ Unicode. Vì vậy, mặc dù nó là tốt cho nhu cầu ASCII, nó sẽ không hoạt động cho tất cả mọi người. Trong trường hợp đó, có một số lựa chọn thay thế.

Tôi có thể thay thế fold -w1bằng một mảng awk:

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

Hoặc greplệnh được đề cập trong một câu trả lời khác:

grep -o .


Hiệu suất

FYI, tôi đã đánh giá tiêu chuẩn cho 3 tùy chọn nói trên. Hai lần đầu tiên nhanh, gần như buộc lại, với vòng lặp gấp nhanh hơn một chút so với vòng lặp while. Không có gì đáng ngạc nhiên xargslà tốc độ chậm nhất ... chậm hơn 75 lần.

Đây là mã kiểm tra (viết tắt):

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

Đây là kết quả:

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

charactertrống cho khoảng trắng với while readgiải pháp đơn giản , có thể có vấn đề nếu các loại khoảng trắng khác nhau phải được phân biệt với nhau.
pkfm

Giải pháp tốt. Tôi thấy rằng cần phải thay đổi read -n1thành read -N1để xử lý các ký tự khoảng trắng một cách chính xác.
nielsen

16

Tôi tin rằng vẫn không có giải pháp lý tưởng nào có thể bảo toàn chính xác tất cả các ký tự khoảng trắng và đủ nhanh, vì vậy tôi sẽ đăng câu trả lời của mình. Sử dụng ${foo:$i:1}hoạt động, nhưng rất chậm, điều này đặc biệt đáng chú ý với các chuỗi lớn, như tôi sẽ trình bày bên dưới.

Ý tưởng của tôi là sự mở rộng của một phương pháp do Six đề xuất , bao gồm read -n1, với một số thay đổi để giữ tất cả các ký tự và hoạt động chính xác cho bất kỳ chuỗi nào:

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

Làm thế nào nó hoạt động:

  • IFS=''- Định nghĩa lại bộ phân tách trường nội bộ thành chuỗi trống ngăn chặn việc tước bỏ khoảng trắng và tab. Thực hiện nó trên cùng một dòng readcó nghĩa là nó sẽ không ảnh hưởng đến các lệnh shell khác.
  • -r- Có nghĩa là "thô", ngăn chặn readviệc coi \ở cuối dòng là một ký tự nối dòng đặc biệt.
  • -d ''- Truyền chuỗi trống làm dấu phân cách ngăn chặn readviệc loại bỏ các ký tự dòng mới. Trên thực tế có nghĩa là byte null được sử dụng làm dấu phân cách. -d ''bằng với -d $'\0'.
  • -n 1 - Có nghĩa là một ký tự tại một thời điểm sẽ được đọc.
  • printf %s "$string"- Sử dụng printfthay vì echo -nlà an toàn hơn, vì echoxử lý -n-enhư là tùy chọn. Nếu bạn chuyển "-e" dưới dạng một chuỗi, echosẽ không in bất cứ thứ gì.
  • < <(...)- Truyền chuỗi vào vòng lặp bằng cách sử dụng thay thế quy trình. Nếu bạn sử dụng here-string thay thế ( done <<< "$string"), một ký tự dòng mới sẽ được thêm vào ở cuối. Ngoài ra, việc truyền chuỗi qua pipe ( printf %s "$string" | while ...) sẽ làm cho vòng lặp chạy trong một vỏ con, có nghĩa là tất cả các hoạt động biến là cục bộ trong vòng lặp.

Bây giờ, hãy kiểm tra hiệu suất với một chuỗi lớn. Tôi đã sử dụng tệp sau làm nguồn:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Tập lệnh sau được gọi thông qua timelệnh:

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

Và kết quả là:

$ time ./test.sh

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

Như chúng ta có thể thấy, nó khá nhanh.
Tiếp theo, tôi đã thay thế vòng lặp bằng một vòng lặp sử dụng mở rộng tham số:

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

Kết quả hiển thị chính xác mức độ ảnh hưởng của việc mất hiệu suất:

$ time ./test.sh

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

Các con số chính xác rất có thể trên các hệ thống khác nhau, nhưng bức tranh tổng thể phải giống nhau.


13

Tôi chỉ thử nghiệm điều này với chuỗi ascii, nhưng bạn có thể làm điều gì đó như:

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

Vòng lặp kiểu C trong câu trả lời của @ chepner nằm trong hàm shell update_terminal_cwdgrep -o .giải pháp rất thông minh, nhưng tôi đã rất ngạc nhiên khi không thấy giải pháp nào sử dụng seq. Đây là của tôi:

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

6

Cũng có thể chia chuỗi thành một mảng ký tự bằng cách sử dụng foldvà sau đó lặp lại trên mảng này:

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 là đầu ra:

Y là chữ o là chữ u là chữ r là chữ M là chữ e là chữ s là chữ s là chữ a là chữ g là chữ e là chữ


0

Một cách tiếp cận khác, nếu bạn không quan tâm đến việc bỏ qua khoảng trắng:

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

0

Một cách khác là:

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

-1

Tôi chia sẻ giải pháp của mình:

read word

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

Điều này rất có lỗi - hãy thử với một chuỗi có chứa a *, bạn sẽ nhận được các tệp trong thư mục hiện tại.
Charles Duffy

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

nơi {1..N}là một phạm vi toàn diện

${#TEXT} là một số ký tự trong một chuỗi

${TEXT[i]} - bạn có thể lấy char từ chuỗi giống như một mục từ một mảng


5
Shellcheck báo cáo "Bash không hỗ trợ các biến trong phạm vi mở rộng cú đúp" Vì vậy, điều này sẽ không làm việc trong Bash
Bren

@Bren Có vẻ như một lỗi đối với tôi.
Sapphire_Brick
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.