Wie mache ich Bash Glob eine String-Variable?

14

Systeminformationen

OS: OS X

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

Hintergrund

Ich möchte, dass Time Machine eine Reihe von Verzeichnissen und Dateien aus meinem git / nodejs-Projekt ausschließt. Meine Projektverzeichnisse befinden sich in ~/code/private/und ~/code/public/deshalb versuche ich, die Bash-Schleife zu verwenden tmutil.

Problem

Kurze Version

Wenn ich eine berechnete Zeichenfolgenvariable habe k, wie mache ich sie in oder direkt vor einer for-Schleife:

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

In der langen Version unten sehen Sie k=$i/$j. Daher kann ich die Zeichenfolge in der for-Schleife nicht fest codieren.

Lange Version

#!/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

Ausgabe

Sie sind nicht kugelförmig. Nicht was ich will.

~/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
quelle
Einfache Anführungszeichen stoppen die Shell-Interpolation in Bash, daher können Sie versuchen, Ihre Variable in doppelte Anführungszeichen zu setzen.
Thomas N
@ThomasN nein, das geht nicht. kist eine berechnete Zeichenfolge, und ich muss es so bleiben, bis die Schleife. Bitte überprüfen Sie meine lange Version.
John Siu
@ThomasN Ich habe die Kurzversion aktualisiert, um sie klarer zu machen.
John Siu

Antworten:

18

Sie können eine weitere Evaluierungsrunde mit erzwingen eval, dies ist jedoch nicht unbedingt erforderlich. (Und es evaltreten ernsthafte Probleme auf, sobald Ihre Dateinamen Sonderzeichen wie enthalten $.) Das Problem liegt nicht beim Globbing, sondern bei der Tilde-Erweiterung.

Globbing erfolgt nach Variablenerweiterung, wenn die Variable nicht in Anführungszeichen gesetzt ist, wie hier (*) :

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

Auf die gleiche Weise ähnelt dies dem, was Sie getan haben und funktioniert:

$ 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

Aber mit der Tilde tut es nicht:

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

Dies ist für Bash eindeutig dokumentiert :

Die Reihenfolge der Erweiterungen ist: Klammererweiterung; Tildeerweiterung, Parameter- und Variablenerweiterung, ...

Die Tildeerweiterung erfolgt vor der Variablenerweiterung, sodass Tilde in Variablen nicht erweitert werden. Die einfache Problemumgehung besteht darin, $HOMEstattdessen den vollständigen Pfad zu verwenden.

(* Das Erweitern von Globs aus Variablen ist normalerweise nicht das, was Sie wollen.)


Etwas anderes:

Wenn Sie die Muster durchlaufen, wie hier:

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

Beachten Sie, dass es, wie $excludenicht zitiert, sowohl aufgeteilt als auch globalisiert ist. Wenn das aktuelle Verzeichnis also etwas enthält, das mit dem Muster übereinstimmt, wird es folgendermaßen erweitert:

$ 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

Verwenden Sie eine Array-Variable anstelle einer geteilten Zeichenfolge, um dies zu umgehen:

$ 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

Als zusätzlichen Bonus können Array-Einträge auch Leerzeichen ohne Probleme mit der Aufteilung enthalten.


Ähnliches könnte getan werden find -path, wenn es Ihnen egal ist, auf welcher Verzeichnisebene sich die Zieldateien befinden sollten. Zum Beispiel, um einen Pfad zu finden, der endet in /e2e/*.js:

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

Wir müssen $HOMEstatt aus ~dem gleichen Grund wie zuvor verwenden und $dirsmüssen in der findBefehlszeile $patternnicht in Anführungszeichen gesetzt werden, damit es aufgeteilt wird, sollten aber in Anführungszeichen gesetzt werden, damit es nicht versehentlich durch die Shell erweitert wird.

(Ich denke, Sie könnten mit -maxdepthGNU find spielen, um zu begrenzen, wie tief die Suche geht, wenn es Sie interessiert, aber das ist ein etwas anderes Problem.)

ilkkachu
quelle
Bist du die einzige Antwort mit find? Eigentlich erkunde ich auch diese Route, da die for-Schleife immer komplizierter wird. Aber ich habe Schwierigkeiten mit dem '-Pfad'.
John Siu
Gutschrift für Sie, da Ihre Informationen zu Tilde '~' direkter zum Hauptproblem führen. Ich werde das endgültige Drehbuch und die Erklärung in einer anderen Antwort veröffentlichen. Aber volle Ehre: D
John Siu
@JohnSiu, yeah, als erstes kam mir die Verwendung von find in den Sinn. Je nach Bedarf kann es auch verwendet werden. (oder besser auch für einige
Zwecke
1
@kevinarpe, ich denke, Arrays sind im Grunde nur dafür gedacht, und ja, "${array[@]}"(mit den Anführungszeichen!) ist dokumentiert (siehe hier und hier ), um die Elemente als eindeutige Wörter zu erweitern, ohne sie weiter aufzuteilen.
Ilkkachu
1
@sixtyfive, na ja, [abc]ist ein Standardteil von Glob-Mustern . ?Ich glaube nicht, dass es notwendig ist, alle hier zu behandeln.
Ilkkachu
4

Sie können es als Array anstelle einer Zeichenfolge speichern, um es später in vielen Fällen zu verwenden, und das Globbing beim Definieren zulassen. In Ihrem Fall zum Beispiel:

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

oder im späteren Beispiel werden Sie evaleinige der Zeichenfolgen benötigen

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
quelle
1
Beachten Sie, wie der $excludePlatzhalter enthält, den Sie deaktivieren Globbing brauchen würde , bevor die Verwendung von Split + glob Operator auf sie und es wieder für die $i/$jund nicht verwenden , evalaber die Verwendung"$i"/$j
Stéphane Chazelas
Sie und ilkkachu geben eine gute Antwort. Seine Antwort identifizierte jedoch das Problem. Also, danke an ihn.
John Siu
2

@ilkkachu Antwort löste das Hauptproblem Globbing. Volle Ehre für ihn.

V1

Da excludeEinträge mit und ohne Platzhalter (*) enthalten sind und möglicherweise auch nicht in allen vorhanden sind, ist nach dem Verschieben von eine zusätzliche Überprüfung erforderlich $i/$j. Ich teile meine Erkenntnisse hier.

#!/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

Output Erklärung

Das Folgende ist die Teilausgabe, um die Situation zu erklären.

/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! ^^^

Die obigen Angaben sind selbsterklärend.

/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

Das obige wird angezeigt, da der Exclude-Eintrag ( $j) kein Platzhalterzeichen enthält und $i/$jeine einfache Zeichenfolgenverkettung ist. Die Datei / dir existiert jedoch nicht.

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

Das oben $jgezeigte Ergebnis enthält als Eintrag ausschließen ( ) einen Platzhalter, hat aber keine Datei- / Verzeichnisübereinstimmung. Beim Verschieben von wird $i/$jnur die ursprüngliche Zeichenfolge zurückgegeben.

V2

V2 verwenden Sie einfache Anführungszeichen evalund shopt -s nullgloberhalten Sie ein sauberes Ergebnis. Keine Datei / Verzeichnis-Endkontrolle erforderlich.

#!/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
quelle
Ein Problem besteht darin, dass for j in $excludedie Globs in $excludezum Zeitpunkt dieser $excludeErweiterung erweitert werden könnten (und das Anrufen evalist problematisch ). Sie möchten, dass das Globbing für for i in $dirund for l in $k, aber nicht für aktiviert wird for j in $exclude. Sie möchten ein set -fvor dem letzteren und ein set +ffür den anderen. Im Allgemeinen möchten Sie Ihren split + glob-Operator optimieren, bevor Sie ihn verwenden. In jedem Fall möchten Sie nicht split + glob für echo $l, also $lsollten Sie dort zitieren.
Stéphane Chazelas
@ StéphaneChazelas beziehen Sie sich auf v1 oder v2? Für v2 sind beide excludeund dirsin einfachen Anführungszeichen ( ), so no globbing till eval`.
John Siu
Globbing erfolgt auf nicht notierte variable Erweiterung in der Liste Zusammenhängen , dass (eine Variable unquoted verlassen) nennen wir manchmal die Spaltung + glob - Operator. Zuweisungen zu skalaren Variablen weisen keine Globen auf. foo=*und foo='*'ist das gleiche. Aber echo $foound echo "$foo"sind es nicht (in Shells wie bash, es wurde in Shells wie zsh, fish oder rc behoben, siehe auch den Link oben). Hier möchten Sie diesen Operator verwenden, an einigen Stellen jedoch nur den aufgeteilten Teil und an anderen nur den globalen Teil.
Stéphane Chazelas
@ StéphaneChazelas Danke für die Info !!! Hat mich irgendwann gekostet, aber jetzt verstehe ich die Besorgnis. Das ist sehr wertvoll !! Vielen Dank!!!
John Siu
1

Mit zsh:

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

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

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

${^array}stringist zu erweitern als $array[1]string $array[2]string.... $=varist das Aufteilen von Wörtern auf die Variable (etwas, was andere Shells standardmäßig tun!), $~vardas Globbing auf die Variable (etwas, was andere Shells ebenfalls standardmäßig tun) (wenn Sie dies normalerweise nicht möchten, hätten Sie $foben in Anführungszeichen setzen müssen andere Muscheln)).

(N)ist ein Glob-Qualifizierer, der nullglob für jeden dieser aus dieser $^array1/$^array2Erweiterung resultierenden Globs aktiviert . Dadurch werden die Globs zu nichts, wenn sie nicht übereinstimmen. Das passiert auch, dass ein Nicht-Glob wie ~/code/private/foo/Thumbs.dbeiner wird, was bedeutet, dass, wenn dieser bestimmte nicht existiert, er nicht enthalten ist.

Stéphane Chazelas
quelle
Das ist wirklich nett. Ich habe getestet und funktioniert. Es scheint jedoch, dass zsh bei der Verwendung von einfachen Anführungszeichen empfindlicher auf Zeilenumbrüche reagiert. Der excludebeiliegende Weg wirkt sich auf den Ausgang aus.
John Siu
@ JohnSiu, oh ja, du hast recht. Es $^arraysieht so aus, als ob der Split + Glob in zwei Schritten ausgeführt werden muss, um sicherzustellen, dass die leeren Elemente verworfen werden (siehe Bearbeiten). Das sieht ein bisschen nach einem Bug aus zsh, ich werde das Problem auf ihrer Mailingliste ansprechen.
Stéphane Chazelas
Ich komme mit einem v2 für bash up, die sauberer ist, aber immer noch nicht so kompakt wie Ihr zsh Skript, lol
John Siu