program story

Bash에서 문자열의 각 문자에 대해 for 루프를 수행하는 방법은 무엇입니까?

inputbox 2020. 11. 22. 19:25
반응형

Bash에서 문자열의 각 문자에 대해 for 루프를 수행하는 방법은 무엇입니까?


다음과 같은 변수가 있습니다.

words="这是一条狗。"

나는 각 문자를 한 번에 하나씩, 예를 들어 처음에 루프를 만들고 싶어 character="这", 다음 character="是", character="一"

내가 아는 유일한 방법은 파일의 개별 줄에 각 문자를 출력 한 다음를 사용 while read line하는 것입니다. 그러나 이것은 매우 비효율적으로 보입니다.

  • for 루프를 통해 문자열의 각 문자를 어떻게 처리 할 수 ​​있습니까?

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

C 스타일 for루프를 사용할 수 있습니다 .

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

${#foo}길이로 확장됩니다 foo. 길이 1 ${foo:$i:1}에서 시작하는 하위 문자열로 확장됩니다 $i.


${#var} 길이를 반환 var

${var:pos:N}pos이후부터 N 개의 문자를 반환합니다.

예 :

$ 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

나는 아무도 분명한 언급하지 않았다 놀랐어요 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일부 아시아 문자로 테스트 한 결과 유니 코드가 지원되지 않는다는 것을 깨달았습니다. 따라서 ASCII 요구 사항에는 문제가 없지만 모든 사람에게 적용되는 것은 아닙니다. 이 경우 몇 가지 대안이 있습니다.

아마도 fold -w1awk 배열로 대체 할 것입니다 .

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

모든 공백 문자를 올바르게 보존하고 충분히 빠른 이상적인 솔루션은 아직 없다고 생각하므로 답변을 게시하겠습니다. ${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아무것도 인쇄하지 않습니다.
  • < <(...) - Passing string to the loop using process substitution. If you use here-strings instead (done <<< "$string"), an extra newline character is appended at the end. Also, passing string through a pipe (printf %s "$string" | while ...) would make the loop run in a subshell, which means all variable operations are local within the loop.

Now, let's test the performance with a huge string. I used the following file as a source:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
The following script was called through time command:

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

And the result is:

$ time ./test.sh

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

As we can see, it is quite fast.
Next, I replaced the loop with one that uses parameter expansion:

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

The output shows exactly how bad the performance loss is:

$ time ./test.sh

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

The exact numbers may very on different systems, but the overall picture should be similar.


I've only tested this with ascii strings, but you could do something like:

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

It is also possible to split the string into a character array using fold and then iterate over this array:

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

The C style loop in @chepner's answer is in the shell function update_terminal_cwd, and the grep -o . solution is clever, but I was surprised not to see a solution using seq. Here's mine:

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

Another approach, if you don't care about whitespace being ignored:

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

Another way is:

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

I share my solution:

read word

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

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

where {1..N} is an inclusive range

${#TEXT} is a number of letters in a string

${TEXT[i]} - you can get char from string like an item from an array

참고URL : https://stackoverflow.com/questions/10551981/how-to-perform-a-for-loop-on-each-character-in-a-string-in-bash

반응형