बैश में एक स्ट्रिंग में प्रत्येक चरित्र पर लूप के लिए प्रदर्शन कैसे करें?


83

मेरे पास इस तरह एक चर है:

words="这是一条狗。"

मैं, एक समय में एक, जैसे पहले पात्रों में से प्रत्येक पर पाश के लिए एक बनाना चाहते character="这"है, तो character="是", character="一"आदि

एकमात्र तरीका मुझे पता है कि प्रत्येक चरित्र को एक फ़ाइल में अलग लाइन में आउटपुट करना है, फिर उपयोग करें while read line, लेकिन यह बहुत अक्षम है।

  • मैं पाश के लिए स्ट्रिंग के माध्यम से प्रत्येक वर्ण को कैसे संसाधित कर सकता हूं?

3
यह ध्यान देने योग्य हो सकता है कि हम बहुत से नौसिखिया प्रश्न देखते हैं जहां ओपी को लगता है कि यह वही है जो वे करना चाहते हैं। बहुत बार, एक बेहतर समाधान जिसके लिए प्रत्येक चरित्र को व्यक्तिगत रूप से संसाधित करने की आवश्यकता नहीं होती है। इसे एक XY समस्या के रूप में जाना जाता है और उचित समाधान यह बताना है कि आप वास्तव में अपने प्रश्न में क्या हासिल करना चाहते हैं , न कि आपके द्वारा सोचे गए चरणों को कैसे निष्पादित करें, इससे आपको वहां पहुंचने में मदद मिलेगी।
ट्रिपलए

जवाबों:


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 पर अच्छा प्रयास। मुझे इसकी आवश्यकता नहीं थी, लेकिन आप मेरे उत्थान को वैसे भी प्राप्त करते हैं।
जॉर्डन

+1 आप 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 की स्थिति में शुरू करने के लिए फैलता है ।


काम करने के लिए आपको कथन के लिए कोष्ठकों के दो सेटों की आवश्यकता क्यों है?
tgun926

यही वाक्यविन्यास की bashआवश्यकता है।
13

3
मुझे पता है कि यह पुराना है, लेकिन, दो कोष्ठकों की आवश्यकता है क्योंकि वे अंकगणितीय संचालन के लिए अनुमति देते हैं। यहाँ देखें => tldp.org/LDP/abs/html/dblparens.html
हैनिबल

8
@ हनीबाल मैं केवल यह बताना चाहता था कि दोहरे कोष्ठकों का यह विशेष उपयोग वास्तव में बाश निर्माण है: for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; doneऔर $ (( expr ) और न ही (( expr )) के समान नहीं है। सभी तीन बैश निर्माणों में, expr को समान माना जाता है और $ (( expr )) भी POSIX है।
नबीन-जानकारी

1
@codeforester जो सरणियों के साथ कुछ नहीं करना है; यह सिर्फ कई अभिव्यक्तियों में bashसे एक है जिसका मूल्यांकन एक अंकगणितीय संदर्भ में किया गया है।
शेपनर

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
व्हाट्सएप के बारे में क्या?
लिएंड्रो

व्हाट्सएप के बारे में क्या ? एक व्हाट्सएप कैरेक्टर एक कैरेक्टर है और यह सभी कैरेक्टर्स को लूप करता है। (हालांकि आपको किसी भी चर या स्ट्रिंग के आसपास दोहरे उद्धरण चिह्नों का उपयोग करने के लिए ध्यान रखना चाहिए जिसमें महत्वपूर्ण व्हाट्सएप शामिल है। अधिक सामान्यतः, हमेशा सब कुछ बोली जब तक आप जानते हैं कि आप क्या कर रहे हैं। )
ट्रिपल जे

23

मुझे आश्चर्य है कि किसी ने भी स्पष्ट bashसमाधान का उपयोग करने का उल्लेख नहीं किया है whileऔरread

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

echo -nविलुप्त होती नईलाइन से बचने के उपयोग पर ध्यान दें । printfएक और अच्छा विकल्प है और आपकी विशेष आवश्यकताओं के लिए अधिक उपयुक्त हो सकता है। अगर आप व्हाट्सएप को इग्नोर करना चाहते हैं तो रिप्लेस कर दें"$words" साथ "${words// /}"

एक और विकल्प है fold । कृपया ध्यान दें कि इसे कभी भी लूप में नहीं खिलाया जाना चाहिए। बल्कि, कुछ समय के लिए लूप का उपयोग करें:

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

बाहरी foldकमांड ( कोरुटिल्स पैकेज) का उपयोग करने का प्राथमिक लाभ संक्षिप्तता होगा। आप इसे किसी अन्य कमांड जैसे आउटपुट xargs( खोजक का हिस्सा) के लिए फ़ीड कर सकते हैं पैकेज ) को निम्नानुसार हैं:

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

आप echoऊपर दिए गए उदाहरण में प्रयुक्त कमांड को उस कमांड से बदलना चाहेंगे जिसे आप प्रत्येक वर्ण के विरुद्ध चलाना चाहते हैं। ध्यान दें किxargs डिफ़ॉल्ट रूप से व्हाट्सएप छोड़ देगा। आप -d '\n'उस व्यवहार को अक्षम करने के लिए उपयोग कर सकते हैं ।


अंतर्राष्ट्रीयकरण

मैंने अभी-अभी परीक्षण किया है fold कुछ एशियाई पात्रों के साथ और महसूस किया कि इसमें यूनिकोड का समर्थन नहीं है। जब तक यह ASCII की जरूरतों के लिए ठीक है, तब तक यह सभी के लिए काम नहीं करेगा। उस मामले में कुछ विकल्प हैं।

मैं शायद fold -w1एक awk सरणी से बदलूंगा:

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

या grepकिसी अन्य उत्तर में उल्लिखित कमांड:

grep -o .


प्रदर्शन

FYI करें, मैंने 3 उपर्युक्त विकल्पों को बेंचमार्क किया। पहले दो तेज थे, लगभग बांधने वाले, जबकि लूप की तुलना में थोड़ा तेजी से लूप। अप्रत्याशित रूप xargsसे सबसे धीमा था ... 75x धीमा।

यहाँ (संक्षिप्त) परीक्षण कोड है:

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सही ढंग से संभाल स्पेस वर्णों की जरूरत थी।
नीलसन

16

मेरा मानना ​​है कि अभी भी कोई आदर्श समाधान नहीं है जो सभी व्हाट्सएप पात्रों को सही ढंग से संरक्षित कर सके और तेजी से पर्याप्त हो, इसलिए मैं अपना उत्तर पोस्ट करूंगा। ${foo:$i:1}कार्यों का उपयोग करना , लेकिन बहुत धीमा है, जो विशेष रूप से बड़े तार के साथ ध्यान देने योग्य है, जैसा कि मैं नीचे दिखाऊंगा।

मेरा विचार छह द्वारा प्रस्तावित एक विधि का विस्तार है , जिसमें 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विकल्प के रूप में। यदि आप एक स्ट्रिंग के रूप में "-ई" पास करते हैं,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

मैंने इसे केवल एससीआई स्ट्रिंग्स के साथ परीक्षण किया है, लेकिन आप कुछ ऐसा कर सकते हैं:

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 स्टाइल लूप शेल फ़ंक्शन में है 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 एक अक्षर ओ है एक अक्षर u है एक पत्र r है एक पत्र है M एक अक्षर ई है एक पत्र s है एक पत्र है एक पत्र है एक पत्र है एक पत्र है एक पत्र है ई एक पत्र है


1

POSIX- संगत शेल पर ASCII वर्णों को पुनरावृत्त करने के लिए, आप पैरामीटर विस्तार का उपयोग करके बाहरी टूल से बच सकते हैं:

#!/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 काम करता है

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


-1

मैं अपना समाधान साझा करता हूं:

read word

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

यह बहुत छोटी गाड़ी है - एक स्ट्रिंग के साथ प्रयास करें जिसमें एक है *, आपको वर्तमान निर्देशिका में फाइलें मिलेंगी।
चार्ल्स डफी

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

जहां {1..N}एक समावेशी रेंज है

${#TEXT} एक स्ट्रिंग में कई अक्षर हैं

${TEXT[i]} - आप एक सरणी से आइटम की तरह स्ट्रिंग से चार ले सकते हैं


5
शेलचेक की रिपोर्ट "बश ब्रेस रेंज एक्सपैंशन में वैरिएबल का समर्थन नहीं करती है" तो यह बश में काम नहीं करेगा
ब्रेन

@ ब्रेन मुझे एक बग की तरह लगता है।
नीलमणि_क्रिक
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.