it-swarm-id.com

Looping melalui file dengan spasi di namanya?

Saya menulis skrip berikut untuk membedakan keluaran dari dua sutradara dengan semua file yang sama di dalamnya:

#!/bin/bash

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

Saya tahu ada cara lain untuk mencapai ini. Anehnya, skrip ini gagal ketika file memiliki spasi di dalamnya. Bagaimana saya bisa mengatasi ini?

Contoh output dari find:

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

Jawaban singkat (paling dekat dengan jawaban Anda, tetapi menangani spasi)

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"

Jawaban yang lebih baik (juga menangani wildcard dan baris baru dalam nama file)

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

Jawaban terbaik (berdasarkan jawaban Gilles )

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

Atau bahkan lebih baik, untuk menghindari menjalankan satu sh per file:

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

Jawaban panjang

Anda memiliki tiga masalah:

  1. Secara default, Shell membagi output dari perintah pada spasi, tab, dan baris baru
  2. Nama file dapat berisi karakter wildcard yang akan diperluas
  3. Bagaimana jika ada direktori yang namanya diakhiri dengan *.csv?

1. Membagi hanya pada baris baru

Untuk mengetahui apa yang harus ditetapkan file, Shell harus mengambil output find dan menafsirkannya entah bagaimana, jika file hanya akan menjadi seluruh output dari find.

Shell membaca variabel IFS, yang ditetapkan ke <space><tab><newline> Secara default.

Kemudian terlihat pada setiap karakter dalam output find. Segera setelah ia melihat karakter apa pun yang ada di IFS, ia berpikir yang menandai akhir nama file, sehingga ia menetapkan file untuk karakter apa pun yang dilihatnya sampai sekarang dan menjalankan loop. Kemudian mulai di mana ia tinggalkan untuk mendapatkan nama file berikutnya, dan menjalankan loop berikutnya, dll, hingga mencapai akhir output.

Jadi secara efektif melakukan ini:

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

Untuk hanya membagi input pada baris baru, Anda harus melakukannya

IFS=$'\n'

sebelum perintah for ... find Anda.

Itu menetapkan IFS ke satu baris baru, sehingga hanya terpecah pada baris baru, dan bukan spasi dan tab juga.

Jika Anda menggunakan sh atau dash alih-alih ksh93, bash atau zsh, Anda perlu menulis IFS=$'\n' suka ini sebagai gantinya:

IFS='
'

Itu mungkin cukup untuk membuat skrip Anda berfungsi, tetapi jika Anda tertarik untuk menangani beberapa kasus sudut lainnya dengan benar, baca terus ...

2. Memperluas $file Tanpa wildcard

Di dalam lingkaran tempat Anda melakukannya

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

shell mencoba memperluas $file (lagi!).

Itu bisa berisi spasi, tetapi karena kita sudah menetapkan IFS di atas, itu tidak akan menjadi masalah di sini.

Tapi itu juga bisa berisi karakter wildcard seperti * Atau ?, Yang akan mengarah pada perilaku yang tidak terduga. (Terima kasih kepada Gilles untuk menunjukkan ini.)

Untuk memberi tahu Shell agar tidak memperluas karakter wildcard, masukkan variabel di dalam tanda kutip ganda, mis.

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

Masalah yang sama juga bisa menggigit kita

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

Misalnya, jika Anda memiliki tiga file ini

file1.csv
file2.csv
*.csv

(sangat tidak mungkin, tetapi masih memungkinkan)

Seolah-olah Anda telah lari

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

yang akan diperluas ke

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

menyebabkan file1.csv dan file2.csv akan diproses dua kali.

Justru yang harus kita lakukan

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 membaca baris dari input standar, membagi baris menjadi kata-kata sesuai IFS dan menyimpannya dalam nama variabel yang Anda tentukan.

Di sini, kami memberi tahu untuk tidak membagi baris menjadi kata-kata, dan untuk menyimpan garis dalam $file.

Perhatikan juga bahwa read line Telah berubah menjadi read line </dev/tty.

Ini karena di dalam loop, input standar berasal dari find melalui pipa.

Jika kita baru saja read, itu akan memakan sebagian atau seluruh nama file, dan beberapa file akan dilewati.

/dev/tty Adalah terminal tempat pengguna menjalankan skrip. Perhatikan bahwa ini akan menyebabkan kesalahan jika skrip dijalankan melalui cron, tetapi saya menganggap ini tidak penting dalam kasus ini.

Lalu, bagaimana jika nama file berisi baris baru?

Kita dapat mengatasinya dengan mengubah -print Menjadi -print0 Dan menggunakan read -d '' Di akhir pipa:

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

Ini membuat find meletakkan byte nol di akhir setiap nama file. Null byte adalah satu-satunya karakter yang tidak diizinkan dalam nama file, jadi ini harus menangani semua nama file yang mungkin, tidak peduli betapa anehnya.

Untuk mendapatkan nama file di sisi lain, kami menggunakan IFS= read -r -d ''.

Di mana kami menggunakan read di atas, kami menggunakan pembatas baris default baris baru, tetapi sekarang, find menggunakan null sebagai pembatas baris. Dalam bash, Anda tidak dapat meneruskan karakter NUL dalam argumen ke perintah (bahkan yang builtin), tetapi bash memahami -d '' Sebagai makna NUL dibatasi . Jadi kami menggunakan -d '' Untuk membuat read menggunakan pembatas baris yang sama dengan find. Perhatikan bahwa -d $'\0', Secara kebetulan, juga berfungsi, karena bash yang tidak mendukung byte NUL memperlakukannya sebagai string kosong.

Agar benar, kami juga menambahkan -r, Yang mengatakan tidak menangani garis miring terbalik dalam nama file khusus. Misalnya, tanpa -r, \<newline> Dihapus, dan \n Dikonversi menjadi n.

Cara yang lebih portabel untuk menulis ini yang tidak memerlukan bash atau zsh atau mengingat semua aturan di atas tentang null byte (sekali lagi, terima kasih kepada Gilles):

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

3. Melompati direktori yang namanya diakhiri dengan * .csv

find . -name "*.csv"

juga akan cocok dengan direktori yang disebut something.csv.

Untuk menghindari ini, tambahkan -type f Ke perintah find.

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

Seperti glenn jackman menunjukkan, dalam kedua contoh ini, perintah untuk mengeksekusi untuk setiap file dijalankan dalam subkulit, jadi jika Anda mengubah variabel apa pun di dalam loop, mereka akan dilupakan.

Jika Anda perlu mengatur variabel dan tetap mengaturnya di akhir loop, Anda bisa menulis ulang untuk menggunakan subtitusi proses seperti ini:

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"

Perhatikan bahwa jika Anda mencoba menyalin dan menempelkan ini pada baris perintah, read line Akan mengkonsumsi echo "$i files processed", Sehingga perintah itu tidak akan dijalankan.

Untuk menghindari ini, Anda dapat menghapus read line </dev/tty Dan mengirim hasilnya ke pager seperti less.


[~ # ~] catatan [~ # ~]

Saya menghapus titik koma (;) Di dalam loop. Anda dapat mengembalikannya jika diinginkan, tetapi tidak diperlukan.

Saat ini, $(command) lebih umum daripada `command`. Ini terutama karena lebih mudah untuk menulis $(command1 $(command2)) daripada `command1 \`command2\``.

read char Tidak benar-benar membaca karakter. Bunyinya seluruh baris jadi saya mengubahnya menjadi read line.

218
Mikel

Skrip ini gagal jika ada nama file yang mengandung spasi atau karakter-karakter Shell globbing \[?*. Perintah find menampilkan satu nama file per baris. Kemudian substitusi perintah `find …` Dievaluasi oleh Shell sebagai berikut:

  1. Jalankan perintah find, ambil outputnya.
  2. Pisahkan output find menjadi kata-kata yang terpisah. Setiap karakter spasi putih adalah pemisah kata.
  3. Untuk setiap Word, jika itu adalah pola globbing, perluas itu ke daftar file yang cocok.

Misalnya, anggap ada tiga file dalam direktori saat ini, yang disebut `foo* bar.csv, foo 1.txt Dan foo 2.txt.

  1. Perintah find mengembalikan ./foo* bar.csv.
  2. Shell membagi string ini di angkasa, menghasilkan dua kata: ./foo* Dan bar.csv.
  3. Karena ./foo* Berisi metacharacter globbing, ia diperluas ke daftar file yang cocok: ./foo 1.txt Dan ./foo 2.txt.
  4. Karenanya loop for dieksekusi berturut-turut dengan ./foo 1.txt, ./foo 2.txt Dan bar.csv.

Anda dapat menghindari sebagian besar masalah pada tahap ini dengan mengurangi pemisahan kata dan mematikan penggumpalan. Untuk mengurangi pemisahan kata, atur variabel IFS ke karakter baris baru; dengan cara ini output find hanya akan dibagi pada baris baru dan spasi akan tetap ada. Untuk mematikan globbing, jalankan set -f. Maka bagian kode ini akan berfungsi selama tidak ada nama file yang mengandung karakter baris baru.

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

(Ini bukan bagian dari masalah Anda, tetapi saya sarankan menggunakan $(…) lebih dari `…`. Mereka memiliki arti yang sama, tetapi versi backquote memiliki aturan mengutip yang aneh.)

Ada masalah lain di bawah ini: diff $file /some/other/path/$file Seharusnya

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

Jika tidak, nilai $file Dibagi menjadi kata-kata dan kata-kata tersebut diperlakukan sebagai pola gumpalan, seperti dengan perintah substitusi di atas. Jika Anda harus mengingat satu hal tentang pemrograman Shell, ingat ini: selalu gunakan tanda kutip ganda di sekitar ekspansi variabel ($foo) Dan pergantian perintah ($(bar)), kecuali Anda tahu Anda ingin membagi. (Di atas, kami tahu kami ingin membagi output find menjadi beberapa baris.)

Cara yang dapat diandalkan untuk menelepon find mengatakannya untuk menjalankan perintah untuk setiap file yang ditemukannya:

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

Dalam hal ini, pendekatan lain adalah membandingkan dua direktori, meskipun Anda harus secara eksplisit mengecualikan semua file "membosankan".

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

Saya terkejut tidak melihat readarray disebutkan. Itu membuat ini sangat mudah bila digunakan bersama dengan <<< operator:

$ touch oneword "two words"

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

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

Menggunakan <<<"$expansion" construct juga memungkinkan Anda untuk memecah variabel yang berisi baris baru ke dalam array, seperti:

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

readarray telah ada di Bash selama bertahun-tahun sekarang, jadi ini mungkin merupakan cara kanonik untuk melakukan ini di Bash.

6
blujay

Ulangi semua file ( semua karakter khusus) dengan temukan sepenuhnya aman (lihat tautan untuk dokumentasi):

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 menemukan memiliki semua yang Anda butuhkan.

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

find dengan sendirinya memperhatikan pemanggilan program secara hemat. -okdir akan meminta Anda sebelum diff (apakah Anda yakin ya/tidak).

Tidak ada Shell yang terlibat, tidak ada globbing, pelawak, pi, pa, po.

Sebagai sidenote: Jika Anda menggabungkan find dengan/while/do/xargs, dalam kebanyakan kasus, Anda salah melakukannya. :)

4
user unknown

Saya terkejut tidak ada yang menyebutkan solusi zsh yang jelas di sini:

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

((D) untuk juga menyertakan file tersembunyi, (N) untuk menghindari kesalahan jika tidak ada kecocokan, (.) untuk membatasi --- reguler file.)

bash4.3 dan di atas sekarang mendukungnya juga sebagian:

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

Nama file dengan spasi di dalamnya terlihat seperti beberapa nama pada baris perintah jika tidak dikutip. Jika file Anda bernama "Hello World.txt", baris diff diperluas ke:

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

yang terlihat seperti empat nama file. Cukup beri tanda kutip di sekitar argumen:

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

Kutipan ganda adalah teman Anda.

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

Kalau tidak, konten variabel mendapatkan Word-split.

1
geekosaur

Dengan bash4, Anda juga dapat menggunakan fungsi mapfile bawaan untuk mengatur larik yang berisi setiap baris dan beralih ke larik ini.

$ 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