Comment faire de bash glob une variable chaîne?

14

Information système

OS: OS X

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

Contexte

Je veux que Time Machine exclue un ensemble de répertoires et de fichiers de tout mon projet git / nodejs. Mes répertoires de projet sont dedans ~/code/private/et ~/code/public/j'essaye donc d'utiliser la boucle bash pour faire le tmutil.

Problème

Version courte

Si j'ai une variable de chaîne calculéek , comment puis-je la faire glob dans ou juste avant une boucle for:

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

Dans la version longue ci-dessous, vous verrez k=$i/$j. Je ne peux donc pas coder en dur la chaîne dans la boucle for.

Version longue

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

Production

Ils ne sont pas globulés. Pas ce que je veux.

~/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
la source
Les guillemets simples arrêtent l'interpolation du shell dans Bash, vous pouvez donc essayer de citer deux fois votre variable.
Thomas N
@ThomasN non, cela ne fonctionne pas. kest une chaîne calculée, et j'en ai besoin de rester ainsi jusqu'à la boucle. Veuillez vérifier ma version longue.
John Siu
@ThomasN J'ai mis à jour la version courte pour la rendre plus claire.
John Siu du

Réponses:

18

Vous pouvez forcer un autre cycle d'évaluation avec eval, mais ce n'est pas réellement nécessaire. (Et evalcommence à avoir de sérieux problèmes au moment où vos noms de fichiers contiennent des caractères spéciaux comme $.) Le problème n'est pas avec la globalisation, mais avec l'extension tilde.

Le globalisation se produit après l' expansion de la variable, si la variable n'est pas citée, comme ici (*) :

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

Donc, dans la même veine, cela ressemble à ce que vous avez fait et fonctionne:

$ 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

Mais avec le tilde, ce n'est pas le cas:

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

Ceci est clairement documenté pour Bash:

L'ordre des expansions est le suivant: expansion des orthèses; expansion tilde, expansion paramètres et variables, ...

L'expansion du tilde se produit avant l'expansion des variables, donc les tildes à l'intérieur des variables ne sont pas développés. La solution de contournement simple consiste à utiliser $HOMEou le chemin complet à la place.

(* l'expansion des globes à partir des variables n'est généralement pas ce que vous voulez)


Autre chose:

Lorsque vous parcourez les motifs, comme ici:

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

notez que, comme il $excluden'est pas cité, il est à la fois divisé et également globulé à ce stade. Donc, si le répertoire actuel contient quelque chose qui correspond au modèle, il est étendu à cela:

$ 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

Pour contourner ce problème, utilisez une variable de tableau au lieu d'une chaîne fractionnée:

$ 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

En prime, les entrées de tableau peuvent également contenir des espaces sans problème de division.


Quelque chose de similaire pourrait être fait avec find -path, si cela ne vous dérange pas quel niveau de répertoire les fichiers ciblés devraient être. Par exemple, pour trouver un chemin se terminant par /e2e/*.js:

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

Nous devons utiliser $HOMEau lieu de ~pour la même raison que précédemment, et $dirsdoit être non cité sur la findligne de commande pour qu'il soit divisé, mais $patterndoit être cité afin qu'il ne soit pas accidentellement développé par le shell.

(Je pense que vous pouvez jouer avec -maxdepthGNU find pour limiter la profondeur de la recherche, si cela vous intéresse, mais c'est un peu un problème différent.)

ilkkachu
la source
Êtes-vous la seule réponse find? En fait, j'explore également cette route, car la boucle for se complique. Mais j'ai du mal avec le '-path'.
John Siu
Nous vous remercions car vos informations sur le tilde '~' concernent plus directement le problème principal. Je posterai le script final et l'explication dans une autre réponse. Mais merci à vous: D
John Siu
@JohnSiu, oui, c'est d'abord ce qui m'est venu à l'esprit. Il pourrait également être utilisable, selon le besoin exact. (ou mieux aussi, pour certaines utilisations.)
ilkkachu
1
@kevinarpe, je pense que les tableaux sont essentiellement destinés à cela, et oui, "${array[@]}"(avec les guillemets!) est documenté (voir ici et ici ) pour s'étendre aux éléments en tant que mots distincts sans les diviser davantage.
ilkkachu
1
@sixtyfive, eh bien, [abc]est une partie standard des modèles glob , comme ?, je ne pense pas qu'il soit nécessaire d'aller les couvrir tous ici.
ilkkachu
4

Vous pouvez l'enregistrer en tant que tableau au lieu d'une chaîne pour l'utiliser ultérieurement dans de nombreux cas et laisser la globalisation se produire lorsque vous la définissez. Dans votre cas, par exemple:

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

ou dans l'exemple suivant, vous aurez besoin de evalcertaines des chaînes

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
la source
1
Notez comment $excludecontient les caractères génériques, vous devez désactiver la globalisation avant d'utiliser l' opérateur split + glob dessus et le restaurer pour le $i/$jet ne pas l' utiliser evalmais l'utiliser"$i"/$j
Stéphane Chazelas
Vous et ilkkachu donnez une bonne réponse. Cependant, sa réponse a identifié le problème. Alors, merci à lui.
John Siu
2

La réponse @ilkkachu a résolu le principal problème de globbing. Plein crédit à lui.

V1

Cependant, en raison du fait de excludecontenir des entrées avec et sans caractère générique (*), et elles peuvent également ne pas exister du tout, une vérification supplémentaire est nécessaire après la globalisation de $i/$j. Je partage mes conclusions ici.

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

Explication de la sortie

Voici la sortie partielle pour expliquer la situation.

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

Les éléments ci-dessus sont explicites.

/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

Ce qui précède apparaît car l'entrée d'exclusion ( $j) n'a pas de caractère générique, $i/$jdevient une concaténation de chaîne simple. Cependant, le fichier / dir n'existe pas.

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

Les éléments ci-dessus apparaissent comme une entrée d'exclusion ( $j) contient un caractère générique mais n'a aucune correspondance de fichier / répertoire, le globbing de $i/$jsimplement renvoyer la chaîne d'origine.

V2

V2 utilise un guillemet simple evalet shopt -s nullglobpour obtenir un résultat net. Aucune vérification finale de fichier / dir ne nécessite.

#!/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
la source
Un problème est que dans for j in $exclude, les globes $excludepourraient augmenter au moment de cette $excludeexpansion (et invoquer evalcela pose problème). Vous souhaitez que la globalisation soit activée pour for i in $dir, et for l in $k, mais pas pour for j in $exclude. Vous voudriez un set -favant ce dernier et un set +fpour l'autre. Plus généralement, vous voudriez régler votre opérateur split + glob avant de l'utiliser. Dans tous les cas, vous ne voulez pas que split + glob soit utilisé echo $l, $lvous devez donc le citer ici.
Stéphane Chazelas
@ StéphaneChazelas parlez-vous de v1 ou v2? Pour la v2, les deux excludeet dirssont en guillemet simple ( ), so no globbing till eval`.
John Siu
Le globalisation a lieu lors de l' expansion de variables non cotées dans des contextes de liste , c'est-à-dire (en laissant une variable sans guillemets) ce que nous appelons parfois l' opérateur split + glob . Il n'y a pas de globalisation dans les affectations aux variables scalaires. foo=*et foo='*'c'est pareil. Mais echo $fooet echo "$foo"ne le sont pas (dans des coquilles comme bash, il a été corrigé dans des coquilles comme zsh, fish ou rc, voir aussi le lien ci-dessus). Ici, vous ne voulez utiliser cet opérateur, mais à certains endroits que la partie scindée, et dans d'autres seulement la partie globale.
Stéphane Chazelas
@ StéphaneChazelas Merci pour l'info !!! Ça m'a pris un moment mais je comprends le souci maintenant. C'est très précieux !! Je vous remercie!!!
John Siu
1

Avec zsh:

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

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

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

${^array}stringest d'étendre au fur et à mesure $array[1]string $array[2]string.... $=varconsiste à séparer les mots de la variable (quelque chose que les autres shells font par défaut!), $~varfait de la globalisation sur la variable (quelque chose d'autres shells également par défaut (quand vous ne le souhaitez généralement pas, vous auriez dû citer $fci-dessus dans autres coquilles)).

(N)est un qualificatif de glob qui active nullglob pour chacun de ces globs résultant de cette $^array1/$^array2expansion. Cela fait que les globes s'étendent à rien lorsqu'ils ne correspondent pas. Cela se produit également pour transformer un non-glob ~/code/private/foo/Thumbs.dben un, ce qui signifie que si ce particulier n'existe pas, il n'est pas inclus.

Stéphane Chazelas
la source
C'est vraiment sympa. J'ai testé et travaille. Cependant, il semble que zsh soit plus sensible à la nouvelle ligne lors de l'utilisation de guillemets simples. La façon dont excludeest enfermé affecte la sortie.
John Siu du
@JohnSiu, oh oui, vous avez raison. Il semble que le split + glob et le $^arraydoit être fait en deux étapes distinctes pour s'assurer que les éléments vides sont jetés (voir éditer). Cela ressemble un peu à un bug zsh, je vais soulever le problème sur leur liste de diffusion.
Stéphane Chazelas
Je viens avec une v2 pour bash, qui est plus propre, mais toujours pas aussi compact que votre script zsh, lol
John Siu