Dizeyi Bash'te bir diziye bölme


640

Bash betiğinde bir satırı parçalara ayırmak ve bunları bir dizide saklamak istiyorum.

Çizgi:

Paris, France, Europe

Ben böyle bir dizide onlara sahip olmak istiyorum:

array[0] = Paris
array[1] = France
array[2] = Europe

Basit kod kullanmak istiyorum, komutun hızı önemli değil. Nasıl yapabilirim?


22
Bu 1 numaralı Google hit ama cevap maalesef çünkü soru maalesef , virgül gibi tek bir karakter değil (virgül-boşluk) sınırlama hakkında soruyor . Sadece ikincisiyle ilgileniyorsanız, yanıtları takip etmek daha kolaydır: stackoverflow.com/questions/918886/…
antak

Bir dizgiyi susturmak ve dizi olarak cutalmayı umursamıyorsanız, akılda tutulması gereken kullanışlı bir bash komutudur. Ayırıcı tanımlanabilir en.wikibooks.org/wiki/Cut Ayrıca sabit genişlikli bir kayıt yapısından da veri çıkarabilirsiniz. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Yanıtlar:


1088
IFS=', ' read -r -a array <<< "$string"

Buradaki karakterlerin $IFS bu durumda alanlar ile ayrılabilir ve böylece ayırıcı olarak bireysel olarak tedavi edilir , ya virgül veya bir boşluk yerine iki karakter dizisi. İlginç bir şekilde, girişte virgül-boşluk göründüğünde boş alanlar oluşturulmaz çünkü boşluk özel olarak ele alınır.

Tek bir öğeye erişmek için:

echo "${array[0]}"

Elemanlar üzerinde yineleme yapmak için:

for element in "${array[@]}"
do
    echo "$element"
done

Hem dizini hem de değeri almak için:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Son örnek yararlıdır çünkü Bash dizileri seyrektir. Başka bir deyişle, bir öğeyi silebilir veya bir öğe ekleyebilirsiniz, ardından dizinler bitişik değildir.

unset "array[1]"
array[42]=Earth

Bir dizideki öğelerin sayısını almak için:

echo "${#array[@]}"

Yukarıda belirtildiği gibi, diziler seyrek olabilir, bu nedenle son elemanı almak için uzunluğu kullanmamalısınız. Bash 4.2 ve sonrasında şunları yapabilirsiniz:

echo "${array[-1]}"

Bash'in herhangi bir sürümünde (2.05b'den sonraki bir yerden):

echo "${array[@]: -1:1}"

Daha büyük negatif ofsetler dizinin sonundan daha uzak seçer. Eski formdaki eksi işaretinden önceki boşluğa dikkat edin. Gerekli.


15
Sadece kullanın IFS=', ', boşlukları ayrı ayrı kaldırmak zorunda değilsiniz. Test:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: Teşekkürler. Ne düşündüğümü bilmiyorum. Bu declare -p arrayarada test çıktısı için kullanmayı seviyorum .
sonraki duyuruya kadar duraklatıldı.

1
Bu alıntılara saygı duymuyor gibi görünüyor. Örneğin France, Europe, "Congo, The Democratic Republic of the"bu kongodan sonra bölünecek.
Yisrael Dov

2
@YisraelDov: Bash'in CSV ile başa çıkmanın bir yolu yok. Tırnak içindeki virgüllerle bunların dışındaki virgül arasındaki farkı söyleyemez. Daha yüksek bir dilde bir lib gibi CSV'yi anlayan bir araç kullanmanız gerekecektir, örneğin Python'daki csv modülü.
sonraki duyuruya kadar duraklatıldı.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")not olarak ayrılır . Bu, yalnızca boşluk IFS=', 'içermeyen alanlarla çalışır, çünkü tek tek karakter kümesidir - dize sınırlayıcısı değil.
dawg

332

Bu sorunun tüm cevapları şu ya da bu şekilde yanlıştır.


Yanlış cevap # 1

IFS=', ' read -r -a array <<< "$string"

1: Bu bir yanlış kullanımdır $IFS. Değeri $IFSdeğişken olduğu olmayan bir alınan tek bir değişken uzunlukta daha ziyade olarak alınır, dizge ayırıcı grubu arasında tek karakterlik her alanda bu dizge ayırıcılar, readgiriş hattı kapalı böler sonlandırılabilir bir dizi karakter ( bu örnekte virgül veya boşluk).

Aslında, dışarıdaki gerçek sopalayıcılar için, tam anlamı $IFSbiraz daha fazladır. Gönderen bash kılavuzu :

Kabuk, IFS'nin her karakterine bir sınırlayıcı gibi davranır ve diğer genişletmelerin sonuçlarını bu karakterleri alan sonlandırıcıları olarak kullanarak kelimelere böler. Eğer IFS kaldırılırsa ya da değeri tam olarak <boşluk> <sekmesi> <satır> , varsayılan, sonra diziler <boşluk> , <tab> ve <satır> başında ve önceki açılımları sonuçlarına sonunda yok sayılır ve başında veya sonunda olmayan herhangi bir IFS karakteri dizisi sözcükleri sınırlandırmaya yarar. Eğer IFS varsayılan dışında bir değere sahiptir, ardından boşluk karakterleri arasında dizileri <boşluk> , <tab> ve <boşluk karakteri IFS ( IFS boşluk karakteri) değerinde olduğu sürece sözcüğün başında ve sonunda yoksayılır . Herhangi bir karakter IFS değil IFS herhangi bitişik birlikte boşluk IFS , bir alanı sınırlandırır boşluk karakterleri. Bir dizi IFS boşluk karakteri de sınırlayıcı olarak ele alınır. IFS değeri null olursa, kelime ayırma gerçekleşmez.

Temel olarak, varsayılan olmayan null olmayan değerler için $IFSalanlardan biri (1) "IFS boşluk karakterleri" (yani <boşluk> , <tab> ve <newline> ( satır beslemesi (LF) anlamına gelen "yeni satır" ),$IFS ) veya (2) mevcut olan herhangi bir sivil "boşluk karakteri IFS" $IFSonu çevreleyen her ne "boşluk karakterleri IFS" ile birlikte giriş satırında.

OP için, bir önceki paragrafta tarif ettiğim ikinci ayırma modunun giriş dizesi için tam olarak istediği şey olması mümkündür, ancak tarif ettiğim ilk ayırma modunun hiç de doğru olmadığından emin olabiliriz. Örneğin, giriş dizesi olsaydı ne olurdu 'Los Angeles, United States, North America'?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2: Bu çözümü tek karakterli bir ayırıcıyla (tek başına virgül, yani takip eden boşluk veya başka bir bagaj olmadan) kullanacak olsanız bile, $stringdeğişkenin değeri herhangi bir LF içeriyorsa read, ilk LF ile karşılaştığında işlemeyi durdurur. readBuiltin sadece çağrı başına bir satır işler. Bu boru veya girdinin bile doğrudur ancak etmek readaçıklamada, biz bu örnekte yapıyorsun gibi burada-string mekanizması ve böylece işlenmemiş girişi kaybolmasına garantilidir. Güç veren kodreadYerleşikliğe , içerdiği komut yapısı içindeki veri akışı hakkında hiçbir bilgiye sahip değildir.

Bunun bir soruna neden olma olasılığının düşük olduğunu iddia edebilirsiniz, ancak yine de mümkünse kaçınılması gereken ince bir tehlike. Bunun nedeni, readyerleşkenin aslında iki düzey girdi ayrımı yapmasıdır: önce satırlara, sonra alanlara. OP sadece bir seviye bölünme istediğinden,read yerleşikin uygun değildir ve bundan kaçınmalıyız.

3: Bu çözümle ilgili açık olmayan potansiyel bir sorun, readboş alanları her zaman boş bırakıyorsa da, boş alanları başka şekilde koruyor olmasıdır. İşte bir demo:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

Belki OP bunu umursamaz, ama hala bilinmeye değer bir sınırlamadır. Çözeltinin sağlamlığını ve genelliğini azaltır.

Bu sorun, readdaha sonra göstereceğim gibi , giriş dizesine beslemeden hemen önce kukla bir izleyen sınırlayıcı ekleyerek çözülebilir .


Yanlış cevap # 2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

Benzer fikir:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(Not: Yanıtlayanın atladığı anlaşılan ikame komutunun etrafına eksik parantezleri ekledim.)

Benzer fikir:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

Bu çözümler, dizeyi alanlara bölmek için dizi atamasında sözcük bölünmesini kullanır. Ne kadar tuhaf ki, readgenel kelime bölme de $IFSözel değişkeni kullanıyor , ancak bu durumda varsayılan <space><tab> <newline> değerine ayarlandığı ima ediliyor ve bir veya daha fazla IFS dolayısıyla herhangi dizisi karakterler (şu anda tümü boşluk karakteri olan) bir alan sınırlayıcısı olarak kabul edilir.

Bu read, kelime bölünmesinin tek başına bir bölünme seviyesi oluşturduğundan, taahhüt edilen iki bölünme seviyesi sorununu çözer . Ancak daha önce olduğu gibi, buradaki sorun, giriş dizesindeki tek tek alanların zaten $IFSkarakterler içerebilmesidir ve bu nedenle kelime bölme işlemi sırasında yanlış bölünürler. Bu, bu yanıt verenler tarafından sağlanan örnek giriş dizelerinden herhangi biri için geçerli değildir (ne kadar uygun ...), ancak elbette bu deyimi kullanan herhangi bir kod tabanının, bu varsayım çizginin bir noktasında ihlal edildiğinde patlar. Bir kez daha, 'Los Angeles, United States, North America'(veya 'Los Angeles:United States:North America') karşı örneğimi düşünün .

Ayrıca, kelime bölme normal olarak takip eder Dosyaismi ( aka dosyayolu aka , yapılırsa, karakter içeren potansiyel bozulmuş kelime olacaktır globbing) *, ?ya da [bunu takiben ](ve eğer extglobayarlanmış, parantez fragmanları öncesinde ?, *, +, @, veya !) bunları dosya sistemi nesneleriyle eşleştirerek ve kelimeleri ("globs") uygun şekilde genişleterek. Bu üç yanıtlayıcıdan ilki, set -fglobbing'i devre dışı bırakmak için önceden çalışarak bu sorunu akıllıca azalttı . Teknik olarak bu işe yarıyor (muhtemelen eklemeniz gerekirset +f daha sonra buna bağlı olabilecek sonraki kod için globbing'i yeniden etkinleştirin), ancak yerel kodda temel bir dizeden diziye ayrıştırma işlemini kesmek için genel kabuk ayarlarıyla uğraşmak istenmez.

Bu cevapla ilgili bir başka sorun da tüm boş alanların kaybolmasıdır. Uygulamaya bağlı olarak bu bir sorun olabilir veya olmayabilir.

Not: Bu çözümü kullanacaksanız, bir komut değiştirme (kabuk çatalı) başlatma, bir boru hattı başlatma ve bir boru hattı başlatma sorununa gitmek yerine ${string//:/ }, parametre genişletme "kalıp değiştirme" biçimini kullanmak daha iyidir ve parametre genişletme tamamen kabuk-dahili bir işlem olduğundan, harici bir yürütülebilir dosya ( trveya sed) çalıştırılır. (Ayrıca, trve sedçözümleri için, giriş değişkeni komut ikamesi içinde çift tırnak içine alınmalıdır; aksi takdirde, kelime bölme echokomutta etkili olur ve alan değerlerini potansiyel olarak karıştırır. Ayrıca, $(...)komut ikamesi biçimi eskisine göre tercih edilir`...` komut ikamelerinin yuvalanmasını basitleştirdiğinden ve metin editörleri tarafından daha iyi sözdizimi vurgulamasına izin verdiğinden, form.


Yanlış cevap # 3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Bu cevap neredeyse # 2 ile aynı . Aradaki fark, cevaplayıcının alanların biri varsayılan olarak temsil edilirken $IFSdiğeri değil, iki karakterle sınırlandırıldığı varsayımını yapmış olmasıdır . IFS ile temsil edilmeyen karakteri bir desen değiştirme genişletmesi kullanarak kaldırarak ve daha sonra hayatta kalan IFS ile temsil edilen sınırlayıcı karakterdeki alanları bölmek için kelime bölme kullanarak bu oldukça özel durumu çözdü.

Bu çok genel bir çözüm değil. Ayrıca, virgülün burada gerçekten "birincil" sınırlayıcı karakter olduğu ve onu sıyırıp alan ayırma için boşluk karakterine bağlı olarak basitçe yanlış olduğu söylenebilir. Bir kez daha, benim counterexample göz önünde bulundurun: 'Los Angeles, United States, North America'.

Ayrıca, dosya adı genişletmesi genişletilmiş sözcükleri bozabilir, ancak bu, ödev için globbing'i set -fve ardından geçici olarak devre dışı bırakarak önlenebilir set +f.

Ayrıca, yine, uygulamaya bağlı bir sorun olabilecek veya olmayabilecek tüm boş alanlar kaybolacaktır.


Yanlış cevap # 4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

Bu, # 2 ve # 3'e benzerdir , çünkü işi yapmak için kelime bölmeyi kullanır, ancak şimdi kod açıkça $IFSyalnızca giriş dizesinde bulunan tek karakterlik alan sınırlayıcıyı içerecek şekilde ayarlanır . Bunun, OP'nin virgül alanı sınırlayıcısı gibi çok karakterli alan sınırlayıcıları için çalışamayacağı tekrarlanmalıdır. Ancak bu örnekte kullanılan LF gibi tek karakterli bir sınırlayıcı için, aslında mükemmel olmaya yaklaşır. Alanlar, daha önce yanlış cevaplarla gördüğümüz gibi istemeden ortada bölünemez ve gerektiğinde yalnızca bir seviye ayrılır.

Bir sorun, dosya adı genişletmesinin etkilenen kelimeleri daha önce açıklandığı gibi bozmasıdır, ancak bir kez daha bu, kritik ifadeyi set -fve içine sararak çözülebilir set +f.

Bir başka potansiyel problem, LF'nin daha önce tanımlandığı gibi bir "IFS boşluk karakteri" olarak nitelendirilmesi nedeniyle, # 2 ve # 3'teki gibi tüm boş alanların kaybolacağıdır . Sınırlayıcı "IFS boşluk karakteri" değilse bu sorun olmaz ve uygulamaya bağlı olarak yine de önemli olmayabilir, ancak çözümün genelliğini bozar.

Yani, bir tek karakteri sınırlayıcı var varsayarak, özetlemek gerekirse, ve o da olmayan bir "boşluk karakteri IFS" dır ya da boş alanlara umurumda değil ve kritik açıklama sarın set -fve set +fdaha sonra bu çözüm çalışmaları , ama başka türlü değil.

(Ayrıca, bilgi uğruna, bash içindeki bir değişkene bir LF atamak $'...'sözdizimi ile daha kolay yapılabilir , örneğin IFS=$'\n';.)


Yanlış cevap # 5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

Benzer fikir:

IFS=', ' eval 'array=($string)'

Bu çözüm etkili bir şekilde # 1 ( $IFSvirgül-boşluğuna ayarlandığı için) ve # 2-4 (dizeyi alanlara bölmek için kelime bölme kullandığından ) arasında bir çaprazlamadır . Bu nedenle, yukarıdaki yanlış cevapların tümünü etkileyen, çoğu dünyanın en kötüsü gibi sorunların çoğundan muzdariptir.

Ayrıca, ikinci varyant ile ilgili olarak, evalargümanın tek tırnaklı bir dize değişmezi olduğu için çağrı tamamen gereksiz gibi görünebilir ve bu nedenle statik olarak bilinir. Ancak aslında evalbu şekilde kullanmanın çok açık bir yararı yoktur . Normalde, yalnızca değişken bir atamadan oluşan basit bir komut çalıştırdığınızda , yani onu takip eden gerçek bir komut kelimesi olmadan, atama kabuk ortamında etkili olur:

IFS=', '; ## changes $IFS in the shell environment

Basit komut birden çok değişken ataması içeriyor olsa bile bu doğrudur ; yine, komut kelimesi olmadığı sürece, tüm değişken atamaları kabuk ortamını etkiler:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

Değişken atama komut adı takılır Ama eğer, o zaman yok (ben bu bir "ön eki atama" diyoruz) değil , bir yerleşik olup olmadığını kabuk ortamını etkileyen ve bunun yerine sadece bakılmaksızın, komutun çevreyi etkiler veya harici:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

Bash el kitabından ilgili alıntı :

Komut adı sonuçlanmazsa, değişken atamaları geçerli kabuk ortamını etkiler. Aksi takdirde, değişkenler yürütülen komutun ortamına eklenir ve geçerli kabuk ortamını etkilemez.

Değişken atamanın bu özelliğinden $IFSyalnızca geçici olarak değiştirmek mümkündür , bu $OIFSda ilk varyanttaki değişkenle yapılan gibi tüm kaydet ve geri yükle oyunundan kaçınmamızı sağlar . Ancak burada karşılaştığımız zorluk, çalıştırmamız gereken komutun kendisinin sadece değişken bir atama olması ve bu nedenle $IFSatamayı geçici hale getirmek için bir komut kelimesi içermemesidir . Kendinizi düşünebilirsiniz, neden ödevi geçici : builtinyapmak için ifadeye bir no-op komut kelimesi $IFSeklemiyorsunuz? Bu işe yaramaz çünkü $arrayatamayı geçici hale getirir :

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

Yani, etkili bir çıkmazdayız, biraz yakalama-22. Ancak, evalkodunu çalıştırdığında, normal, statik kaynak kodu gibi kabuk ortamında çalıştırır ve bu nedenle , önek ataması , kabuk ortamında etkili olması $arrayiçin evalbağımsız değişken içindeki atamayı çalıştırabiliriz. komutun $IFSönüne evaleklenirse, evalkomuttan daha uzun süre kullanılmaz . Bu, bu çözümün ikinci varyantında kullanılan hiledir:

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

Gördüğünüz gibi, aslında oldukça zekice bir hile ve tam olarak neyin gerekli olduğunu (en azından atama etkisi ile ilgili) oldukça açık olmayan bir şekilde gerçekleştiriyor. Aslında buna rağmen genel olarak bu hile karşı değilim eval; güvenlik tehditlerine karşı koruma sağlamak için bağımsız değişken dizesini tek tırnak içine almaya dikkat edin.

Fakat yine de, "tüm dünyaların en kötüsü" sorunların toplanması nedeniyle, bu OP'nin gerekliliğine hala yanlış bir cevaptır.


Yanlış cevap no.

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Um ... ne? OP bir diziye ayrıştırılması gereken bir dize değişkenine sahiptir. Bu "cevap", bir dizi değişmezine yapıştırılan giriş dizesinin sözcük içeriği ile başlar. Sanırım bunu yapmanın bir yolu var.

Yanıtlayan, $IFSdeğişkenin tüm bağlamlardaki tüm bash ayrıştırmalarını etkilediğini varsaymış olabilir , ki bu doğru değildir. Bash el kitabından:

IFS     Genişletmeden sonra sözcük bölme ve read builtin komutuyla satırları kelimelere bölme için kullanılan Dahili Alan Ayırıcı . Varsayılan değer şudur: <boşluk ><tab> <newline> .

Yani $IFSözel değişken aslında sadece iki bağlamda kullanılır: gerçekleştirilir (1) kelimesi bölünme genişleme sonrasında (yani değil tarafından kelimelere bölme girdi hatları için ve (2) bash kaynak kodunu ayrıştırılırken) readyerleşiğini.

Bunu daha netleştirmeye çalışayım. Ayrıştırma ile yürütme arasında bir ayrım yapmak iyi olabilir diye düşünüyorum . Bash ilk gerekir ayrıştırmak Açıkçası olan kaynak kodunu, ayrıştırma olayı ve daha sonra durum yürütür genişleme resmin içine geldiğinde hangi kod. Genişletme gerçekten bir yürütme olayıdır. Dahası, $IFSyukarıda alıntıladığım değişkenin tanımıyla ilgili sorun yaşıyorum; yerine kelime bölme yapılır söyleyerek daha genişlemeden sonra , o kelime bölme yapılır söyleyebilirim sırasında belki de daha doğrusu, kelime bölme olduğunu genişleme veya bir kısmıgenişleme süreci. "Sözcük bölme" ifadesi yalnızca bu genişleme adımına karşılık gelir; ne yazık ki dokümanlar "split" ve "words" kelimelerini çok fazla atmış gibi görünse de, bash kaynak kodunun ayrıştırılmasına atıfta bulunmak için asla kullanılmamalıdır. İşte bash kılavuzunun linux.die.net sürümünden ilgili bir alıntı :

Genişletme, komut satırında sözcüklere ayrıldıktan sonra gerçekleştirilir. Gerçekleştirilen yedi tür genişletme vardır: küme ayracı genişletme , tilde genişletme , parametre ve değişken genişletme , komut ikamesi , aritmetik genişletme , sözcük bölme ve yol adı genişletme .

Genişlemelerin sırası: küme ayracı genişlemesi; tilde genişletme, parametre ve değişken genişletme, aritmetik genişletme ve komut değiştirme (soldan sağa şekilde yapılır); sözcük bölme; ve yol adı genişletmesi.

Genişleme bölümünün ilk cümlesinde "kelimeler" yerine "jetonlar" kelimesini tercih ettiğinden, kılavuzun GNU sürümünün biraz daha iyi olduğunu iddia edebilirsiniz :

Genişletme, belirteçlere ayrıldıktan sonra komut satırında gerçekleştirilir.

Önemli olan, $IFSbash'ın kaynak kodunu ayrıştırma şeklini değiştirmemesi. Bash kaynak kodunun ayrıştırılması aslında komut dizileri, komut listeleri, boru hatları, parametre genişletmeleri, aritmetik ikameler ve komut ikameleri gibi kabuk gramerinin çeşitli öğelerinin tanınmasını içeren çok karmaşık bir süreçtir. Çoğunlukla, bash ayrıştırma işlemi, değişken atamaları gibi kullanıcı düzeyindeki eylemlerle değiştirilemez (aslında, bu kuralın bazı küçük istisnaları vardır; örneğin, çeşitli compatxxkabuk ayarlarına bakınanında davranışı ayrıştırma işleminin belirli yönlerini değiştirebilir). Bu karmaşık ayrıştırma işleminden kaynaklanan akış yukarı "sözcükler" / "belirteçleri", daha sonra, genişletilmiş (genişleyen?) Metnin sözcük akışının aşağı akış kısmına sözcük bölünmesi olarak genel "genişleme" işlemine göre genişletilir. kelimeler sadece bu sürecin bir adımıdır. Sözcük bölme yalnızca önceki genişletme adımından tükenmiş metne dokunur; kaynak bytestream öğesinden hemen ayrıştırılan değişmez metni etkilemez.


Yanlış cevap no. 7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

Bu en iyi çözümlerden biridir. Kullanmaya geri döndüğümüze dikkat edin read. Daha önce söylemedim mi read, çünkü bu sadece bir tane ihtiyacımız olduğunda iki seviyede bölme gerçekleştiriyor mu? Buradaki hile, readetkili bir şekilde sadece bir seviye bölme yapacak şekilde, özellikle çağrı başına sadece bir alanı bölerek çağırabilirsiniz, bu da onu bir döngüde tekrar tekrar çağırmanın maliyetini gerektirir. Biraz çabuk bir el, ama işe yarıyor.

Ama sorunlar var. Birincisi: En az bir NAME bağımsız değişkeni sağladığınızda read, giriş dizesinden ayrılmış her alanda önde gelen ve arkadaki boşlukları otomatik olarak yok sayar. Bu, $IFSbu yayında daha önce açıklandığı gibi varsayılan değerine ayarlanmış olsun veya olmasın oluşur . Şimdi, OP spesifik kullanım durumu için bunu önemsemeyebilir ve aslında, ayrıştırma davranışının arzu edilen bir özelliği olabilir. Ancak bir dizeyi alanlara ayrıştırmak isteyen herkes bunu istemeyecektir. Ancak bir çözüm var: Açıkça görülmeyen bir kullanımı readsıfır NAME argümanını iletmektir. Bu durumda, reado adında bir değişkende giriş akımından aldığını tüm giriş hattını saklayacaktır $REPLYöyle bir bonus olarak, ve değildeğerden baş ve sondaki boşlukları sıyırın. Bu, readkabuk programlama kariyerimde sık sık kullandığım çok sağlam bir kullanımdır . İşte davranıştaki farklılığın bir gösterimi:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

Bu çözümle ilgili ikinci sorun, OP'nin virgül alanı gibi özel bir alan ayırıcısının durumunu ele almamasıdır. Daha önce olduğu gibi, çok çözümlü ayırıcılar desteklenmez, bu da bu çözümün talihsiz bir sınırlamasıdır. Seçeneğe ayırıcı belirterek en azından virgül üzerinde bölünmeye çalışabiliriz -d, ama ne olduğuna bakın:

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

Tahmin edilebileceği gibi, hesaplanmamış çevreleyen beyaz alan alan değerlerine çekilmiştir ve bu nedenle daha sonra düzeltme işlemleri ile düzeltilmesi gerekecektir (bu doğrudan while döngüsü içinde de yapılabilir). Ancak başka bir açık hata daha var: Avrupa eksik! Ona ne oldu? Yanıt, readson alanda bir son alan sonlandırıcısıyla karşılaşmadan dosya sonuna (bu durumda dize sonu diyebiliriz) isabet ederse başarısız bir dönüş kodu döndürür. Bu while döngüsünün erken kırılmasına neden olur ve son alanı kaybederiz.

Teknik olarak aynı hata önceki örnekleri de etkiledi; aradaki fark, alan ayırıcısının LF olarak alınmasıdır, bu -dseçeneği belirtmediğinizde varsayılan değerdir ve <<<("here-string") mekanizması, dizeyi beslemeden hemen önce otomatik olarak bir LF ekler komuta giriş. Bu nedenle, bu durumlarda, yanlışlıkla girişe ek bir kukla sonlandırıcı ekleyerek bırakılan bir son alan sorununu yanlışlıkla çözdük. Bu çözüme "kukla sonlandırıcı" çözümü diyelim. Kukla-sonlandırıcı çözümünü herhangi bir özel sınırlayıcı için, burada dizede başlatırken giriş dizesiyle birleştirerek kendimiz uygulayabiliriz:

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Orada sorun çözüldü. Başka bir çözüm ise while döngüsünü sadece (1) readdöndürülen başarısızlık ve (2) $REPLYboşsa kırmaktır , yani readdosya sonuna gelmeden önce herhangi bir karakteri okuyamazdı. Demo:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Bu yaklaşım ayrıca <<<yeniden yönlendirme operatörü tarafından burada dizeye otomatik olarak eklenen gizli LF'yi de ortaya çıkarır . Elbette, bir an önce açıklandığı gibi açık bir kırpma işlemi ile ayrı ayrı çıkarılabilir, ancak açıkçası manuel kukla sonlandırıcı yaklaşımı doğrudan çözer, bu yüzden bununla gidebiliriz. Manuel kukla-sonlandırıcı çözümü aslında bu iki problemi (düşülen son alan problemi ve ekli LF problemi) bir seferde çözmesi açısından oldukça uygundur.

Yani, genel olarak, bu oldukça güçlü bir çözümdür. Sadece kalan zayıflık, daha sonra ele alacağım çok karakterli sınırlayıcılar için destek eksikliğidir.


Yanlış cevap no.

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Bu aslında # 7 ile aynı mesajdan; cevaplayan aynı mesajda iki çözüm sundu.)

readarrayEşanlamlıdır yerleşik, mapfileidealdir. Bir çekim akışını bir çekimde bir dizi değişkenine ayrıştıran yerleşik bir komuttur; döngülerle, koşullarla, değişikliklerle veya başka bir şeyle uğraşmak yok. Ve girdi dizesindeki boşlukları gizlice gizlemez. Ve ( -Overilmezse), atamadan önce hedef diziyi temizler. Ama yine de mükemmel değil, bu yüzden ona "yanlış cevap" olarak eleştirim.

İlk olarak, bunu yoldan çıkarmak için, tıpkı readalan ayrıştırma işlemi readarraygibi, boşsa arka alanı bıraktığını unutmayın . Yine, bu muhtemelen OP için bir endişe değildir, ancak bazı kullanım durumları için olabilir. Birazdan buna geri döneceğim.

İkincisi, daha önce olduğu gibi, çok karakterli sınırlayıcıları desteklemez. Bir an için bunun için bir düzeltme yapacağım.

Üçüncüsü, yazılı çözüm OP'nin girdi dizesini ayrıştırmaz ve aslında onu ayrıştırmak için olduğu gibi kullanılamaz. Ben de bu konuyu birazdan genişleteceğim.

Yukarıdaki nedenlerden dolayı, hala OP'nin sorusuna "yanlış bir cevap" olduğunu düşünüyorum. Aşağıda doğru cevap olduğunu düşündüğüm şeyi vereceğim.


Doğru cevap

Sadece seçeneği belirterek # 8'in çalışması için naif bir girişim -d:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Sonucun # 7'deread tartışılan döngü çözümünün çift ​​koşullu yaklaşımından elde ettiğimiz sonuçla aynı olduğunu görüyoruz . Bunu manuel kukla sonlandırıcı hile ile neredeyse çözebiliriz:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

Buradaki sorun readarray, <<<yönlendirme operatörünün LF'yi giriş dizesine eklediği ve dolayısıyla arka alanın boş olmadığı (aksi takdirde bırakılacağı) , arka alanın korunmasıdır . Gerçekte son dizi elemanını açıkça ayarlayarak bununla ilgilenebiliriz:

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Aslında ilişkili olan geriye kalan sadece iki sorun, (1) kesilmesi gereken yabancı boşluk ve (2) çok karakterli sınırlayıcılar için destek eksikliğidir.

Beyaz alan elbette daha sonra kesilebilir (örneğin, Bash değişkeninden beyaz alan nasıl kırpılır? Bölümüne bakın ). Ancak çok karakterli bir sınırlayıcıyı hackleyebiliyorsak, bu her iki sorunu da tek seferde çözecektir.

Ne yazık ki, çok karakterli bir sınırlayıcının çalışmasını sağlamanın doğrudan bir yolu yoktur . Düşündüğüm en iyi çözüm, çok karakterli sınırlayıcıyı, giriş dizesinin içeriğiyle çarpışmayacağı garanti edilecek tek karakterli bir ayırıcıyla değiştirmek için giriş dizesini önceden işlemektir. Bu garantiye sahip tek karakter NUL baytıdır . Bunun nedeni, bash (zsh içinde olmasa da, tesadüfen) değişkenlerin NUL baytını içerememesidir. Bu ön işleme aşaması, bir proses ikamesinde satır içi olarak gerçekleştirilebilir. Awk kullanarak nasıl yapılacağı aşağıda açıklanmıştır :

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

Nihayet orada! Bu çözüm hatalı olarak ortadaki alanları bölmeyecek, erken kesilmeyecek, boş alanları bırakmayacak, dosya adı genişletmelerinde kendini bozmayacak, otomatik olarak önde ve arkadaki boşlukları şeritlemeyecek, sonunda bir kaçak yolcu LF bırakmayacak, döngü gerektirmez ve tek karakterli sınırlayıcıya yerleşmez.


Düzeltme çözümü

Son olarak, belirsiz -C callbackseçeneğini kullanarak kendi oldukça karmaşık kırpma çözümümü göstermek istedim readarray. Ne yazık ki, Stack Overflow'un acımasız 30.000 karakter yazı sınırına karşı yerim bitti, bu yüzden açıklayamayacağım. Bunu okuyucu için bir egzersiz olarak bırakacağım.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

8
Ayrıca (anlaşılır bir şekilde bunu yapmak için bir alanınız olmasa da) ilk -dseçeneğin readarrayBash 4.4'te göründüğünü belirtmek de yararlı olabilir .
fbicknel

2
Harika cevap (+1). Eğer awk'ınızı awk '{ gsub(/,[ ]+|$/,"\0"); print }'finalin birleştirmesini değiştirir ve ortadan kaldırırsanız, ", " o zaman final kaydını elimine etmek için jimnastikten geçmeniz gerekmez. Yani: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")destekleyen Bash üzerinde readarray. Metodunuzun Bash 4.4+ olduğunu unutmayın. Çünkü -dinreadarray
dawg

3
@datUser Bu talihsiz. Bash sürümünüz için çok eski olması gerekir readarray. Bu durumda, üzerine kurulu ikinci en iyi çözümü kullanabilirsiniz read. Buna atıfta bulunuyorum: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";( awkçok karakterli sınırlayıcı desteğine ihtiyacınız varsa ikame ile ). Herhangi bir sorunla karşılaşırsanız bana bildirin; Eminim ki bu çözüm, yirmi yıl önce piyasaya sürülen sürüm 2-bir şeye geri dönecek şekilde oldukça eski bash sürümlerinde çalışmalıdır.
bgoldst

1
Vay be, ne harika bir cevap! Hee hee, benim cevabım: bash senaryosunu attı ve python'u kovdu!
artfulrobot

1
@datUX OSX üzerindeki bash hala 3.2'de kalmıştır (2007'de piyasaya sürülmüştür); Homebrew'da bulunan bash'i OS X'te 4.X bash sürümleri elde etmek için kullandım
JDS

222

İşte IFS'yi ayarlamadan bir yol:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

Fikir dize değiştirme kullanmaktır:

${string//substring/replacement}

$ substring öğesinin tüm eşleşmelerini boşlukla değiştirmek ve ardından diziyi başlatmak için değiştirilen dizeyi kullanmak için:

(element1 element2 ... elementN)

Not: Bu cevap split + glob operatörünü kullanır . Bu nedenle, bazı karakterlerin (örneğin *) genişlemesini önlemek için, bu komut dosyası için globbing'i duraklatmak iyi bir fikirdir.


1
Bu yaklaşımı kullandım ... bölünmek için uzun bir iple karşılaşana kadar. Bir dakikadan fazla% 100 CPU (sonra öldürdüm). Yazık çünkü bu yöntem IFS'deki bazı karakterlere değil, bir dizeye bölünmeye izin veriyor.
Werner Lehmann

Bir dakikadan fazla% 100 CPU zamanı bana bir yerde yanlış bir şey olması gerektiği gibi geliyor. Bu dize ne kadar sürdü, MB veya GB boyutunda mı? Bence, normalde, sadece küçük bir dize bölünmesine ihtiyacınız olacaksa, Bash içinde kalmak istersiniz, ancak büyük bir dosyaysa, bunu yapmak için Perl gibi bir şey yürütürüm.

12
UYARI: Bu yaklaşımla ilgili bir sorunla karşılaştım. * Adında bir öğeniz varsa, cwd'nizin tüm öğelerini de alırsınız. böylece string = "1: 2: 3: 4: *" uygulamanıza bağlı olarak beklenmedik ve muhtemelen tehlikeli sonuçlar verecektir. (IFS = ',' read -a array <<< "$ string") ile aynı hatayı alamadım ve bu kullanmak güvenli görünüyor.
Dieter Gribnitz

4
alıntı ${string//:/ }kabuk genişlemesini önler
Andrew White

1
OSX'te aşağıdakileri kullanmak zorunda kaldım:array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Üç yazdırır


8
Aslında bu yaklaşımı tercih ediyorum. Basit.
shrimpwagon

4
Bunu kopyalayıp yapıştırdım ve yankı ile işe yaramadı, ancak bir for döngüsünde kullandığımda işe yaradı.
Ben

2
Bu belirtildiği gibi çalışmaz. @ Jmoney38 veya karides arabası, bunu bir terminale yapıştırabilir ve istediğiniz çıktıyı alabilirseniz, lütfen sonucu buraya yapıştırın.
abalter

2
@abalter Benim için çalışıyor a=($(echo $t | tr ',' "\n")). İle aynı sonuç a=($(echo $t | tr ',' ' ')).
yaprak

@procrastinator VERSION="16.04.2 LTS (Xenial Xerus)"Bir bashkabukta denedim ve son olarak echoboş bir satır yazdırıyor. Hangi Linux sürümünü ve hangi kabuğu kullanıyorsunuz? Ne yazık ki, terminal oturumunu bir yorumda görüntüleyemezsiniz.
abalter

29

Bazen kabul edilen cevapta tarif edilen yöntemin, özellikle de ayırıcı bir satır başı ise, işe yaramadı.
Bu gibi durumlarda şu şekilde çözdüm:

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 Bu tamamen benim için çalıştı. Ben bir dizi içine bir satırsonu ile bölünmüş birden fazla dizeleri koymak gerekiyordu, ve read -a arr <<< "$strings"ile çalışmadı IFS=$'\n'.
Stefan van den Akker


Bu asıl soruya tam olarak cevap vermiyor.
Mike

29

Kabul edilen cevap bir satırdaki değerler için geçerlidir.
Değişkenin birkaç satırı varsa:

string='first line
        second line
        third line'

Tüm satırları almak için çok farklı bir komuta ihtiyacımız var:

while read -r line; do lines+=("$line"); done <<<"$string"

Ya da çok daha basit bash readarray :

readarray -t lines <<<"$string"

Bir printf özelliğinden yararlanarak tüm satırları yazdırmak çok kolaydır:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
Her çözüm her durum için işe yaramazken, tekrardan bahsetmeniz ... son iki saatimi 5 dakika ile değiştirdi ... oyumu aldınız
Angry 84


6

Dizenizi bir diziye bölmenin anahtarı, çok karakterli sınırlayıcısıdır ", ". IFSÇok karakterli sınırlayıcılar için kullanılan herhangi bir çözüm , IFS bir dize değil, bu karakterlerden oluşan bir küme olduğundan doğası gereği yanlıştır.

Eğer atarsanız IFS=", "o zaman dize BİRİ kıracak ","OR " "veya bunların herhangi bir kombinasyonu olan iki karakter sınırlayıcı doğru bir temsili değil ", ".

Sen kullanabilirsiniz awkveya sedsüreç ikamesi ile dize bölmek:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Doğrudan Bash'te bir normal ifadeyi kullanmak daha etkilidir:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

İkinci formda, alt kabuk yoktur ve doğal olarak daha hızlı olacaktır.


Bgoldst tarafından düzenleyin: İşte çözümümü dawg'ninreadarray regex çözümüyle karşılaştıran bazı kriterler ve ben de onun readçözümünü ekledim (not: çözümümle daha fazla uyum sağlamak için normal ifade çözümünü biraz değiştirdim) (ayrıca aşağıdaki yorumlarıma bakın) İleti):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

Çok güzel bir çözüm! Normal bir maçta bir döngü kullanmayı hiç düşünmedim, şık kullanımı $BASH_REMATCH. Çalışır ve gerçekten alt kabukları yumurtlamaktan kaçınır. Benden +1. Bununla birlikte, eleştiriyle, normal ifadenin kendisi biraz ideal değildir, çünkü açgözlü olmayan çarpanlar için destek eksikliği etrafında çalışmak için sınırlayıcı belirtecinin bir kısmını (özellikle virgül) çoğaltmak zorunda kaldınız. (ayrıca etraf), bash içine yerleştirilmiş "genişletilmiş" regex aroması). Bu onu biraz daha genel ve sağlam hale getirir.
bgoldst

İkincisi, bazı kıyaslama yaptım ve performans ufacık dizeler için diğer çözümlerden daha iyi olmasına rağmen, tekrarlanan dizi yeniden oluşturma nedeniyle katlanarak daha da kötüleşiyor ve çok büyük dizeler için felaket oluyor. Cevabınızdaki düzenlememe bakın.
bgoldst

@bgoldst: Ne harika bir kriter! Normal ifadeyi savunmak için, binlerce alanın (normal ifadenin bölündüğü) 10'lu veya 100'lü alanlar için muhtemelen \nbu alanları içeren bir tür kayıt ( sınırlandırılmış metin satırları gibi ) olacaktır, bu nedenle felaket yavaşlaması muhtemelen gerçekleşmeyecektir. 100.000 alan içeren bir dizeniz varsa - belki Bash ideal değildir ;-) Benchmark için teşekkürler. Bir iki şey öğrendim.
dawg

4

Saf bash çok karakterli sınırlayıcı çözüm.

Diğerlerinin bu iş parçacığında işaret ettiği gibi, OP'nin sorusu bir diziye ayrıştırılmak üzere virgülle ayrılmış bir dize örneği verdi, ancak yalnızca virgül sınırlayıcılar, tek karakter sınırlayıcılar veya çok karakterle ilgilenip ilgilenmediğini göstermedi sınırlayıcı.

Google bu yanıtı arama sonuçlarının üstünde veya üstünde sıralamaya meyilli olduğu için, okuyuculara birden fazla karakter sınırlayıcı sorusuna güçlü bir cevap vermek istedim, çünkü bu en az bir yanıtta da belirtildi.

Çok karakterli bir sınırlayıcı sorununa çözüm arıyorsanız, Mallikarjun M'nin gönderisini, özellikle parametre genişletmeyi kullanarak bu zarif saf BASH çözümünü sağlayan gniourf_gniourf'un yanıtını gözden geçirmenizi öneririm :

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Alıntı yapılan yoruma / referans verilen gönderiye bağlantı

Atıfta bulunulan soruya bağlantı: Çok karakterli bir sınırlayıcıdaki bir dizeyi bash'da nasıl bölerim?


1
Bkz yorumumu benzer ama gelişmiş yaklaşım için.
xebeche

3

Bu benim için OSX'te çalışır:

string="1 2 3 4 5"
declare -a array=($string)

Dizeniz farklı sınırlayıcıya sahipse, sadece 1'ini boşluklarla değiştirin:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Basit :-)


Bir artı olan Bash ve Zsh için çalışır!
Elijah W.Gagne

2

IFS'yi değiştirmeden yapmanın başka bir yolu:

read -r -a myarray <<< "${string//, /$IFS}"

IFS'yi istediğiniz sınırlayıcıyla eşleşecek şekilde değiştirmek yerine , istediğimiz sınırlayıcının tüm oluşumlarını via ", "içeriğiyle değiştirebiliriz .$IFS"${string//, /$IFS}"

Belki de bu çok büyük dizeler için yavaş olacaktır?

Bu Dennis Williamson'ın cevabına dayanıyor.


2

Word1, word2, ... gibi bir girdi ayrıştırmaya çalışırken bu yazıyla karşılaştım.

Yukarıdakilerin hiçbiri bana yardımcı olmadı. awk kullanarak çözdü. Birine yardımcı olursa:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

Bunu dene

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Basit. İsterseniz, bir bildirim ekleyebilir (ve virgülleri de kaldırabilirsiniz):

IFS=' ';declare -a array=(Paris France Europe)

IFS yukarıdakileri geri almak için eklenir, ancak taze bash örneğinde olmadan çalışır


1

Dizeyi dizi nesnesine bölmek için tr komutunu kullanabiliriz. Hem MacOS hem de Linux'ta çalışır

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Başka bir seçenek IFS komutunu kullan

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Bunu kullan:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
Kötü: sözcük bölme ve yol adı genişletmeye tabidir. Kötü cevaplar vermek için lütfen eski soruları iyi cevaplarla canlandırmayın.
gniourf_gniourf

2
Bu kötü bir cevap olabilir, ancak yine de geçerli bir cevaptır. İşaretçiler / yorumcular: Bunun gibi yanlış cevaplar için, aşağı oy, silmeyin!
Scott Weldon

2
@gniourf_gniourf Bunun neden kötü bir cevap olduğunu açıklayabilir misiniz? Ne zaman başarısız olduğunu gerçekten anlamıyorum.
George Sovetov

3
@GeorgeSovetov: Dediğim gibi, kelime bölme ve yol adı genişletmeye tabi. Daha genel olarak, bir dizeyi array=( $string )(ne yazık ki çok yaygın) bir antipattern olduğu gibi bir diziye bölmek : kelime bölme gerçekleşir string='Prague, Czech Republic, Europe':; Yol adı genişletmesi oluşur: string='foo[abcd],bar[efgh]'ör. foodVeya barfdizininizde bir dosya varsa başarısız olur . Böyle bir yapının tek geçerli kullanımı ne zaman stringbir küredir.
gniourf_gniourf

0

GÜNCELLEME: eval ile ilgili sorunlar nedeniyle bunu yapmayın.

Biraz daha az törenle:

IFS=', ' eval 'array=($string)'

Örneğin

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
eval kötüdür! bunu yapma.
caesarsol

1
Pfft. Hayır. Bunun için yeterince büyük betikler yazıyorsanız, yanlış yapıyorsunuz demektir. Uygulama kodunda eval kötüdür. Kabuk kodlamasında yaygın, gerekli ve önemsizdir.
user1009908

2
$Değişkenine bir koy ve göreceksin ... Pek çok senaryo yazdım ve hiç bir zaman tek bir kullanmak zorunda eval
kalmadım

2
Haklısınız, bu yalnızca girişin temiz olduğu biliniyorsa kullanılabilir. Sağlam bir çözüm değil.
user1009908

Eval'ı kullanmak zorunda olduğum tek zaman, kendi kodunu / modüllerini kendi kendine oluşturacak bir uygulama içindi ... VE bunun hiçbir zaman kullanıcı girişi yoktu ...
Angry 84

0

İşte benim hack!

Dizeleri dizelerle bölmek, bash kullanarak yapmak oldukça sıkıcı bir şeydir. Olan şey, sadece birkaç durumda (";", "/", "." Ve benzeri) çalışan sınırlı yaklaşımlara sahip olduğumuz ya da çıktılarda çeşitli yan etkilere sahip olduğumuzdur.

Aşağıdaki yaklaşım bir dizi manevra gerektirdi, ancak bunun çoğu ihtiyacımız için işe yarayacağına inanıyorum!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

Çok satırlı elemanlar için neden böyle bir şey olmasın?

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Başka bir yol:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Şimdi öğeleriniz "dizi" dizisinde saklanır. Öğeleri yinelemek için:

for i in ${arr[@]}; do echo $i; done

1
Cevabımda bu fikri ele alıyorum ; bkz. Yanlış cevap # 5 (özellikle evalhile tartışmamla ilgileniyor olabilirsiniz ). Çözümünüz $IFS, olaydan sonra virgül-boşluk değerine ayarlanmıştır.
bgoldst

-1

Bunu çözmenin pek çok yolu olduğundan, çözümümüzde ne görmek istediğimizi tanımlayarak başlayalım.

  1. Bash readarraybu amaç için bir yapı sağlar. Kullanalım.
  2. Ek bir öğeyi değiştirmek IFS, değiştirmek , kullanmak evalveya eklemek gibi çirkin ve gereksiz hilelerden kaçının ve çıkarın.
  3. Benzer sorunlara kolayca uyarlanabilen basit, okunabilir bir yaklaşım bulun.

readarrayKomut ayırıcı olarak satırsonu ile kullanmak en kolay yoldur. Diğer sınırlayıcılarla diziye fazladan bir öğe ekleyebilir. En temiz yaklaşım, girdimizi ilk readarrayönce onu geçirmeden önce iyi çalışan bir forma uyarlamaktır .

Bu örnekteki girdi gelmez değil bir çok karakterli ayırıcı var. Biraz sağduyu uygularsak, en iyi şekilde her bir öğenin kırpılması gerekebilecek virgülle ayrılmış girdi olarak anlaşılır. Benim çözümüm girdiyi virgülle birden çok satıra bölmek, her öğeyi kırpmak ve hepsini iletmektir readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Başka bir yaklaşım şunlar olabilir:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Bundan sonra 'arr' dört dizeli bir dizidir. Bu, IFS veya okuma veya diğer özel şeylerle uğraşmayı gerektirmez, bu nedenle çok daha basit ve doğrudan.


Diğer cevaplarla aynı (ne yazık ki yaygın) antipattern: kelime bölme ve dosya adı genişletmeye tabi.
gniourf_gniourf
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.