Як зробити bash glob струнною змінною?


14

Інформація про систему

ОС: ОС 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, тож ви можете спробувати подвійне цитування вашої змінної.
Томас N

@ThomasN ні, це не працює. k- це обчислений рядок, і мені потрібно, щоб він залишався таким чином до циклу. Перевірте мою довгу версію.
Джон Сіу

@ThomasN Я оновив коротку версію, щоб зробити її більш зрозумілою.
Джон Сіу

Відповіді:


18

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

Globbing відбувається після розширення змінної, якщо змінна не цитується, як тут (*) :

$ 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

Це чітко задокументовано для Баша:

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

Розширення Tilde відбувається перед змінним розширенням, тому тильди всередині змінних не розгортаються. Простий спосіб вирішити це $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? Насправді я також досліджую цей маршрут, оскільки ускладнення циклу ускладнюється. Але у мене виникають труднощі з «шляхом».
Джон Сіу

Кредит вам як ваша інформація про tilde '~' є більш прямим до основної проблеми. Остаточний сценарій та пояснення я опублікую в іншій відповіді. Але повна честь вам: D
Джон Сіу

@JohnSiu, так, використовуючи функцію find - це те, що спочатку прийшло в голову. Це може бути корисним і в залежності від конкретної потреби. (а ще краще для деяких застосувань.)
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
Stéphane Chazelas

І ви, і ількакачу даєте хорошу відповідь. Однак його відповідь визначила це питання. Тож заслуга йому.
Джон Сіу

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

Вищезгадане відображається, оскільки запис виключення ( $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

Вищезазначене відображається як виключення запису ( $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, перш ніж використовувати його. У будь-якому випадку, ви не хочете, щоб розділити + глобул для echo $l, тому $lслід його цитувати.
Стефан Шазелас

@ StéphaneChazelas ви переглядаєте v1 чи v2? Для v2 обидва excludeі dirsв одній цитаті ( ), so no globbing till eval`.
Джон Сіу

Глоббінг відбувається після розширення змін, що не котируються, в контекстах списку , що (залишаючи змінну без котирування) - це те, що ми іноді називаємо оператором split + glob . У присвоєннях скалярних змінних немає глобалізації. foo=*і foo='*'те саме. Але echo $fooі echo "$foo"не є (в шкаралупі подобається bash, це було зафіксовано в оболонках , як ЗШ, риби або гс см також посилання вище). Тут же хочу використовувати цей оператор, але в деяких місцях тільки розділову частину, а в інших тільки Глоба частини.
Стефан Шазелас

@ 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, о так, ти маєш рацію. Здається, розділення + глобус і $^arrayнеобхідно зробити в два окремі етапи, щоб переконатися, що порожні елементи відкидаються (див. Редагування). Це трохи схоже на помилку zsh, я підніму це питання у їхньому списку розсилки.
Стефан Шазелас

Я придумав v2 для bash, який є більш чистим, але все ще не таким компактним, як ваш скрипт zsh, lol
Джон Сіу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.