Como fazer bash glob uma variável de string?

14

Informação do sistema

SO: OS X

bash: GNU bash, versão 3.2.57 (1) -release (x86_64-apple-darwin16)

fundo

Quero que o time machine exclua um conjunto de diretórios e arquivos de todo o meu projeto git / nodejs. Meus diretórios de projeto estão ~/code/private/e ~/code/public/por isso estou tentando usar o loop bash para fazer o tmutil.

Questão

Versão curta

Se eu tenho uma variável de string calculadak , como faço para torná-la global antes ou antes de um loop 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

Na versão longa abaixo, você verá k=$i/$j. Portanto, não posso codificar a string no loop for.

Versão longa

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

Resultado

Eles não são globbed. Não é o que eu quero.

~/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
fonte
As aspas simples interrompem a interpolação do shell no Bash, portanto, você pode tentar aspas duplas na sua variável.
Thomas N
@ Thomasho não, isso não funciona. ké uma string calculada e preciso que continue assim até o loop. Por favor, verifique minha versão longa.
John Siu
@ThomasN Atualizei a versão curta para torná-la mais clara.
John Siu

Respostas:

18

Você pode forçar outra rodada de avaliação eval, mas isso não é realmente necessário. (E evalcomeça a ter problemas sérios no momento em que os nomes de seus arquivos contêm caracteres especiais como $.) O problema não é com globbing, mas com a expansão do til.

O globbing ocorre após a expansão da variável, se a variável não estiver entre aspas, como aqui (*) :

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

Portanto, na mesma linha, isso é semelhante ao que você fez e 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

Mas com o til não:

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

Isso está claramente documentado para o Bash:

A ordem das expansões é: expansão de chaves; expansão de til, expansão variável e de parâmetros, ...

A expansão do til ocorre antes da expansão da variável, portanto, os tildes dentro das variáveis ​​não são expandidos. A solução fácil é usar $HOMEou o caminho completo.

(* expansão de globs de variáveis ​​geralmente não é o que você deseja)


Outra coisa:

Quando você faz um loop sobre os padrões, como aqui:

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

observe que, como $excludenão é citado, ele é dividido e também está cheio neste momento. Portanto, se o diretório atual contiver algo que corresponda ao padrão, ele será expandido para isso:

$ 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 contornar isso, use uma variável de matriz em vez de uma seqüência de caracteres 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 um bônus adicional, as entradas da matriz também podem conter espaços em branco sem problemas com a divisão.


Algo semelhante poderia ser feito find -path, se você não se importar com o nível de diretório dos arquivos de destino. Por exemplo, para encontrar qualquer caminho que termine em /e2e/*.js:

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

Temos que usar $HOMEem vez de ~pela mesma razão como antes, e $dirsprecisa ser não cotadas na findlinha de comando por isso fica dividida, mas $patterndeve ser citado por isso não é acidentalmente expandida pelo shell.

(Acho que você pode brincar com o -maxdepthGNU para limitar a profundidade da pesquisa, se você se importa, mas isso é um pouco diferente.)

ilkkachu
fonte
Você é a única resposta find? Na verdade, também estou explorando essa rota, pois o loop for está ficando complicado. Mas estou tendo dificuldades com o '-path'.
John Siu
O crédito a você, pois suas informações sobre o til '~' é mais direto para a questão principal. Vou postar o script final e a explicação em outra resposta. Mas crédito total para você: D
John Siu
@ JohnSiu, sim, o uso de find foi o que primeiro veio à mente. Também pode ser utilizável, dependendo da necessidade exata. (ou melhor também, para alguns usos.)
ilkkachu
1
@ kevinarpe, acho que as matrizes são basicamente para isso, e sim, "${array[@]}"(com as aspas!) está documentada (veja aqui e aqui ) para expandir para os elementos como palavras distintas sem dividi-las ainda mais.
22418 ilkkachu
1
@ sixtyfive, bem, [abc]é uma parte padrão dos padrões glob , tipo ?, eu não acho que seja necessário cobrir todos eles aqui.
ilkkachu 21/01
4

Você pode salvá-lo como uma matriz, em vez de uma string, para usá-lo posteriormente em muitos casos, e permitir que o globbing aconteça quando você o definir. No seu caso, por exemplo:

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

ou no exemplo posterior, você precisará de evalalgumas das strings

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
fonte
1
Note como $excludecontém wildcards, você precisa desativar englobamento antes de utilizar a divisão + glob operador nele e restaurá-lo para o $i/$je não usar eval, mas o uso"$i"/$j
Stéphane Chazelas
Você e ilkkachu dão uma boa resposta. No entanto, sua resposta identificou o problema. Então, agradeça a ele.
John Siu
2

A resposta do @ilkkachu resolveu o principal problema de globbing. Crédito total para ele.

V1

No entanto, devido ao fato de excludeconter entradas com e sem curinga (*), e elas também podem não existir, é necessária uma verificação extra após o globbing de $i/$j. Estou compartilhando minhas descobertas aqui.

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

Explicação da saída

A seguir, é apresentada a saída parcial para explicar a situação.

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

Os itens acima são auto-explicativos.

/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

O exemplo acima é exibido porque a entrada de exclusão ( $j) não possui curinga,$i/$j torna-se uma concatenação de sequência simples. No entanto, o arquivo / dir não 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

Os itens acima são exibidos como entrada de exclusão ($j ) contêm curinga, mas não têm correspondência de arquivo / diretório; o globbing $i/$japenas retorna a string original.

V2

V2 use aspas simples evale shopt -s nullglobobtenha um resultado limpo. Nenhuma verificação final de arquivo / diretório requer.

#!/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
fonte
Um problema é que for j in $exclude, no caso , os globs $excludepodem ser expandidos no momento dessa $excludeexpansão (e solicitar evalisso está causando problemas). Você gostaria que o globbing estivesse ativado para for i in $dir, e for l in $knão para for j in $exclude. Você gostaria de um set -fantes do último e um set +fpara o outro. Em geral, você deseja ajustar seu operador split + glob antes de usá-lo. De qualquer forma, você não deseja split + glob para echo $l, portanto, $ldeve ser citado lá.
Stéphane Chazelas
@ StéphaneChazelas você está se referindo à v1 ou v2? Para v2, ambos excludee dirsestão entre aspas simples ( ), so no globbing till eval`.
John Siu
O globbing ocorre na expansão de variáveis ​​não citadas em contextos de lista , que (deixando uma variável não citada) é o que às vezes chamamos de operador split + glob . Não há globbing nas atribuições para variáveis ​​escalares. foo=*e foo='*'é o mesmo. Mas echo $fooe echo "$foo"não é (em conchas como bash, foi fixado em conchas como zsh, fish ou rc, veja também o link acima). Aqui você não quiser usar esse operador, mas em alguns lugares apenas a parte dividida, e em outros apenas a parte glob.
Stéphane Chazelas
@ StéphaneChazelas Obrigado pela informação !!! Me levou um dia, mas agora entendo a preocupação. Isso é muito valioso !! Obrigado!!!
John Siu
1

Com zsh:

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

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

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

${^array}stringé expandir como $array[1]string $array[2]string.... $=varé executar a divisão de palavras na variável (algo que outras conchas fazem por padrão!), $~varfaz globbing na variável (algo que outras conchas também por padrão (quando você geralmente não as deseja, você teria que citar$f acima) outras conchas)).

(N)é um qualificador glob que ativa o nullglob para cada um desses globs resultantes dessa $^array1/$^array2expansão. Isso faz com que os globs se expandam para nada quando não combinam. Isso também transforma um não-glob como ~/code/private/foo/Thumbs.dbem um, o que significa que, se esse particular não existir, ele não será incluído.

Stéphane Chazelas
fonte
Isso é muito legal. Eu testei e funciona. No entanto, parece que o zsh é mais sensível à nova linha ao usar aspas simples. O caminho excludeestá afetando a saída.
John Siu
@ JohnSiu, oh sim, você está certo. Parece que o split + glob e $^arraydeve ser feito em duas etapas separadas para garantir que os elementos vazios sejam descartados (consulte a edição). Isso parece um bug zsh, vou abordar a questão na lista de discussão deles.
Stéphane Chazelas
Eu venho com um v2 para o bash, que é mais limpo, mas ainda não é tão compacto quanto o seu script zsh, lol
John Siu