Cara yang sesuai dengan POSIX untuk bekerja dengan daftar nama file yang mungkin dengan spasi putih

14

Saya telah melihat panduan skrip Bash menyarankan penggunaan array untuk bekerja dengan nama file yang mengandung spasi. Namun DashAsBinSh menyarankan bahwa array tidak portabel sehingga saya mencari cara yang sesuai dengan POSIX untuk bekerja dengan daftar nama file yang mungkin mengandung spasi.

Saya ingin memodifikasi skrip contoh di bawah ini agar dapat echo

foo/target/a.jar
foo/target/b.jar
bar/target/lol whitespace.jar

Ini skripnya

#!/usr/bin/env sh

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"
# this would be produced by a 'ls' command
# We can execute the ls within the script, if it helps

dostuffwith() { echo $1; };

F_LOCATIONS=$INPUT
ALL_FILES=$(for f in $F_LOCATIONS; do echo `basename $f`; done)
ALL_FILES=$(echo "$ALL_FILES" | sort | uniq)

for f in $ALL_FILES
do
    fpath=$(echo "$F_LOCATIONS" | grep -m1 $f)
    dostuffwith $fpath
done
Eero Aaltonen
sumber
Kemungkinan sama pada SO: stackoverflow.com/questions/6499486/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Jawaban:

8

POSIX kerang memiliki satu array: parameter posisi ( $1, $2, dll, secara kolektif disebut sebagai "$@").

set -- 'foo/target/a.jar' 'foo/target/b.jar' 'bar/target/b.jar' 'bar/target/lol whitespace.jar'
set -- "$@" '/another/one at the end.jar'

for jar do
  dostuffwith "$jar"
done

Ini tidak nyaman karena hanya ada satu, dan itu merusak penggunaan parameter posisi lainnya. Parameter posisi bersifat lokal untuk suatu fungsi, yang terkadang merupakan berkah dan terkadang kutukan.

Jika nama file Anda dijamin tidak mengandung baris baru, Anda dapat menggunakan baris baru sebagai pemisah. Saat Anda memperluas variabel, pertama-tama matikan globbing dengan set -fdan atur daftar karakter pemisah bidang yang IFShanya berisi baris baru.

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"

set -f; IFS='
'                           # turn off variable value expansion except for splitting at newlines
for jar in $INPUT; do
  set +f; unset IFS
  dostuffwith "$jar"        # restore globbing and field splitting at all whitespace
done
set +f; unset IFS           # do it again in case $INPUT was empty

Dengan item dalam daftar Anda dipisahkan oleh baris baru, Anda dapat menggunakan banyak perintah pemrosesan teks yang bermanfaat, khususnya sort.

Ingatlah untuk selalu menempatkan tanda kutip ganda di sekitar substitusi variabel, kecuali ketika Anda secara eksplisit ingin pemisahan bidang terjadi (juga globbing, kecuali Anda mematikannya).

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Jawaban dan penjelasan yang bagus. Saya akan menandai ini sebagai diterima karena ini membuat sort | uniqlangkah awal berfungsi sebagaimana dimaksud.
Eero Aaltonen
5

Karena $INPUTvariabel Anda menggunakan baris baru sebagai pemisah, saya akan menganggap bahwa file Anda tidak akan memiliki baris baru dalam namanya. Karena itu, ya, ada cara sederhana untuk mengulangi file dan menjaga spasi.

Idenya adalah menggunakan readshell builtin. Biasanya readakan terpecah pada spasi putih mana saja, sehingga ruang akan memecahnya. Tapi Anda bisa mengatur IFS=$'\n'dan itu akan terpecah hanya pada baris baru. Jadi Anda dapat mengulangi setiap baris dalam daftar Anda.

Inilah solusi terkecil yang bisa saya temukan:

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"

dostuffwith() {
    echo "$1"
}

echo "$INPUT" | awk -F/ '{if (!seen[$NF]++) print }' | \
while IFS=$'\n' read file; do
  dostuffwith "$file"
done

Pada dasarnya ia mengirimkan "$ INPUT" ke awkmana deduplicate berdasarkan nama file (itu terpecah /dan kemudian mencetak baris jika item terakhir belum terlihat sebelumnya). Kemudian setelah awk membuat daftar path file, kita gunakan while readuntuk mengulangi daftar.

Patrick
sumber
$ checkbashisms bar.sh kemungkinan bashism di bar.sh baris 14 (<<< di sini string)
Eero Aaltonen
1
@EeroAaltonen Mengubahnya agar tidak menggunakan herestring. Perhatikan bahwa dengan perubahan ini, whileloop, dan dengan demikian dostuffwithdijalankan dalam subkulit. Jadi setiap variabel atau perubahan yang dilakukan pada shell yang berjalan akan hilang ketika loop selesai. Satu-satunya alternatif adalah menggunakan heredoc penuh, yang bukan tidak menyenangkan, tetapi saya pikir ini akan lebih baik.
Patrick
Saya memberikan poin berdasarkan lebih banyak pada keterbacaan daripada kecil. Ini pasti berfungsi dan sudah memberi +1 untuk itu.
Eero Aaltonen
IFS="\n"membagi karakter backslash dan n. Namun read file, tidak ada pemisahan. IFS="\n"masih berguna karena menghapus karakter kosong dari $ IFS yang jika tidak akan dilucuti di awal dan akhir input. Untuk membaca garis, sintaks kanonik adalah IFS= read -r line, meskipun IFS=anything read -r line(disediakan apapun tidak mengandung kosong) akan bekerja dengan baik.
Stéphane Chazelas
oops. Tidak yakin bagaimana saya mengaturnya. Tetap.
Patrick