从bashshell脚本中的glob目录中的命令行执行命令

从bashshell脚本中的glob目录中的命令行执行命令,bash,for-loop,directory,glob,Bash,For Loop,Directory,Glob,在bashshell脚本do for.sh中,我想使用bash在glob中命名的所有目录中执行一个命令。这已经被回答了很多次了,但是我想在命令行中提供命令本身。换句话说,假设我有目录: ./do-for.sh repo-* 'git commit -a -m "Added new files."' foo bar 我想进去 do-for * pwd 并让bash在foo中打印工作目录,然后在bar中打印 通过阅读网上无数的答案,我想我可以做到: for dir in $1; do p

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

./do-for.sh repo-* 'git commit -a -m "Added new files."'
  • 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批处理文件版本上运行良好。)


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

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

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

在本例中,问题不在于元字符的扩展,而在于您的脚本具有未定义数量的参数,其中最后一个参数是针对所有先前参数执行的命令

#!/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),脚本必须更长,因为必须测试每个参数是否为目录,直到识别命令为止(或向后直到识别目录为止)

下面是一个接受语法的备选脚本:
/script.sh
例如:
/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

我会这样做:

#!/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\u dir

最后一个循环访问每个目录并执行包含其余参数的命令

#!/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
这对你的工作很有用

./do-for.sh repo-* git commit -a -m "Added new files."
示例–但是如果
repo-*
扩展到除目录以外的任何内容,脚本将中断,因为它将尝试作为命令的一部分执行文件名


例如,如果glob和命令由一个指示符(如
--
)分隔,它可能会变得更稳定,但是如果您知道glob始终只是目录,这应该会起作用。

我假设您对您的用户开放,必须提供某种分隔符,就像这样

./do-for.sh repo-* -- git commit -a -m "Added new files."
您的脚本可以执行以下操作(这只是为了解释概念,我还没有测试实际代码):

此时,数组中有所有目标文件,“$@”只包含要执行的命令。剩下要做的就是:

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

请注意,此解决方案的优点是,如果您的用户忘记了“-”分隔符,将通知她(而不是因为引用而失败)。

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

do_for() { find . -type d \( ! -name . \) -not -path '*/\.*' -name $1 -exec bash -c "cd '{}' && "${@:2}" " \;  }
因此,他们可以键入类似于“添加新文件”这样的内容,如
do\u for repo-*git commit-a-m
注意,如果要单独使用*,则必须将其转义:

do_for \* pwd 

通配符在传递给任何程序或脚本之前由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."
find . -type d \( -wholename 'YOURPATTERN' \) -print0 | xargs -0 YOURCOMMAND

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

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."
find . -type d \( -wholename 'YOURPATTERN' \) -print0 | xargs -0 YOURCOMMAND

查看
man find
了解更多选项。

我将从您提到的两倍于工作的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命令在类似情况下采用的解决方案类似,并且只有在目录名扩展包含与分隔符完全相同的目录名时才会失败(这是一个不太可能发生的事件,在上面的示例中不会发生,但在gen中不会发生)
        # 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."
#!/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
        ./do-for.sh 'repository for project' git commit -a -m "Added new files."