2016-12-17 214 views
4

在bash shell脚本中do-for.sh我想在使用bash的glob中命名的所有目录中执行命令。这已经回答了很多次,但我想在命令行上提供命令本身。换句话说假设我有目录:从bash shell脚本中的glob目录中的命令行执行命令

  • foo
  • bar

我想进入

do-for * pwd 

,并有bash的打印工作目录foo,然后里面bar

从阅读在网络上许许多多的答案,我想我能做到这一点:

for dir in $1; do 
    pushd ${dir} 
    $2 $3 $4 $5 $6 $7 $8 $9 
    popd 
done 

但显然,水珠*被扩展到其他的命令行参数变量!所以第一次通过循环,对于$2 $3 $4 $5 $6 $7 $8 $9我预计foo pwd,但它似乎我得到foo bar

如何让命令行上的glob不被扩展为其他参数?或者有更好的方法来解决这个问题吗?

为了使这个更清楚,这里是我想要如何使用批处理文件。 (这工作正常的Windows批处理文件版本,顺便说一句。)

./do-for.sh repo-* git commit -a -m "Added new files." 
+0

请参阅http://mywiki.wooledge.org/Quotes我向你保证它会回答你所有的问题。 – andlrc

+0

用'do-for'*'pwd'引用它。但是如果任何目录的名字中有空格,你的脚本将无法工作。 – Barmar

+0

andlr我相信你的页面是一个很好的参考,但标准的堆栈溢出实践是提供答案;链接是次要的。而Barmar,我不想强​​迫我的用户添加引号。 –

回答

6

我会假设你是开放的用户必须提供某种分离的,像这样

./do-for.sh repo-* -- git commit -a -m "Added new files." 

你的脚本可以做这样的事情(这只是为了说明这个概念,我还没有测试实际的代码):

CURRENT_DIR="$PWD" 

declare -a FILES=() 

for ARG in "[email protected]" 
do 
    [[ "$ARG" != "--" ]] || break 
    FILES+=("$ARG") 
    shift 
done 

if 
    [[ "${1-}" = "--" ]] 
then 
    shift 
else 
    echo "You must terminate the file list with -- to separate it from the command" 
    (return, exit, whatever you prefer to stop the script/function) 
fi 

在这一点上,你必须在一个阵列中的所有目标文件,“$ @”只包含要执行的命令。所有这一切都剩下要做的就是:

for FILE in "${FILES[@]-}" 
do 
    cd "$FILE" 
    "[email protected]" 
    cd "$CURRENT_DIR" 
done 

请注意,该解决方案具有以下优点:如果您的用户忘记了“ - ”分隔,她将被通知(而不是失败,由于报价)。

+0

其他建议的解决方案假定使用的命令永远不会有名称等于有效的目录。虽然这允许没有分隔符的解决方案,但根据您的情况,这也是一个有风险的假设。我个人更喜欢有一个分隔符来允许可能被忽视的微妙失败模式。 – Fred

+0

不幸的是,似乎没有完美的答案。这似乎只是bash的限制。许多答案都很有帮助,尤其是@Dario的答案非常全面和写得很好。这可能是最简单和最优雅的解决方案,尽管在很大程度上它是一个偏好问题。我个人决定用'do:'作为分隔符。非常感谢大家的回应! –

+0

谢谢。有很多事情Bash没有完美的解决方案,关键是找到一个可行的解决方案,不是太慢(对于大多数脚本来说很少是问题),并且足够清晰,以至于在5年后您需要时可以理解它修改或修复它。至少,这是我想要做的。 – Fred

1

在bash,那么你可以执行“设置-o noglob”,这将抑制外壳扩展路径名(水珠)。但是,在执行脚本之前,必须在运行的shell上设置它,否则应引用您在参数中提供的任何元字符。

+0

我不能强制我的用户在运行我的shell脚本之前设置一些不明确的(对他们)标志。 –

1

find-while-read组合是解析文件名最安全的组合之一。这样做下面

#!/bin/bash 
myfunc(){ 
cd "$2" 
eval "$1" # Execute the command parsed as an argument 
} 
cur_dir=$(pwd) # storing the current directory 
find . -type d -print0 | while read -rd '' dname 
do 
myfunc "pwd" "$dname" 
cd "$cur_dir" #Remember myfunc changes the current working dir, so you need this 
done 
3

在这种情况下,问题不是元字符的扩展,仅仅是你的脚本有参数的不确定数,其中最后一个是为所有以前的参数执行命令。

#!/bin/bash 
CMND=$(eval echo "\${$#}")  # get the command as last argument without arguments or 
while [[ $# -gt 1 ]]; do   # execute loop for each argument except last one 
    (cd "$1" && eval "$CMND") # switch to each directory received and execute the command 
    shift     # throw away 1st arg and move to the next one in line 
done 

用途:(实施例./script.sh * LS -l)./script.sh * pwd./script.sh * "ls -l"

要让命令后跟参数的脚本具有要长,因为每个参数具有如果待测试它是一个目录,直到命令被识别为止(或者直到一个dir被识别为止)。

这里是另一种脚本,将接受语法:./script.sh <dirs...> <command> <arguments...> 例如:./script.sh * ls -la

# Move all dirs from args to DIRS array 
typeset -i COUNT=0 
while [[ $# -gt 1 ]]; do 
    [[ -d "$1" ]] && DIRS[COUNT++]="$1" && shift || break 
done 

# Validate that the command received is valid 
which "$1" >/dev/null 2>&1 || { echo "invalid command: $1"; exit 1; } 

# Execute the command + it's arguments for each dir from array 
for D in "${DIRS[@]}"; do 
    (cd "$D" && eval "[email protected]") 
done 
+0

所以只是为了澄清,这是否是一个有效的用法? './do-for.sh repo- * git commit -a -m“添加了新文件。”' –

+0

不,因为命令需要很多参数。与脚本一起工作的表单是:'./do-for.sh repo- *'git commit -a -m“添加了新文件。” '' 我会给你写更长的版本,以完全符合你的要求。 – czvtools

+0

请注意,这是非常简单的写在Windows批处理文件。 :) –

2

这里是我会怎么做:

#!/bin/bash 

# Read directory arguments into dirs array 
for arg in "[email protected]"; do 
    if [[ -d $arg ]]; then 
     dirs+=("$arg") 
    else 
     break 
    fi 
done 

# Remove directories from arguments 
shift ${#dirs[@]} 

cur_dir=$PWD 

# Loop through directories and execute command 
for dir in "${dirs[@]}"; do 
    cd "$dir" 
    "[email protected]" 
    cd "$cur_dir" 
done 

这遍历参数所看到扩展后,只要它们是目录,它们就会被添加到dirs阵列中。只要遇到第一个非目录参数,我们就认为现在该命令开始。

然后将目录从shift的参数中删除,我们将当前目录存储在cur_dir中。

最后一个循环访问每个目录并执行由其余参数组成的命令。

这适用于您的

./do-for.sh repo-* git commit -a -m "Added new files." 

例如–但如果repo-*扩展到比目录以外的任何的脚本打破,因为它会试图执行的文件名作为命令的一部分。

它可以更加稳定的,如果,例如,水珠和命令是由一个指标,如--分开的,但如果你知道水珠永远只是目录,这应该工作。

+0

我刚刚注意到这与解决方案非常相似@czvtools。他的最后一个版本不适合你,@Garret? –

+0

哦,你正在检查每个参数是否是一个目录---聪明。我还没有看到@czvtools的更新答案。 –

1

为什么不保持简单,创​​建使用find但减轻了您打字的命令,例如用户的负担壳功能:

do_for() { find . -type d \(! -name . \) -not -path '*/\.*' -name $1 -exec bash -c "cd '{}' && "${@:2}" " \; } 

这样他们就可以输入类似do_for repo-* git commit -a -m "Added new files." 注,如果你想通过自己使用*,你将不得不逃避它:

do_for \* pwd 
1

通配符被shell传递给任何程序或脚本之前评估。对此你无能为力。

但是,如果你接受引述通配符的表达,则该脚本应该做的伎俩

#!/usr/bin/env bash 

for dir in $1; do (
    cd "$dir" 
    "${@:2}" 
) done 

我尝试过了两个试验目录和它似乎是工作。使用这样的:

mkdir test_dir1 test_dir2 
./do-for.sh "test_dir*" git init 
./do-for.sh "test_dir*" touch test_file 
./do-for.sh "test_dir*" git add . 
./do-for.sh "test_dir*" git status 
./do-for.sh "test_dir*" git commit -m "Added new files." 
+0

我想有人在之前提到过。他们表示,如果我没有记错的话,它不适用于包含空格的目录。真的吗? –

+0

这不会是一个问题。 'cd'$ dir“'中的引用处理空格。 –

+0

@HaraldNordgren您对'cd“$ dir”'处理空间是正确的,但是'$ 1'被扩展的项目将仅包含最近shell中的空格。我不记得什么时候做出改变,但我清楚地记得,几年前,当一个路径名包含空格时,它在这个状态中扩展为两个不同的项目。不要批评你的答案,只是说OP应该知道...... – Dario

1

没有人建议用find的解决方案?为什么不尝试这样的事:

find . -type d \(-wholename 'YOURPATTERN' \) -print0 | xargs -0 YOURCOMMAND 

man find了更多的选择。

2

我将与你提到的两倍,工作在Windows批处理文件开始。最大的区别是,在Windows上,外壳不作任何通配,把它留给各种命令(和他们每个人做不同的看法),而在Linux/Unix文件名匹配通常是由壳做的,可通过引用或转义来阻止。同时在Windows方法和Linux的做法有其优点,和他们比较不同在不同的使用情况。

对于经常bash用户,报价

./do-for.sh repo-'*' git commit -a -m "Added new files." 

或逃避

./do-for.sh repo-\* git commit -a -m "Added new files." 

是最简单的解决方案,因为他们是什么,他们每天都在坚持使用。如果用户需要不同的语法,你迄今提出的所有的解决方案,我将分为四类提出我自己的(请注意,在下面do-for.sh为各自例如为不同脚本采取相应的解决方案之前,它可以在其他答案中找到)

  • 禁用shell匹配。这是笨拙的,因为,即使你还记得哪些外壳选择这样做,你必须记住要重置为默认有壳之后正常工作。

  • 使用分隔:

    ./do-for.sh repo-* -- git commit -a -m "Added new files." 
    

这工作,类似于与其他shell命令类似的情况下采取的解决办法,只有当你的目录名的扩展包括目录名称完全失败等于所述分离器(一个不太可能的情况,这不会在上面的例子中发生的,但在一般情况可能会发生。)

  • 具有命令作为最后参数,其余全部是目录:

    ./do-for.sh repo-* 'git commit -a -m "Added new files."' 
    

这工作,但同样,它涉及到报价,甚至可能嵌套,并且没有一点在它宁愿更常规的引用通配的字符。

  • 自作聪明:

    ./do-for.sh repo-* git commit -a -m "Added new files." 
    

,并考虑待处理目录,直到你打一个名字是不是一个目录。这在很多情况下都会起作用,但可能会以模糊的方式失败(例如,当您拥有一个与命令类似的目录时)。

我的解决方案不属于任何提及的类别。事实上,我建议不要在脚本的第一个参数中使用*作为通配符。 (这与split命令使用的语法类似,您可以为要生成的文件提供非全局前缀参数。)我有两个版本(以下代码)。与第一个版本,你会做以下几点:

 # repo- is a prefix: the command will be excuted in all 
     # subdirectories whose name starts with it 
     ./do-for.sh repo- git commit -a -m "Added new files." 

     # The command will be excuted in all subdirectories 
     # of the current one 
     ./do-for.sh . git commit -a -m "Added new files." 

     # If you want the command to be executed in exactly 
     # one subdirectory with no globbing at all, 
     # '/' can be used as a 'stop character'. But why 
     # use do-for.sh in this case? 
     ./do-for.sh repo/ git commit -a -m "Added new files." 

     # Use '.' to disable the stop character. 
     # The command will be excuted in all subdirectories of the 
     # given one (paths have to be always relative, though) 
     ./do-for.sh repos/. git commit -a -m "Added new files." 

第二个版本涉及到使用通配符字符外壳一无所知,如SQL的%字符

 # the command will be excuted in all subdirectories 
     # matching the SQL glob 
     ./do-for.sh repo-% git commit -a -m "Added new files." 
     ./do-for.sh user-%-repo git commit -a -m "Added new files." 
     ./do-for.sh % git commit -a -m "Added new files." 

第二个版本是更加灵活,因为它允许非最终的球体,但对于bash世界而言不太标准。

下面是代码:

#!/bin/bash 
if [ "$#" -lt 2 ]; then 
    echo "Usage: ${0##*/} PREFIX command..." >&2 
    exit 1 
fi 

pathPrefix="$1" 
shift 

### For second version, comment out the following five lines 
case "$pathPrefix" in 
    (*/) pathPrefix="${pathPrefix%/}" ;; # Stop character, remove it 
    (*.) pathPrefix="${pathPrefix%.}*" ;; # Replace final dot with glob 
    (*) pathPrefix+=\* ;;     # Add a final glob 
esac 
### For second version, uncomment the following line 
# pathPrefix="${pathPrefix//%/*}"  # Add a final glob 

tmp=${pathPrefix//[^\/]} # Count how many levels down we have to go 
maxDepth=$((1+${#tmp})) 


# Please note that this won’t work if matched directory names 
# contain newline characters (comment added for those bash freaks who 
# care about extreme cases) 
declare -a directories=() 
while read d; do 
    directories+=("$d") 
done < <(find . -maxdepth "$maxDepth" -path ./"$pathPrefix" -type d -print) 

curDir="$(pwd)" 
for d in "${directories[@]}"; do 
    cd "$d"; 
    "[email protected]" 
    cd "$curDir" 
done 

与Windows,你仍然需要如果前缀包含空格

使用引号
 ./do-for.sh 'repository for project' git commit -a -m "Added new files." 

(但如果前缀不包含空格,你可以避免引用它,它将正确处理以该前缀开头的任何包含空格的目录名;有明显的变化,第二个版本中的%-patterns也是如此)。请注意Windows和Linux环境之间的其他相关差异,例如路径名中的区分大小写,字符被认为特殊的区别等等。

+0

谢谢你写得很好的答案!最后我不得不选择一个,因此这个奖项归于@Fred,因为这是我决定在现实生活中使用的方法。 –