¿Cómo hacer que bash glob sea una variable de cadena?

14

Información del sistema

OS: OS X

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

Antecedentes

Quiero que Time Machine excluya un conjunto de directorios y archivos de todo mi proyecto git / nodejs. Mis directorios de proyecto están en el directorio ~/code/private/y ~/code/public/por eso estoy tratando de usar bash looping para hacer el tmutil.

Problema

Version corta

Si tengo una variable de cadena calculadak , ¿cómo hago que sea global o justo antes de un ciclo 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

En la versión larga a continuación, verá k=$i/$j. Entonces no puedo codificar la cadena en el bucle for.

Versión larga

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

Salida

No son globbed. No es lo que quiero.

~/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
fuente
Las comillas simples detienen la interpolación de shell en Bash, por lo que puede intentar hacer una doble cita de su variable.
Thomas N
@ThomasN no, eso no funciona. kes una cadena calculada, y necesito que permanezca así hasta el bucle. Por favor revise mi versión larga.
John Siu
@ThomasN Actualicé la versión corta para hacerlo más claro.
John Siu

Respuestas:

18

Puede forzar otra ronda de evaluación con eval, pero eso no es realmente necesario. (Y evalcomienza a tener serios problemas en el momento en que los nombres de sus archivos contienen caracteres especiales como $). El problema no es el engorde, sino la expansión de tilde.

El globalizado ocurre después de la expansión de la variable, si la variable no se cita, como aquí (*) :

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

Entonces, en la misma línea, esto es similar a lo que hiciste, y funciona:

$ 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

Pero con la tilde no lo hace:

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

Esto está claramente documentado para Bash:

El orden de las expansiones es: expansión de llaves; expansión de tilde, expansión de parámetros y variables, ...

La expansión de tildes ocurre antes de la expansión de variables, por lo que las tildes dentro de las variables no se expanden. La solución fácil es usar $HOMEo la ruta completa en su lugar.

(* expandir globos desde variables generalmente no es lo que quieres)


Otra cosa:

Cuando recorres los patrones, como aquí:

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

Tenga en cuenta que, como $excludeno se cita, está dividido y también englobado en este punto. Entonces, si el directorio actual contiene algo que coincide con el patrón, se expande a eso:

$ 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

Para evitar esto, use una variable de matriz en lugar de una cadena dividida:

$ 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

Como una ventaja adicional, las entradas de matriz también pueden contener espacios en blanco sin problemas con la división.


Se podría hacer algo similar find -pathsi no le importa en qué nivel de directorio deberían estar los archivos de destino. Por ejemplo, para encontrar cualquier ruta que termine en /e2e/*.js:

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

Tenemos que usarlo en $HOMElugar de hacerlo ~por la misma razón que antes, y $dirsdebe estar sin comillas en la findlínea de comando para que se divida, pero $patterndebe citarse para que el shell no lo expanda accidentalmente.

(Creo que podría jugar con -maxdepthGNU para limitar la profundidad de la búsqueda, si le importa, pero ese es un problema un poco diferente).

ilkkachu
fuente
¿Eres la única respuesta con find? De hecho, también estoy explorando esa ruta, ya que el ciclo for se está complicando. Pero estoy teniendo dificultades con el 'camino'.
John Siu
El crédito a usted como su información sobre tilde '~' es más directo al problema principal. Publicaré el guión final y la explicación en otra respuesta. Pero crédito completo para usted: D
John Siu
@JohnSiu, sí, usar find fue lo primero que me vino a la mente. También puede ser utilizable, dependiendo de la necesidad exacta. (o mejor también, para algunos usos.)
ilkkachu
1
@kevinarpe, creo que las matrices están destinadas básicamente a eso, y sí, "${array[@]}"(¡con las comillas!) está documentado (ver aquí y aquí ) para expandir a los elementos como palabras distintas sin dividirlos más.
ilkkachu
1
@sixtyfive, bueno, [abc]es una parte estándar de los patrones globales , como ?, no creo que sea necesario cubrirlos todos aquí.
ilkkachu
4

Puede guardarlo como una matriz en lugar de una cadena para usarlo más tarde en muchos casos y dejar que ocurra el bloqueo cuando lo defina. En su caso, por ejemplo:

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

o en el ejemplo posterior, necesitará evalalgunas de las cadenas

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
fuente
1
Tenga en cuenta cómo $excludecontiene los comodines, necesitaría deshabilitar el globbing antes de usar el operador split + glob en él y restaurarlo para $i/$jy no usar evalsino usar"$i"/$j
Stéphane Chazelas
Tanto usted como ilkkachu dan una buena respuesta. Sin embargo, su respuesta identificó el problema. Así que acéptalo a él.
John Siu
2

La respuesta de @ilkkachu resolvió el principal problema global. Todo el crédito para él.

V1

Sin embargo, debido a que excludecontienen entradas con y sin comodín (*), y también pueden no existir en absoluto, se necesita una comprobación adicional después de la aplicación global $i/$j. Estoy compartiendo mis hallazgos aquí.

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

Explicación de salida

A continuación se muestra el resultado parcial para explicar la situación.

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

Lo anterior se explica por sí mismo.

/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

Lo anterior se muestra porque la entrada de exclusión ( $j) no tiene comodín, se $i/$jconvierte en una concatenación de cadena simple. Sin embargo, el archivo / directorio no existe.

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

Lo anterior se muestra como una entrada exclude ( $j) que contiene comodines pero no tiene coincidencia de archivo / directorio, el globing $i/$jsolo devuelve la cadena original.

V2

V2 utiliza comillas simples evaly shopt -s nullglobpara obtener un resultado limpio. No se requiere verificación final de archivo / directorio.

#!/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
fuente
Un problema es que en for j in $excludeel interior, los globos $excludepodrían expandirse en el momento de esa $excludeexpansión (y recurrir evala eso es pedir problemas). Desea habilitar el globbing para for i in $dir, y for l in $k, pero no para for j in $exclude. Querrías un set -fantes del último y un set +fpara el otro. En términos más generales, querrá ajustar su operador de división + glob antes de usarlo. En cualquier caso, no desea dividir + glob para echo $l, por lo que $ldebe citarse allí.
Stéphane Chazelas
@ StéphaneChazelas ¿te refieres a v1 o v2? Para v2, ambos excludey dirsestán entre comillas simples ( ), so no globbing till eval`.
John Siu
Globbing se lleva a cabo en la expansión de variables sin comillas en contextos de lista , que (dejando una variable sin comillas) es lo que a veces llamamos el operador split + glob . No hay problema en las asignaciones a variables escalares. foo=*Y foo='*'es lo mismo. Pero echo $fooy echo "$foo"no lo son (en shells como bash, se ha corregido en shells como zsh, fish o rc, vea también el enlace de arriba). Aquí no desea utilizar ese operador, pero en algunos lugares sólo la parte dividida, y en otros sólo la parte pegote.
Stéphane Chazelas
@ StéphaneChazelas Gracias por la información !!! Me llevó alguna vez, pero ahora entiendo la preocupación. ¡Esto es muy valioso! ¡¡¡Gracias!!!
John Siu
1

Con zsh:

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

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

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

${^array}stringes expandir como $array[1]string $array[2]string.... $=varconsiste en dividir las palabras en la variable (¡algo que hacen otros shells de forma predeterminada!), $~varse pega a la variable (algo que otros shells también de forma predeterminada (cuando generalmente no quiere que lo hagan, habría tenido que citar $farriba en otras conchas)).

(N)es un calificador de glob que activa nullglob para cada uno de esos globos que resultan de esa $^array1/$^array2expansión. Eso hace que los globos se expandan a la nada cuando no coinciden. Eso también pasa a convertir un no-glob ~/code/private/foo/Thumbs.dben uno, lo que significa que si ese particular no existe, no está incluido.

Stéphane Chazelas
fuente
Esto es realmente lindo Lo probé y funciona. Sin embargo, parece que zsh es más sensible a la nueva línea cuando se usa comilla simple. La forma excludeencerrada está afectando la salida.
John Siu
@ JohnSiu, oh sí, tienes razón. Parece que el split + glob y el $^arraydebe hacerse en dos pasos separados para asegurarse de que los elementos vacíos se descartan (ver edición). Parece un error zsh, plantearé el problema en su lista de correo.
Stéphane Chazelas
Se me ocurrió un v2 para bash, que es más limpio, pero aún no tan compacto como su script zsh, jajaja
John Siu