Bash döngüsü listesindeki beyaz boşluktan nasıl kaçabilirim?


121

Belirli bir dizinin tüm alt dizinlerinde (ancak dosyalarında değil) döngü yapan bir bash kabuk betiğim var. Sorun, bazı dizin adlarının boşluk içermesidir.

İşte test rehberimin içeriği:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Ve dizinlerde dolaşan kod:

for f in `find test/* -type d`; do
  echo $f
done

İşte çıktı:

Test / Baltimore
Test / Kiraz
Tepe
Test / Edison 
Test / Yeni
York
Kent
Test / Philadelphia

Cherry Hill ve New York City, 2 veya 3 ayrı giriş olarak değerlendirilir.

Dosya adlarından alıntı yapmayı denedim, şöyle:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

ama boşuna.

Bunu yapmanın basit bir yolu olmalı.


Aşağıdaki cevaplar harika. Ancak bunu daha karmaşık hale getirmek için - her zaman test dizinimde listelenen dizinleri kullanmak istemiyorum. Bazen dizin adlarını bunun yerine komut satırı parametreleri olarak geçirmek istiyorum.

Charles'ın IFS'yi kurma önerisini aldım ve şunu buldum:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

ve bu, komut satırı argümanlarında boşluk olmadıkça (bu argümanlar alıntılanmış olsa bile) gayet iyi çalışır. Örneğin, komut dosyasını test.sh "Cherry Hill" "New York City"şu şekilde çağırmak: aşağıdaki çıktıyı üretir:

Kiraz
Tepe
Yeni
York
Kent

re: edit, list="$@"orijinal değerin listesini tamamen atarak onu bir dizeye daraltır. Lütfen cevabımdaki uygulamaları aynen verildiği gibi takip edin - böyle bir görev burada hiçbir yerde teşvik edilmemektedir; Bir programa komut satırı bağımsız değişkenlerinin bir listesini iletmek istiyorsanız, bunları bir dizide toplamanız ve bu diziyi doğrudan genişletmeniz gerekir.
Charles Duffy

Yanıtlar:


105

İlk önce, bunu bu şekilde yapma. En iyi yaklaşım, find -execdoğru şekilde kullanmaktır :

# this is safe
find test -type d -exec echo '{}' +

Diğer güvenli yaklaşım, NUL ile sonlandırılmış listeyi kullanmaktır, ancak bu, bulma desteğinizi gerektirir -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Ayrıca find'dan bir diziyi doldurabilir ve bu diziyi daha sonra iletebilirsiniz:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Bulmanız desteklemiyorsa -print0, sonucunuz güvensizdir - adlarında satırsonu içeren dosyalar varsa (evet, yasaldır) aşağıdakiler istenildiği gibi davranmayacaktır:

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Yukarıdakilerden birini kullanmayacaksanız, üçüncü bir yaklaşım (kelime bölme yapmadan önce alt işlemin tüm çıktısını okuduğu için hem zaman hem de bellek kullanımı açısından daha az verimli), kullanmayan bir IFSdeğişken kullanmaktır. boşluk karakteri içermiyor. (Globbing kapatma set -fgibi glob karakterleri içeren dizeleri önlemek için) [], *ya da ?genişletilmiş olmaktan:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Son olarak, komut satırı parametresi durumu için, eğer kabuğunuz destekliyorsa dizileri kullanmalısınız (yani ksh, bash veya zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

ayrılığı koruyacak. Alıntı yapmanın (ve $@yerine kullanımının $*) önemli olduğuna dikkat edin. Diziler, glob ifadeleri gibi başka şekillerde de doldurulabilir:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1
-exec için bu '+' çeşidini bilmiyordum. tatlı
Johannes Schaub - litb

1
xargs gibi argümanları yalnızca verilen komutun sonuna koyabilir gibi görünüyor: / bu beni bazen
rahatsız ediyor

-Exec [ad] {} + 'nın bir GNU ve 4.4-BSD uzantısı olduğunu düşünüyorum. (En azından Solaris 8'de görünmüyor ve ben de AIX 4.3'te olduğunu düşünmüyorum.) Geri
kalanımızın da xargs'a bağlanmış

2
Daha önce $ '\ n' sözdizimini hiç görmemiştim. Bu nasıl çalışıyor? (IFS = '\ n' veya IFS = "\ n" nin işe yarayacağını düşünmüştüm, ama işe yaramıyor.)
MCS

1
@crosstalk kesinlikle Solaris 10'da, sadece kullandım.
Nick

26
find . -type d | while read file; do echo $file; done

Ancak, dosya adı satırsonu içeriyorsa çalışmaz. Dizin adını bir değişkende ne zaman kullanmak istediğinizi bildiğim tek çözüm yukarıdakidir. Eğer sadece bir komut çalıştırmak istiyorsanız, xargs kullanın.

find . -type d -print0 | xargs -0 echo 'The directory is: '

Xargs'a gerek yok, find -exec ... {} bölümüne bakın +
Charles Duffy

4
@Charles: Çok sayıda dosya için, xargs çok daha etkilidir: yalnızca bir işlem üretir. -Exec seçeneği, her dosya için daha yavaş bir sıra olabilecek yeni bir işlemi zorlar.
Adam Rosenfield

1
Xargs'ı daha çok seviyorum. Bu ikisi aslında her ikisini de yapıyor gibi görünüyor, ancak xargs'ın paralel koşmak gibi daha fazla seçeneği var
Johannes Schaub - litb

2
Adam, hayır bu '+' mümkün olduğu kadar çok dosya adı toplayacak ve sonra çalıştıracak. ancak paralel koşmak gibi düzgün işlevleri olmayacak :)
Johannes Schaub - litb

2
Dosya adlarıyla bir şeyler yapmak istiyorsanız, bunlardan alıntı yapmanız gerekeceğini unutmayın. Örneğin:find . -type d | while read file; do ls "$file"; done
David Moles

23

Dosya adındaki sekmeleri ve / veya beyaz boşlukları işleyen basit bir çözüm. Yeni satırlar gibi dosya adındaki diğer garip karakterlerle uğraşmanız gerekiyorsa, başka bir yanıt seçin.

Test rehberi

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Dizinlere girilecek kod

find test -type d | while read f ; do
  echo "$f"
done

Bağımsız "$f"değişken olarak kullanılıyorsa dosya adı tırnak ( ) olmalıdır . Tırnak işaretleri olmadan, boşluklar bağımsız değişken ayırıcı görevi görür ve çağrılan komuta birden çok bağımsız değişken verilir.

Ve çıktı:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

teşekkürler, bu, geçerli klasördeki her dizinin ne kadar alan kullandığını listelemek için oluşturduğum takma ad için çalıştı, önceki enkarnasyondaki boşluklarla bazı dizinlerde boğuluyordu. Bu zsh çalışır, ancak diğer bazı yanıtlar vermedi:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid

2
Zsh kullanıyorsanız, bunu da yapabilirsiniz: alias duc='du -sh *(/)'
Zsh

@cbliard Bu hala arabalı. Bir dosya adıyla, örneğin bir sekme dizisi veya birden çok boşlukla çalıştırmayı deneyin; yankınızda alıntı yapmadığınız için bunlardan herhangi birini tek bir boşlukla değiştirdiğini fark edeceksiniz. Ve bir de satırsonu içeren dosya adları var ...
Charles Duffy

@CharlesDuffy Sekme dizileri ve birden fazla boşlukla denedim. Alıntılarla çalışır. Yeni satırlarla da denedim ve hiç çalışmıyor. Cevabı buna göre güncelledim. Bunu belirttiğiniz için teşekkür ederim.
cbliard

1
@cbliard Doğru - echo komutunuza alıntılar eklemek, elde ettiğim şeydi. Yeni satırlara gelince, bul -print0ve kullanarak bu çalışmayı yapabilirsiniz IFS='' read -r -d '' f.
Charles Duffy

7

Bu, standart Unix'te son derece yanıltıcıdır ve çoğu çözüm satırsonu veya başka bir karakterle kötü çalışır. Ancak, GNU araç setini kullanıyorsanız, bu findseçeneği -print0kullanabilir xargsve karşılık gelen seçenekle -0(eksi sıfır) kullanabilirsiniz. Basit bir dosya adında bulunamayan iki karakter vardır; bunlar eğik çizgi ve NUL '\ 0'. Açıktır ki, eğik çizgi, yol adlarında görünür, bu nedenle, adın sonunu işaretlemek için bir NUL '\ 0' kullanmanın GNU çözümü ustaca ve aptallıktan uzaktır.


4

Neden koymuyorsun

IFS='\n'

for komutunun önünde? Bu, alan ayırıcısını <Boşluk> <Sekme> <Yeni satır> yerine yalnızca <Yeni Satır> olarak değiştirir.


4
find . -print0|while read -d $'\0' file; do echo "$file"; done

1
-d $'\0'tam olarak aynıdır -d ''- bash, NUL ile sonlandırılmış dizeler kullandığı için, boş bir dizenin ilk karakteri bir NUL'dur ve aynı nedenle, NUL'lar C dizgilerinin içinde hiçbir şekilde temsil edilemez.
Charles Duffy

4

kullanırım

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Bu yeterli olmaz mı? Http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html adresinden
alınan fikir


harika ipucu: bu, boşlukların bir bağımsız değişkeni birden çok parametreye böldüğü bir komut satırı osascript (OS X AppleScript) seçenekleri için çok yararlıdır, burada yalnızca birinin amaçlandığı
tim

Hayır, yeterli değil. Verimsizdir (gereksiz kullanımı nedeniyle $(echo ...)), glob ifadelerine sahip dosya adlarını doğru şekilde işlemez, $'\b'veya $ '\ n' karakterleri içeren dosya adlarını doğru şekilde işlemez ve dahası, birden fazla beyaz alanı çalıştırmayı tek beyaz boşluk karakterine dönüştürür. yanlış alıntı nedeniyle çıktı tarafı.
Charles Duffy

4

Listeleri dize olarak saklamayın; Tüm bu sınırlayıcı kafa karışıklığını önlemek için bunları diziler olarak depolayın. Aşağıda, testin tüm alt dizinlerinde veya komut satırında sağlanan liste üzerinde çalışacak örnek bir komut dosyası verilmiştir:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Şimdi bunu bir veya iki eğri içeren bir test dizininde deneyelim:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1
Bu taraftaki geri Looking - aslında orada oldu POSIX sh bir çözüm: yeniden verebilecek "$@"ile kendisine ekleme, dizi set -- "$@" "$f".
Charles Duffy

4

IFS'yi (dahili alan ayırıcı) geçici olarak şu şekilde kullanabilirsiniz:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS


Lütfen açıklama yapınız.
Steve K

IFS, ayırıcı sembolün ne olduğunu belirtti, ardından beyaz boşluklu dosya adı kesilmeyecekti.
muhteşem10

$ IFS = $ OLD_IFS sonunda şu şekilde olmalıdır: IFS = $ OLD_IFS
Michel

3

ps sadece girdideki boşlukla ilgiliyse, bazı çift tırnak işaretleri benim için sorunsuz çalıştı ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;

2

Ne eklemek için Jonathan söyledi: kullanmak -print0için seçeneği findile birlikte xargsaşağıdaki şekilde:

find test/* -type d -print0 | xargs -0 command

Bu, komutu commanduygun argümanlarla çalıştıracaktır; İçlerinde boşluklar olan dizinler uygun şekilde alıntılanacaktır (yani, tek bir argüman olarak aktarılacaklar).


1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Yukarıdaki kod .mov dosyalarını .avi'ye dönüştürecektir. .Mov dosyaları farklı klasörlerdedir ve klasör adlarında da boşluklar vardır . Yukarıdaki komut dosyam .mov dosyalarını aynı klasörde .avi dosyasına dönüştürecek. İnsanlara yardım edip etmediğini bilmiyorum.

Durum:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Şerefe!


echo "$name" | ...eğer işi yapmaz nameise -nve ters eğik çizgi-çıkış sıralarını adları ile nasıl davranacağını da uygulanmasına bağlı - POSIX davranışını yapar echoXSI-genişletilmiş oysa bu durumda açıkça tanımlanmamış (POSIX ters eğik çizgi-kaçış dizileri standart tanımlı davranış genişlemesini yapar ve GNU sistemleri - bash dahil - olmadan POSIXLY_CORRECT=1aradan uygulayarak POSIX standardı -e(spec gerektirir oysa echo -eyazdırmak için -e. çıkışına) printf '%s\n' "$name" | ...güvenlidir.
Charles Duffy

1

Yol adlarında da boşluklarla uğraşmak zorundaydı. Sonunda yaptığım şey bir özyineleme kullanmaktı ve for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}

1
functionAnahtar kelimeyi kullanmayın - kodunuzu POSIX sh ile uyumsuz hale getirir, ancak başka yararlı bir amacı yoktur. recursedir() {İki pareni ekleyerek ve işlev anahtar sözcüğünü kaldırarak bir işlev tanımlayabilirsiniz ve bu, tüm POSIX uyumlu kabuklarla uyumlu olacaktır.
Charles Duffy

1

Dosya listesini Bash dizisine dönüştürün. Bu, Matt McClure'un Bash işlevinden bir dizi döndürme yaklaşımını kullanır: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Sonuç bir yoldur herhangi bir çok satırlı girişi Bash dizisine dönüştürmek için.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Bu yaklaşım, kötü karakterler varken bile işe yarıyor gibi görünüyor ve herhangi bir girdiyi Bash dizisine dönüştürmenin genel bir yoludur. Dezavantajı, giriş uzunsa, Bash'in komut satırı boyut sınırlarını aşabilir veya büyük miktarda bellek kullanabilirsiniz.

Listede nihayetinde çalışan döngünün aynı zamanda borulu listeye sahip olduğu yaklaşımlar, stdin okumanın kolay olmaması (kullanıcıdan girdi istemek gibi) ve döngünün yeni bir süreç olması dezavantajına sahiptir, bu nedenle değişkenlerin nedenini merak ediyor olabilirsiniz. Döngü içinde ayarladığınız döngü bittikten sonra kullanılamaz.

Ayrıca IFS'yi ayarlamayı sevmiyorum, diğer kodları bozabilir.


IFS='' readAynı satırda kullanırsanız , IFS ayarı yalnızca okuma komutu için mevcuttur ve ondan çıkmaz. IFS'yi bu şekilde ayarlamaktan hoşlanmamak için hiçbir neden yok.
Charles Duffy

1

Pek çok karmaşık cevap görüyorum. Find yardımcı programının çıktısını geçirmek veya bir döngü yazmak istemiyorum, çünkü find bunun için "exec" seçeneğine sahip.

Benim sorunum, dbf uzantılı tüm dosyaları mevcut klasöre taşımak istemem ve bazılarının beyaz boşluk içermesiydi.

Ben de öyle hallettim:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Benim için çok basit görünüyor


0

Benim arasında bazı benzerlikler vardır dışarı sadece bulundu soruya ve sizin. Bağımsız değişkenleri komutlara geçirmek istiyorsanız,

test.sh "Cherry Hill" "New York City"

sırayla yazdırmak için

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

$ @ karakterinin çift tırnak içine alındığına dikkat edin, burada bazı notlar


0

Belirli bir klasördeki birkaç dizini veya dosyayı sırayla sıkıştırmak için aynı konsepte ihtiyacım vardı. Listeyi ls'den ayrıştırmak ve adda boşluk sorununu önlemek için awk kullanarak çözdüm.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

ne düşünüyorsun?


Dosya adlarında yeni satırlar varsa bunun düzgün çalışmayacağını düşünüyorum. Belki de denemelisin.
user000001


-3

Benim için bu işe yarıyor ve neredeyse "temiz":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done

4
Ama bu daha kötü. Bulmanın etrafındaki çift tırnak işaretleri, tüm yol adlarının tek bir dize olarak birleştirilmesine neden olur. Değişim yankı bir etmek ls sorunu görmek için.
NVRAM

-4

Sadece basit bir varyant problemi vardı ... Yazılan .flv dosyalarını .mp3'e (esneme) dönüştürün.

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

Tüm Macintosh kullanıcı flash dosyalarını özyinelemeli olarak bulun ve bunları sese dönüştürün (kopyalama, kod dönüştürme yok) ... bu yukarıdaki süre gibi, sadece 'içinde dosya için' yerine okuduğuna dikkat edin .


2
readSonra insen yineleme ediyoruz listesinde bir daha kelimedir. Gönderdiğiniz şey, soruyu soranın sahip olduklarının biraz bozuk bir versiyonudur ve işe yaramaz. Farklı bir şey yayınlamayı düşünmüş olabilirsiniz, ancak yine de muhtemelen burada başka cevaplarla kaplıdır.
Gilles 'SO- kötü olmayı bırak'
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.