it-swarm-tr.com

Adları boşluk olan dosyalar arasında dolaşmak mı?

İki direktörün çıktılarını aynı dosyalara sahip olarak ayırmak için aşağıdaki komut dosyasını yazdım:

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

Bunu başarmanın başka yolları olduğunu biliyorum. Merakla, dosyaları içinde boşluk olduğunda bu komut dosyası başarısız olur. Bununla nasıl başa çıkabilirim?

Buluntu örneği:

./zQuery - abc - Do Not Prompt for Date.csv
160
Amir Afghani

Kısa cevap (cevabınıza en yakın ancak boşlukları işler)

OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
     echo "file = $file"
     diff "$file" "/some/other/path/$file"
     read line
done
IFS="$OIFS"

Daha iyi yanıt (dosya adlarında joker karakterleri ve yeni satırları da işler)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

En iyi cevap ( Gilles'in cevabına göre )

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

Ya da daha iyisi, dosya başına bir sh çalıştırmamak için:

find . -type f -name '*.csv' -exec sh -c '
  for file do
    echo "$file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
  done
' sh {} +

Uzun cevap

Üç sorununuz var:

  1. Kabuk varsayılan olarak bir komutun çıktısını boşluklara, sekmelere ve yeni satırlara böler
  2. Dosya adları genişletilecek joker karakterler içerebilir
  3. Adı *.csv İle biten bir dizin varsa ne olur?

1. Yalnızca yeni satırlara bölme

file olarak neyin ayarlanacağını anlamak için Kabuk find çıktısını almalı ve bir şekilde yorumlamalıdır, aksi takdirde file yalnızca find.

Kabuk, varsayılan olarak <space><tab><newline> Olarak ayarlanmış olan IFS değişkenini okur.

Sonra find çıktısındaki her karaktere bakar. IFS içindeki herhangi bir karakteri görür görmez, dosya adının sonunu işaretlediğini düşünür, bu yüzden file öğesini şimdiye kadar gördüğü karakterlere ayarlar ve döngüyü çalıştırır. Sonra bir sonraki dosya adını almak için kaldığı yerden başlar ve çıktının sonuna ulaşana kadar bir sonraki döngüyü vb. Çalıştırır.

Yani bunu etkili bir şekilde yapıyor:

for file in "zquery" "-" "abc" ...

Girdiyi yalnızca yeni satırlara bölmesini söylemek için yapmanız gerekenler

IFS=$'\n'

for ... find komutundan önce.

Bu, IFS değerini tek bir satırsonuna ayarlar; böylece boşluklar ve sekmeler de değil, yalnızca satırsonlar üzerinde bölünür.

ksh93, sh veya dash yerine bash veya zsh kullanıyorsanız, IFS=$'\n' Yazmanız gerekir bunun gibi:

IFS='
'

Bu muhtemelen betiğinizin çalışması için yeterlidir, ancak diğer bazı köşe durumlarını düzgün bir şekilde ele almak istiyorsanız, okumaya devam edin ...

2. Joker karakterler olmadan $file Öğesini genişletme

Döngünün içinde yaptığınız yer

diff $file /some/other/path/$file

kabuk $file 'u (tekrar!) genişletmeye çalışır.

Boşluklar içerebilir, ancak yukarıda IFS ayarını yaptığımız için bu bir sorun olmayacak.

Ancak, * Veya ? Gibi joker karakterler de içerebilir ve bu da öngörülemeyen davranışlara yol açar. (Gilles'e bunu işaret ettiği için teşekkürler.)

Kabuk'a joker karakterleri genişletmemesini söylemek için, değişkeni çift tırnak içine alın (ör.

diff "$file" "/some/other/path/$file"

Aynı sorun bizi

for file in `find . -name "*.csv"`

Örneğin, bu üç dosyaya sahipseniz

file1.csv
file2.csv
*.csv

(çok olası değil, ancak yine de mümkün)

Sanki kaçmış gibisin

for file in file1.csv file2.csv *.csv

hangi genişleyecek

for file in file1.csv file2.csv *.csv file1.csv file2.csv

file1.csv ve file2.csv öğelerinin iki kez işlenmesine neden oluyor.

Bunun yerine, yapmalıyız

find . -name "*.csv" -print | while IFS= read -r file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

read, standart girdideki satırları okur, satırı IFS 'a göre kelimelere böler ve belirttiğiniz değişken adlarında saklar.

Burada, satırı kelimelere bölmemesini ve satırı $file İçine kaydetmemesini söylüyoruz.

Ayrıca read line İfadesinin read line </dev/tty Olarak değiştiğini unutmayın.

Bunun nedeni, döngü içinde, standart girişin find kanalından gelmesidir.

read yapsaydık, dosya adının bir kısmını veya tamamını tüketir ve bazı dosyalar atlanır.

/dev/tty, Kullanıcının komut dosyasını çalıştırdığı terminaldir. Komut dosyası cron üzerinden çalıştırılırsa bu bir hataya neden olacağını unutmayın, ancak bu durumda bunun önemli olmadığını varsayalım.

Peki, bir dosya adı yeni satır içeriyorsa ne olur?

Bunu -print Öğesini -print0 Olarak değiştirerek ve bir boru hattının sonunda read -d '' Kullanarak halledebiliriz:

find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read char </dev/tty
done

Bu, find öğesinin her dosya adının sonuna boş bir bayt koymasını sağlar. Boş adlar, dosya adlarında izin verilmeyen tek karakterdir, bu nedenle, ne kadar garip olursa olsun, tüm olası dosya adlarını işlemelidir.

Diğer taraftan dosya adını almak için IFS= read -r -d '' Kullanıyoruz.

Yukarıda read kullandığımız yerde, yeni satırın varsayılan satır sınırlayıcısını kullandık, ancak şimdi find satır sınırlayıcı olarak null kullanıyor. bash 'da, bir bağımsız değişkente bir NUL karakterini bir komuta (yerleşik olanlar bile) geçiremezsiniz, ancak bash, -d ''' U anlam NUL ayrılmış . Bu yüzden readfind ile aynı satır sınırlayıcıyı kullanmak için -d '' Kullanıyoruz. -d $'\0' 'Un da ​​tesadüfen çalıştığını unutmayın, çünkü bash NUL baytlarını desteklemiyorsa boş dizgi olarak kabul edilir.

Doğru olmak için, dosya adlarındaki ters eğik çizgileri özel olarak işlemediğini söyleyen -r Öğesini de ekliyoruz. Örneğin, -r Olmadan \<newline> Kaldırılır ve \n, n biçimine dönüştürülür.

bash veya zsh gerektirmeyen veya null baytlarla ilgili yukarıdaki tüm kuralları hatırlamayan daha taşınabilir bir yöntem (yine Gilles sayesinde):

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' {} ';'

3. Adları * .csv ile biten dizinleri atlama

find . -name "*.csv"

something.csv adlı dizinlerle de eşleşecektir.

Bundan kaçınmak için, find komutuna -type f Ekleyin.

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

glenn jackman belirttiği gibi, bu örneklerin her ikisinde de, her dosya için yürütülecek komutlar bir alt kabukta çalıştırılır, bu nedenle döngü içindeki değişkenleri değiştirirseniz unutulurlar.

Değişkenleri ayarlamanız ve yine de döngünün sonunda ayarlanmasını sağlamanız gerekirse, aşağıdaki gibi işlem ikamesi kullanmak için bunu yeniden yazabilirsiniz:

i=0
while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
    i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"

Bunu komut satırına kopyalayıp yapıştırmayı denerseniz, read line Öğesinin echo "$i files processed" Tüketeceğini unutmayın, böylece komut çalıştırılmaz.

Bundan kaçınmak için read line </dev/tty Dosyasını kaldırabilir ve sonucu less gibi bir çağrı cihazına gönderebilirsiniz.


[~ # ~] notlar [~ # ~]

Döngü içindeki noktalı virgülleri (;) Kaldırdım. İsterseniz onları geri koyabilirsiniz, ancak gerekli değildir.

Bugünlerde $(command), `command` 'Dan daha yaygın. Bunun nedeni, $(command1 $(command2)) yazmanın `command1 \`command2\`` 'Dan daha kolay olmasıdır.

read char Bir karakteri gerçekten okumuyor. Tüm satırı okur, bu yüzden read line Olarak değiştirdim.

218
Mikel

Herhangi bir dosya adı boşluk veya Shell globbing karakterleri \[?* İçeriyorsa bu komut dosyası başarısız olur. find komutu her satıra bir dosya adı verir. Ardından `find …` Komutunun değiştirilmesi Shell tarafından aşağıdaki gibi değerlendirilir:

  1. find komutunu yürütün, çıktısını alın.
  2. find çıktısını ayrı sözcüklere bölün. Herhangi bir boşluk karakteri bir Word ayırıcısıdır.
  3. Her bir Word için, bir globbing deseni ise, eşleştiği dosyalar listesine genişletin.

Örneğin, geçerli dizinde `foo* bar.csv, foo 1.txt Ve foo 2.txt Adlı üç dosya olduğunu varsayalım.

  1. find komutu ./foo* bar.csv Döndürür.
  2. Kabuk bu dizeyi boşlukta böler ve iki kelime üretir: ./foo* Ve bar.csv.
  3. ./foo* Büyük bir metakarakter içerdiğinden, eşleşen dosyalar listesine genişletildi: ./foo 1.txt Ve ./foo 2.txt.
  4. Bu nedenle for döngüsü ./foo 1.txt, ./foo 2.txt Ve bar.csv İle art arda yürütülür.

Bu aşamada, Word bölünmesini kısarak ve globbing'i kapatarak sorunların çoğunu önleyebilirsiniz. Word bölünmesini azaltmak için IFS değişkenini tek bir satırsonu karakterine ayarlayın; bu şekilde find çıktısı yalnızca yeni satırlara bölünecek ve boşluklar kalacaktır. Globbing'i kapatmak için set -f Komutunu çalıştırın. Daha sonra, hiçbir dosya adı yeni satır karakteri içermediği sürece kodun bu kısmı çalışır.

IFS='
'
set -f
for file in $(find . -name "*.csv"); do …

(Bu sorununuzun bir parçası değildir, ancak `…` Üzerinde $(…) kullanmanızı öneririz. Aynı anlama sahiptirler, ancak backquote sürümünde garip alıntı kuralları vardır.)

Aşağıda başka bir sorun daha var: diff $file /some/other/path/$file

diff "$file" "/some/other/path/$file"

Aksi takdirde, $file Değeri kelimelere bölünür ve kelimeler yukarıdaki komut yerine yazılır gibi glob kalıpları olarak ele alınır. Kabuk programlama hakkında bir şey hatırlamanız gerekiyorsa, şunu unutmayın: değişken genişletmeler ($foo) Ve komut ikameleri (her zaman için ($(bar)) = her zaman çift tırnak kullanın bölmek istediğini biliyorum. (Yukarıda, find çıktısını satırlara bölmek istediğimizi biliyorduk.)

find işlevini çağırmanın güvenilir bir yolu, bulduğu her dosya için bir komut çalıştırmasını söylemektir:

find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'

Bu durumda, başka bir yaklaşım, tüm "sıkıcı" dosyaları açıkça hariç tutmanız gerekse de, iki dizini karşılaştırmaktır.

diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path

Belirtilen readarray ifadesini görmemeye şaşırdım. <<< İşleci ile birlikte kullanıldığında bunu çok kolaylaştırır:

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

<<<"$expansion" Yapısını kullanmak, yeni satır içeren değişkenleri dizilere ayırmanıza da izin verir:

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarray yıllardır Bash'ta, bu yüzden bu muhtemelen Bash'de kanonik bir yol olmalı.

6
blujay

tamamen güvenli bulma ile ( herhangi bir özel karakter dahil) dolaşın: (belgeler için bağlantıya bakın):

exec 9< <( find "$absolute_dir_path" -type f -print0 )
while IFS= read -r -d '' -u 9
do
    file_path="$(readlink -fn -- "$REPLY"; echo x)"
    file_path="${file_path%x}"
    echo "START${file_path}END"
done
6
l0b0

Afaik find ihtiyacınız olan her şeye sahiptir.

find . -okdir diff {} /some/other/path/{} ";"

find, programları güvenli bir şekilde çağırmakla ilgilenir. -okdir farktan önce sizden bilgi isteyecektir (evet/hayır olduğundan emin misiniz).

Kabuk yok, globbing, joker, pi, pa, po yok.

Sidenote olarak: find komutunu for/while/do/xargs ile birleştirirseniz, çoğu durumda yanlış yaparsınız. :)

4
user unknown

Kimse henüz burada zsh çözümden bahsetmedi şaşırdım:

for file (**/*.csv(ND.)) {
  do-something-with $file
}

((D) ayrıca gizli dosyaları dahil etmek için, (N) eşleşme yoksa hatayı önlemek için (.) ile sınırlamak için normal dosya.)

bash4.3 ve üzeri şimdi de kısmen destekliyor:

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done
4

İçinde boşluk bulunan dosya adları, alıntılanmamışlarsa komut satırında birden çok ada benzer. Dosyanıza "Hello World.txt" adı verilirse, fark satırı şu şekilde genişler:

diff Hello World.txt /some/other/path/Hello World.txt

dört dosya ismine benziyor. Sadece argümanların içine alıntılar koyun:

diff "$file" "/some/other/path/$file"
2
Ross Smith

Çift tırnak arkadaşın.

diff "$file" "/some/other/path/$file"

Aksi takdirde, değişkenin içeriği Word'e bölünür.

1
geekosaur

Bash4 ile, her satırı içeren bir dizi ayarlamak ve bu dizi üzerinde yineleme yapmak için yerleşik mapfile işlevini de kullanabilirsiniz.

$ tree 
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1
1
kitekat75