从bash shell脚本中的glob目录内的命令行执行命令

时间:2016-12-17 00:18:39

标签: bash for-loop directory glob

在bash shell脚本do-for.sh中,我想使用bash在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

显然虽然glob *扩展到了其他命令行参数变量!因此,第一次通过循环,对于$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."

9 个答案:

答案 0 :(得分:6)

我会假设您向用户开放,必须提供某种分隔符,例如

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

你的脚本可以做类似的事情(这只是为了解释这个概念,我还没有测试过实际的代码):

CURRENT_DIR="$PWD"

declare -a FILES=()

for ARG in "$@"
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"
  "$@"
  cd "$CURRENT_DIR"
done

请注意,此解决方案的优势在于,如果您的用户忘记了" - "分隔符,她将收到通知(而不是由于引用而失败)。

答案 1 :(得分: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 * pwd./script.sh * "ls -l"

要使命令后跟参数(例如./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 "$@" )
done

答案 2 :(得分:2)

我将如何做到这一点:

#!/bin/bash

# Read directory arguments into dirs array
for arg in "$@"; 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"
    "$@"
    cd "$cur_dir"
done

这会在扩展后循环查看参数,只要它们是目录,它们就会被添加到dirs数组中。遇到第一个非目录参数时,我们假设现在命令开始了。

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

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

这适用于您的

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

示例 - 但如果repo-*扩展到目录以外的任何内容,则脚本会中断,因为它会尝试在命令中执行文件名。

例如,如果glob和命令被--之类的指示符分隔,那么它可以变得更稳定,但是如果你知道那个glob将永远只是目录,这应该工作。

答案 3 :(得分:2)

我将从您提到两次工作的Windows批处理文件开始。最大的区别在于,在Windows上,shell不进行任何通配,将其留给各种命令(并且每个命令都以不同方式执行),而在Linux / Unix上,通常由shell完成通配,并且可以是通过引用或逃避来防止。 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 globbing。这很笨拙,因为即使你记得它做了哪个shell选项,你也必须记住将它重置为默认值以使shell在之后正常工作。

  • 使用分隔符:

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

这很有效,类似于在其他shell命令的类似情况下采用的解决方案,只有当你的目录名扩展包含一个完全等于分隔符的目录名时才会失败(这是一个不太可能发生的事件,不会发生在上面的例子,但一般情况下可能会发生。)

  • 将命令作为 last 参数,所有其余的都是目录:

    ./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."

第二个版本涉及使用shell不知道的全局字符,例如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";
  "$@"
  cd "$curDir"
done

与在Windows中一样,如果前缀包含空格

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

(但是如果前缀不包含空格,则可以避免引用它,并且它将正确处理以该前缀开头的任何包含空格的目录名;明显的更改,对于第二个中的%-patterns也是如此版本)。

请注意Windows和Linux环境之间的其他相关差异,例如路径名中的区分大小写,字符被视为特殊的差异等等。

答案 4 :(得分:1)

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

答案 5 :(得分: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

答案 6 :(得分:1)

为什么不保持简单并创建一个使用find的shell函数,但减轻了用户输入命令的负担,例如:

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 

答案 7 :(得分:1)

通配符在传递给任何程序或脚本之前由shell进行评估。你无能为力。

但是如果你接受引用globbing表达式,那么这个脚本应该做的伎俩

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

答案 8 :(得分:1)

没有人使用find提出解决方案?为什么不尝试这样的事情:

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

查看man find了解更多选项。