如何使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中的Shell插值,因此您可以尝试将变量双引号。
Thomas N

@ThomasN不,那不起作用。k是一个计算得出的字符串,我需要一直这样直到循环。请检查我的长版本。

@ThomasN我更新了简短版本以使其更清晰。

Answers:


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

这是明确记载的击:

扩展顺序为:大括号扩展;波浪线扩展,参数和变量扩展,...

波浪线扩展发生在变量扩展之前,因此变量内部的波浪线不会扩展。最简单的解决方法是使用$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来限制搜索的深度,但这是另一个问题。)


你是一个答案find吗?实际上,我也正在探索这条路线,因为for循环越来越复杂。但是我在“路径”上遇到了困难。
约翰兆

感谢您,因为有关代字号“〜”的信息更直接地反映了主要问题。我将在另一个答案中发布最终脚本和解释。但是,请您

@JohnSiu,是的,首先想到的是使用查找。根据实际需要,它可能也可用。(或更好了,对于一些用途。)
ilkkachu

1
@kevinarpe,我认为数组基本上是用于此目的的,是的,"${array[@]}"(带有引号!)已记录在案(请参见此处此处),以扩展为元素,成为不同的词,而无需进一步拆分。
ilkkachu

1
@sixtyfive [abc]glob模式的标准部分,例如?,我认为没有必要在这里介绍所有模式。
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éphaneChazelas,2016年

您和ilkkachu都给了很好的答案。但是他的回答确定了问题所在。因此,相信他。

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变成了纯字符串连接。但是文件/目录不存在。

/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使用单引号,evalshopt -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应在该处使用引号。
斯特凡Chazelas

@StéphaneChazelas您是指v1还是v2?对于v2,两者excludedirs都在单引号中(), so no globbing till eval`。–
John Siu

通配符发生在列表上下文中的未加引号的变量扩展,即我们有时称为split + glob运算符(不加引号)。标量变量的分配不会出现混乱。foo=*并且foo='*'是相同的。但是echo $fooecho "$foo"并且不是(在诸如的shell中bash,它已在诸如zsh,fish或rc的shell中固定,另请参见上面的链接)。在这里,您确实要使用该运算符,但在某些地方,只有拆分部分,而在其他地方,则只有全局部分。
斯特凡Chazelas

@StéphaneChazelas感谢您的信息!!!某个时候花了我,但我现在明白了。这非常有价值!!谢谢!!!
John Siu

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是对变量执行分词(默认情况下,其他shell会执行此操作!),$~var会对变量进行闪动(默认情况下,其他shell也将执行某些操作)(当您通常不希望它们执行时,您必须在$f上面引用其他炮弹))。

(N)是一个glob限定符,它为由该扩展产生的每个glob启用nullglob$^array1/$^array2。这使得当它们不匹配时,小球扩展为零。这也碰巧将非全局变量~/code/private/foo/Thumbs.db变成了一个,这意味着如果不存在该特定变量,则不包含该特定变量。


真的很好 我测试并工作。但是,似乎zsh在使用单引号时对换行符更为敏感。exclude封闭的方式影响输出。
约翰兆

@JohnSiu,哦,是的,你是对的。似乎split + glob和$^array必须在两个单独的步骤中完成,以确保丢弃空元素(请参见编辑)。看起来有点像中的错误zsh,我将在他们的邮件列表中提出这个问题。
斯特凡Chazelas

我想出了一个v2 for bash,它更干净,但仍然不如您的zsh脚本紧凑,大声笑
John Siu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.