Как сделать bash glob строковой переменной?

14

Системная информация

ОС: OS X

bash: GNU bash, версия 3.2.57 (1) -релиз (x86_64-apple-darwin16)

Фон

Я хочу, чтобы машина времени исключала набор каталогов и файлов из всего моего проекта git / nodejs. Мои каталоги проектов находятся в, ~/code/private/и ~/code/public/поэтому я пытаюсь использовать цикл bash, чтобы сделать tmutil.

вопрос

Укороченная версия

Если у меня есть вычисляемая строковая переменная k, как мне сделать ее внутри или прямо перед циклом 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

В длинной версии ниже вы увидите k=$i/$j. Поэтому я не могу жестко закодировать строку в цикле for.

Длинная версия

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

Выход

Они не сгущены. Не то, что я хочу.

~/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
Джон Сиу
источник
Одинарные кавычки останавливают интерполяцию оболочки в Bash, так что вы можете попробовать заключить двойную кавычку в вашу переменную.
Томас Н
@ TomasN нет, это не работает. kявляется вычисляемой строкой, и мне нужно, чтобы она оставалась такой до цикла. Пожалуйста, проверьте мою длинную версию.
Джон Сиу
@ThomasN Я обновил короткую версию, чтобы сделать ее более понятной.
Джон Сиу

Ответы:

18

Вы можете провести еще один раунд оценки eval, но на самом деле это не обязательно. (И evalначинаются серьезные проблемы, когда имена ваших файлов содержат специальные символы, такие как $.) Проблема не в глобализации, а в расширении тильды.

Глобирование происходит после раскрытия переменной, если переменная не заключена в кавычки, как здесь (*) :

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

Итак, в том же духе, это похоже на то, что вы сделали, и работает:

$ 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

Но с тильдой это не так:

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

Это ясно задокументировано для Bash:

Порядок разложений: раскладывание скобок; расширение тильды, расширение параметров и переменных, ...

Расширение тильды происходит до раскрытия переменных, поэтому тильды внутри переменных не раскрываются. Простой обходной путь - использовать $HOMEполный путь или вместо него.

(* расширение глобусов из переменных обычно не то, что вы хотите)


Еще одна вещь:

Когда вы перебираете шаблоны, как здесь:

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

обратите внимание, что, как $excludeи в кавычках, он и расколот, и в этом месте также слит. Таким образом, если текущий каталог содержит что-то, соответствующее шаблону, он расширяется до следующего:

$ 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

Чтобы обойти это, используйте переменную-массив вместо разделенной строки:

$ 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

В качестве дополнительного бонуса записи массива также могут содержать пробелы без проблем с разбиением.


Нечто подобное можно сделать find -path, если вы не возражаете против того, каким уровнем каталогов должны быть целевые файлы. Например, чтобы найти любой путь, заканчивающийся на /e2e/*.js:

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

Мы должны использовать $HOMEвместо того же ~по той же причине, что и раньше, и $dirsдолжны быть заключены в кавычки в findкомандной строке, чтобы разделить их, но $patternдолжны быть заключены в кавычки, чтобы случайно не раскрыться оболочкой.

(Я думаю, что вы могли бы поиграть с -maxdepthGNU find, чтобы ограничить глубину поиска, если вам интересно, но это немного другая проблема.)

ilkkachu
источник
Вы один ответ с find? На самом деле я тоже изучаю этот маршрут, так как цикл for усложняется. Но у меня возникли проблемы с «-path».
Джон Сиу
Благодарим вас за то, что ваша информация о тильде '~' более непосредственно связана с основным вопросом. Я опубликую окончательный сценарий и объяснение в другом ответе. Но полная заслуга тебе: D
Джон Сиу
@JohnSiu, да, первое, что пришло в голову, - это найти. Это также может быть полезным, в зависимости от конкретной необходимости. (или даже лучше, для некоторых целей.)
ilkkachu
1
@kevinarpe, я думаю, что массивы в основном предназначены именно для этого, и да, "${array[@]}"(с кавычками!) задокументировано (см. здесь и здесь ) расширить элементы как отдельные слова, не разбивая их дальше.
ilkkachu
1
@sixtyfive, ну, [abc]это стандартная часть шаблонов глобусов , вроде бы ?, я не думаю, что здесь необходимо охватить все из них.
ilkkachu
4

Вы можете сохранить его в виде массива вместо строки, чтобы использовать его позже во многих случаях и разрешить глобализацию при ее определении. В вашем случае, например:

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

или в последнем примере вам понадобятся evalнекоторые строки

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
Эрик Ренуф
источник
1
Обратите внимание на то, что $excludeсодержит подстановочные знаки, вам нужно отключить глобирование перед использованием оператора split + glob на нем и восстановить его для использования $i/$jи не использовать, evalа использовать"$i"/$j
Стефан Шазелас
И вы, и илккачу дайте хороший ответ. Однако его ответ определил проблему. Так что кредит ему.
Джон Сиу
2

Ответ @ilkkachu решил основную проблему. Полный кредит ему.

V1

Однако из-за того, что в них excludeсодержатся записи как с подстановочными знаками (*), так и без них, а также они могут отсутствовать во всех, требуется дополнительная проверка после выделения $i/$j. Я делюсь своими выводами здесь.

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

Объяснение вывода

Ниже приводится частичный вывод, чтобы объяснить ситуацию.

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

Вышесказанное говорит само за себя.

/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

Выше показано, потому что exclude entry ( $j) не имеет подстановочных знаков, $i/$jстановится простой конкатенацией строк. Однако файл / dir не существует.

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

Выше показано, что как exclude entry ( $j) содержит подстановочный знак, но не соответствует файлу / каталогу, $i/$jпросто возвращает исходную строку.

V2

V2 использовать одинарные кавычки evalи shopt -s nullglobполучить чистый результат. Не требуется проверка файла / каталога.

#!/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
Джон Сиу
источник
Одна проблема заключается в том for j in $exclude, что $excludeво время этого $excludeрасширения глобусы могут быть расширены (и вызов evalэтого порождает проблемы). Вы бы хотели, чтобы глобирование было включено for i in $dir, for l in $kно не для for j in $exclude. Вы бы хотели set -fдо последнего и set +fдля другого. В более общем смысле, вы бы хотели настроить свой оператор split + glob перед его использованием. В любом случае, вы не хотите, чтобы split + glob использовался echo $l, поэтому $lдолжны быть указаны там.
Стефан Шазелас
@ StéphaneChazelas вы ссылаетесь на v1 или v2? Для v2 оба excludeи dirsнаходятся в одинарных ), so no globbing till кавычках ( eval`.
John Siu
Глобирование происходит при раскрытии переменных без кавычек в контекстах списков , что (оставляя переменную без кавычек) - это то, что мы иногда называем оператором split + glob . В назначениях на скалярные переменные нет сглаживания. foo=*и foo='*'то же самое. Но echo $fooи echo "$foo"нет (в оболочках типа bash, это было исправлено в оболочках, таких как zsh, fish или rc, см. Также ссылку выше). Здесь же хочу использовать этот оператор, но в некоторых местах только разделительную часть, а в других только Глоба части.
Стефан Шазелас
@ StéphaneChazelas Спасибо за информацию! Взял меня когда-нибудь, но теперь я понимаю беспокойство. Это очень ценно !! Спасибо!!!
Джон Сиу
1

С zsh:

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

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

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

${^array}stringэто расширить как $array[1]string $array[2]string.... $=varвыполняет разделение слов в переменной (что по умолчанию делают другие оболочки!), $~varвыполняет глобализацию переменной (что по умолчанию делают и другие оболочки (когда вы, как правило, не хотите этого, вы должны были бы процитировать $fвыше в другие снаряды)).

(N)является классификатором глобуса, который включает nullglob для каждого из этих глобусов, полученных в результате этого $^array1/$^array2расширения. Это заставляет шары расширяться до нуля, когда они не совпадают. Это также происходит, чтобы превратить не-глобус, как ~/code/private/foo/Thumbs.dbв один, что означает, что если этот конкретный не существует, он не включается.

Стефан Шазелас
источник
Это действительно мило. Я проверял и работает. Однако кажется, что zsh более чувствителен к переводу строки при использовании одинарных кавычек. Способ excludeзаключается в том, что влияет на вывод.
Джон Сиу
@JohnSiu, о да, ты прав. Кажется, что split + glob и $^arrayдолжны быть выполнены в два отдельных шага, чтобы убедиться, что пустые элементы отбрасываются (см. Редактирование). Это похоже на ошибку zsh, я подниму вопрос в их списке рассылки.
Стефан Шазелас
Я придумываю v2 для bash, который чище, но все же не так компактен, как ваш сценарий zsh, лол
Джон Сиу