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


82

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

words="这是一条狗。"

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

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

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

3
OP 가 이것이 그들이 원하는 일 이라고 생각 하는 많은 초보자 질문을 볼 수 있다는 것을 언급 할 가치가 있습니다 . 종종 각 문자를 개별적으로 처리 할 필요가없는 더 나은 솔루션이 가능합니다. 이것은 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 sed의 결과 문자열에 for 루프를 사용할 수 있습니다.
Tyzoid

233

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

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

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


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

1
공백은 어떻습니까?
Leandro

무엇 에 대한 공백? 공백 문자는 문자이며 모든 문자를 반복합니다. (하지만 중요한 공백을 포함하는 모든 변수 또는 문자열 주위에 큰 따옴표를 사용하는 데주의해야합니다.보다 일반적으로 수행중인 작업을 알지 않는 한 항상 모든 것을 인용하십시오 . )
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일부 아시아 문자로 테스트 한 결과 유니 코드가 지원되지 않는다는 것을 깨달았습니다. 따라서 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

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아무것도 인쇄하지 않습니다.
  • < <(...)-프로세스 대체를 사용하여 문자열을 루프에 전달합니다. 대신 here-strings ( 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

@chepner의 답변에서 C 스타일 루프는 shell function 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는 문자 e는 문자


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

이것은 매우 버그가 있습니다.를 포함하는 문자열로 시도 *하면 현재 디렉토리에 파일이 저장됩니다.
Charles Duffy

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

{1..N}포함 범위는 어디 입니까

${#TEXT} 문자열의 여러 글자

${TEXT[i]} -배열의 항목처럼 문자열에서 문자를 얻을 수 있습니다.


5
Shellcheck 배쉬이 그래서하지 않습니다 일 "강타가 중괄호 범위 확장 변수를 지원하지 않습니다"보고
브렌

@Bren은 나에게 버그처럼 보입니다.
Sapphire_Brick
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.