Bagaimana membuat bash glob sebagai variabel string?

14

Sistem Informasi

OS: OS X

bash: GNU bash, versi 3.2.57 (1) -release (x86_64-apple-darwin16)

Latar Belakang

Saya ingin mesin waktu untuk mengecualikan satu set direktori dan file dari semua proyek git / nodejs saya. Direktori proyek saya sedang dalam ~/code/private/dan ~/code/public/jadi saya mencoba menggunakan bash looping untuk melakukannya tmutil.

Isu

Versi pendek

Jika saya memiliki variabel string terhitungk , bagaimana cara membuatnya glob atau tepat sebelum for-loop:

i='~/code/public/*'
j='*.launch'
k=$i/$j # $k='~/code/public/*/*.launch'

for i in $k # I need $k to glob here
do
    echo $i
done

Dalam versi panjang di bawah ini, Anda akan melihat k=$i/$j. Jadi saya tidak bisa membuat hardcode string dalam for for.

Versi Panjang

#!/bin/bash
exclude='
*.launch
.classpath
.sass-cache
Thumbs.db
bower_components
build
connect.lock
coverage
dist
e2e/*.js
e2e/*.map
libpeerconnection.log
node_modules
npm-debug.log
testem.log
tmp
typings
'

dirs='
~/code/private/*
~/code/public/*
'

for i in $dirs
do
    for j in $exclude
    do
        k=$i/$j # It is correct up to this line

        for l in $k # I need it glob here
        do
            echo $l
        #   Command I want to execute
        #   tmutil addexclusion $l
        done
    done
done

Keluaran

Mereka tidak menggumpal. Bukan yang saya inginkan.

~/code/private/*/*.launch                                                                                   
~/code/private/*/.DS_Store                                                                                  
~/code/private/*/.classpath                                                                                 
~/code/private/*/.sass-cache                                                                                
~/code/private/*/.settings                                                                                  
~/code/private/*/Thumbs.db                                                                                  
~/code/private/*/bower_components                                                                           
~/code/private/*/build                                                                                      
~/code/private/*/connect.lock                                                                               
~/code/private/*/coverage                                                                                   
~/code/private/*/dist                                                                                       
~/code/private/*/e2e/*.js                                                                                   
~/code/private/*/e2e/*.map                                                                                  
~/code/private/*/libpeerconnection.log                                                                      
~/code/private/*/node_modules                                                                               
~/code/private/*/npm-debug.log                                                                              
~/code/private/*/testem.log                                                                                 
~/code/private/*/tmp                                                                                        
~/code/private/*/typings                                                                                    
~/code/public/*/*.launch                                                                                    
~/code/public/*/.DS_Store                                                                                   
~/code/public/*/.classpath                                                                                  
~/code/public/*/.sass-cache                                                                                 
~/code/public/*/.settings                                                                                   
~/code/public/*/Thumbs.db                                                                                   
~/code/public/*/bower_components                                                                            
~/code/public/*/build                                                                                       
~/code/public/*/connect.lock                                                                                
~/code/public/*/coverage                                                                                    
~/code/public/*/dist                                                                                        
~/code/public/*/e2e/*.js                                                                                    
~/code/public/*/e2e/*.map                                                                                   
~/code/public/*/libpeerconnection.log                                                                       
~/code/public/*/node_modules                                                                                
~/code/public/*/npm-debug.log                                                                               
~/code/public/*/testem.log                                                                                  
~/code/public/*/tmp                                                                                         
~/code/public/*/typings
John Siu
sumber
Kutipan tunggal menghentikan interpolasi shell di Bash, jadi Anda dapat mencoba dua kali mengutip variabel Anda.
Thomas N
@ Thomas tidak, itu tidak berhasil. kadalah string yang dihitung, dan saya membutuhkannya tetap seperti itu sampai loop. Silakan periksa versi panjang saya.
John Siu
@ Thomas Saya memperbarui versi singkat untuk membuatnya lebih jelas.
John Siu

Jawaban:

18

Anda dapat memaksakan putaran evaluasi lain eval, tetapi itu sebenarnya tidak perlu. (Dan evalmulai mengalami masalah serius saat nama file Anda mengandung karakter khusus seperti $.) Masalahnya bukan dengan globbing, tetapi dengan ekspansi tilde.

Globbing terjadi setelah ekspansi variabel, jika variabel tidak dikutip, seperti di sini (*) :

$ x="/tm*" ; echo $x
/tmp

Jadi, dalam nada yang sama, ini mirip dengan apa yang Anda lakukan, dan bekerja:

$ mkdir -p ~/public/foo/ ; touch ~/public/foo/x.launch
$ i="$HOME/public/*"; j="*.launch"; k="$i/$j"
$ echo $k
/home/foo/public/foo/x.launch

Tetapi dengan tilde itu tidak:

$ i="~/public/*"; j="*.launch"; k="$i/$j"
$ echo $k
~/public/*/*.launch

Ini jelas didokumentasikan untuk Bash:

Urutan ekspansi adalah: ekspansi brace; tilde ekspansi, ekspansi parameter dan variabel, ...

Ekspansi Tilde terjadi sebelum ekspansi variabel sehingga tildes di dalam variabel tidak diperluas. Solusi mudahnya adalah menggunakan$HOME atau path lengkap sebagai gantinya.

(* meluaskan gumpalan dari variabel biasanya bukan yang Anda inginkan)


Hal lain:

Ketika Anda mengulangi pola, seperti di sini:

exclude="foo *bar"
for j in $exclude ; do
    ...

perhatikan bahwa seperti $excludedikutip, keduanya terpecah, dan juga menggumpal pada saat ini. Jadi jika direktori saat ini berisi sesuatu yang cocok dengan polanya, itu diperluas menjadi:

$ i="$HOME/public/foo"
$ exclude="*.launch"
$ touch $i/real.launch
$ for j in $exclude ; do           # split and glob, no match
    echo "$i"/$j ; done
/home/foo/public/foo/real.launch

$ touch ./hello.launch
$ for j in $exclude ; do           # split and glob, matches in current dir!
    echo "$i"/$j ; done
/home/foo/public/foo/hello.launch  # not the expected result

Untuk mengatasinya, gunakan variabel array alih-alih string yang dipisah:

$ exclude=("*.launch")
$ exclude+=("something else")
$ for j in "${exclude[@]}" ; do echo "$i"/$j ; done
/home/foo/public/foo/real.launch
/home/foo/public/foo/something else

Sebagai bonus tambahan, entri array juga dapat berisi spasi tanpa masalah dengan pemisahan.


Hal serupa dapat dilakukan dengan find -path, jika Anda tidak keberatan dengan level direktori apa file yang ditargetkan seharusnya. Misalnya untuk menemukan jalur yang diakhiri dengan /e2e/*.js:

$ dirs="$HOME/public $HOME/private"
$ pattern="*/e2e/*.js"
$ find $dirs -path "$pattern"
/home/foo/public/one/two/three/e2e/asdf.js

Kita harus menggunakan $HOMEalih-alih ~karena alasan yang sama seperti sebelumnya, dan $dirsperlu tanda kutip pada findbaris perintah sehingga terpecah, tetapi$pattern dikutip di harus dikutip sehingga tidak secara tidak sengaja diperluas oleh shell.

(Saya pikir Anda bisa bermain dengan -maxdepthdi GNU menemukan untuk membatasi seberapa dalam pencarian berjalan, jika Anda peduli, tapi itu sedikit masalah yang berbeda.)

ilkkachu
sumber
Apakah Anda satu-satunya jawaban find? Saya sebenarnya menjelajahi rute itu juga, karena for-loop semakin rumit. Tetapi saya mengalami kesulitan dengan '-path'.
John Siu
Kredit kepada Anda sebagai info Anda tentang tilde '~' lebih langsung ke masalah utama. Saya akan memposting naskah terakhir dan penjelasan dalam jawaban lain. Namun kredit penuh untuk Anda: D
John Siu
@ JohnSiu, yeah, menggunakan find adalah yang pertama kali terlintas di benak saya. Mungkin bisa digunakan juga, tergantung pada kebutuhan yang tepat. (atau lebih baik juga, untuk beberapa penggunaan.)
ilkkachu
1
@ kevinarpe, saya pikir array pada dasarnya dimaksudkan hanya untuk itu, dan ya, "${array[@]}"(dengan tanda kutip!) didokumentasikan (lihat di sini dan di sini ) untuk memperluas ke elemen sebagai kata-kata yang berbeda tanpa memisahkan mereka lebih jauh.
ilkkachu
1
@sixtyfive, yah, [abc]adalah bagian standar dari pola glob , seperti ?, saya rasa tidak perlu untuk menutupi semuanya di sini.
ilkkachu
4

Anda dapat menyimpannya sebagai array alih-alih string untuk digunakan nanti dalam banyak kasus dan membiarkan globbing terjadi ketika Anda mendefinisikannya. Dalam kasus Anda, misalnya:

k=(~/code/public/*/*.launch)
for i in "${k[@]}"; do

atau dalam contoh nanti, Anda perlu evalbeberapa string

dirs=(~/code/private/* ~/code/public/*)
for i in "${dirs[@]}"; do
    for j in $exclude; do
        eval "for k in $i/$j; do tmutil addexclusion \"\$k\"; done"
    done
done
Eric Renouf
sumber
1
Perhatikan bagaimana $excludemengandung wildcard, Anda harus menonaktifkan globbing sebelum menggunakan operator split + glob di dalamnya dan mengembalikannya untuk $i/$jdan tidak menggunakan evaltetapi menggunakan"$i"/$j
Stéphane Chazelas
Baik Anda dan ilkkachu memberikan jawaban yang baik. Namun jawabannya mengidentifikasi masalah tersebut. Jadi kredit padanya.
John Siu
2

Jawaban @ilkkachu memecahkan masalah globbing utama. Penghargaan penuh padanya.

V1

Namun, karena excludemengandung entri baik dengan dan tanpa wildcard (*), dan juga mungkin tidak ada sama sekali, pemeriksaan tambahan diperlukan setelah globbing dari $i/$j. Saya membagikan temuan saya di sini.

#!/bin/bash
exclude="
*.launch
.DS_Store
.classpath
.sass-cache
.settings
Thumbs.db
bower_components
build
connect.lock
coverage
dist
e2e/*.js
e2e/*.map
libpeerconnection.log
node_modules
npm-debug.log
testem.log
tmp
typings
"

dirs="
$HOME/code/private/*
$HOME/code/public/*
"

# loop $dirs
for i in $dirs; do
    for j in $exclude ; do
        for k in $i/$j; do
            echo -e "$k"
            if [ -f $k ] || [ -d $k ] ; then
                # Only execute command if dir/file exist
                echo -e "\t^^^ Above file/dir exist! ^^^"
            fi
        done
    done
done

Penjelasan Keluaran

Berikut ini adalah output parsial untuk menjelaskan situasi.

/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/a.launch
    ^^^ Above file/dir exist! ^^^
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/b.launch
    ^^^ Above file/dir exist! ^^^
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/.DS_Store
    ^^^ Above file/dir exist! ^^^

Di atas cukup jelas.

/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/.classpath
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/.sass-cache
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/.settings
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/Thumbs.db
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/bower_components
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/build
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/connect.lock
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/coverage
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/dist

Di atas muncul karena entri mengecualikan ( $j) tidak memiliki wildcard, $i/$jmenjadi penggabungan string sederhana. Namun file / dir tidak ada.

/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/e2e/*.js
/Volumes/HD2/JS/code/public/simple-api-example-ng2-express/e2e/*.map

Di atas muncul sebagai mengecualikan entri ( $j) berisi wildcard tetapi tidak memiliki file / direktori yang cocok, globbing dari $i/$jhanya mengembalikan string asli.

V2

V2 menggunakan kutipan tunggal, evaldan shopt -s nullglobuntuk mendapatkan hasil yang bersih. Tidak diperlukan pemeriksaan akhir file / dir.

#!/bin/bash
exclude='
*.launch
.sass-cache
Thumbs.db
bower_components
build
connect.lock
coverage
dist
e2e/*.js
e2e/*.map
libpeerconnection.log
node_modules
npm-debug.log
testem.log
tmp
typings
'

dirs='
$HOME/code/private/*
$HOME/code/public/*
'

for i in $dirs; do
    for j in $exclude ; do
        shopt -s nullglob
        eval "k=$i/$j"
        for l in $k; do
            echo $l
        done
        shopt -u nullglob
    done
done
John Siu
sumber
Satu masalah adalah bahwa di for j in $exclude, gumpalan di dalam $excludedapat diperluas pada saat $excludeekspansi itu (dan memanggil evalyang meminta masalah). Anda ingin globbing diaktifkan untuk for i in $dir, dan for l in $k, tetapi tidak untuk for j in $exclude. Anda ingin yang pertama set -fdan set +fyang lain. Secara umum, Anda ingin menyetel operator glob + split Anda sebelum menggunakannya. Bagaimanapun, Anda tidak ingin split + glob untuk echo $l, jadi $lharus dikutip di sana.
Stéphane Chazelas
@ StéphaneChazelas yang Anda maksud dengan v1 atau v2? Untuk v2, keduanya excludedan dirsberada dalam kutipan tunggal ( ), so no globbing till eval`.
John Siu
Globbing terjadi pada ekspansi variabel yang tidak dikutip dalam konteks daftar , yang (meninggalkan variabel tidak dikutip) kadang-kadang kita sebut operator split + glob . Tidak ada globbing dalam tugas untuk variabel skalar. foo=*dan foo='*'sama. Tapi echo $foodan echo "$foo"tidak (dalam cangkang seperti bash, sudah diperbaiki dalam cangkang seperti zsh, ikan atau rc, lihat juga tautan di atas). Di sini Anda tidak ingin menggunakan operator yang, tetapi di beberapa tempat hanya bagian split, dan lain-lain hanya bagian gumpal.
Stéphane Chazelas
@ StéphaneChazelas Terima kasih atas informasinya !!! Membawa saya kadang-kadang tetapi saya mengerti keprihatinan sekarang. Ini sangat berharga !! Terima kasih!!!
John Siu
1

Dengan zsh:

exclude='
*.launch
.classpath
.sass-cache
Thumbs.db
...
'

dirs=(
~/code/private/*
~/code/public/*
)

for f ($^dirs/${^${=~exclude}}(N)) {
  echo $f
}

${^array}stringadalah untuk memperluas sebagai $array[1]string $array[2]string.... $=varadalah untuk melakukan pemisahan kata pada variabel (sesuatu yang dilakukan oleh shell lain secara default!), $~varmelakukan globbing pada variabel (sesuatu yang shell lain juga secara default (ketika Anda umumnya tidak menginginkannya, Anda harus mengutip $fdi atas dalam kerang lainnya)).

(N)adalah kualifikasi global yang mengaktifkan nullglob untuk masing-masing gumpalan yang dihasilkan dari $^array1/$^array2ekspansi itu. Itu membuat gumpalan melebar ke nol ketika mereka tidak cocok. Itu juga terjadi untuk mengubah non-gumpal suka ~/code/private/foo/Thumbs.dbmenjadi satu, yang berarti bahwa jika itu tidak ada, itu tidak termasuk.

Stéphane Chazelas
sumber
Ini sangat bagus. Saya diuji dan bekerja. Namun, tampaknya zsh lebih sensitif terhadap baris baru saat menggunakan kutipan tunggal. Cara excludetertutup mempengaruhi output.
John Siu
@ JohnSiu, oh ya, kamu benar. Tampaknya split + glob dan $^arrayharus dilakukan dalam dua langkah terpisah untuk memastikan elemen kosong dibuang (lihat edit). Itu terlihat seperti bug zsh, saya akan mengangkat masalah ini di milis mereka.
Stéphane Chazelas
Saya datang dengan v2 untuk bash, yang lebih bersih, tapi masih tidak sekompak skrip zsh Anda, lol
John Siu