使用作为 bash 脚本参数传递的 glob 表达式

Rei*_*ein 2 linux bash shell

特尔;博士:

为什么不调用./myscript foo*myscript具有var=$1相同的调用./myscriptvar=foo*硬编码?


更长的形式

我在编写的 bash 脚本中遇到了一个奇怪的问题。我确信有一个简单的解释,但我无法弄清楚。

我正在尝试传递要在脚本中作为变量分配的命令行参数。

我希望脚本允许 2 个命令行参数,如下所示:

$ bash my_bash_script.bash args1 args2
Run Code Online (Sandbox Code Playgroud)

在我的脚本中,我分配了这样的变量:

ARGS1=$1
ARGS2=$2
Run Code Online (Sandbox Code Playgroud)

Args 1 是要添加到输出文件的字符串描述符。

Args 2 是一组目录:“dir1, dir2, dir3”,我将其作为 dir*

当我dir*在脚本中分配给 ARGS2 时它工作正常,但是当我dir*作为第二个命令行参数传递时,它只包含dir1dir*.

我认为这与 shell 如何处理通配符(即使作为 args 传递)有关,但我并不真正理解它。

任何帮助,将不胜感激。


环境/使用

我有一组目录:

dir_1_y_map, dir_1_x_map, dir_2_y_map, dir_2_x_map,
    ... dir_10_y_map, dir_10_x_map...
Run Code Online (Sandbox Code Playgroud)

这些目录里面,我试图访问一个文件扩展名".status"通过*.status,并".report.txt"通过*report.txt

我想dir_*_map作为第二个参数传递给脚本并将其存储在变量 ARGS2 中,然后使用它在每个目录中搜索".status"".report"文件。

问题是dir_*_map从命令行传递不会给出目录列表,而只是列表中的第一项。如果我ARGS2=dir_*_map在脚本中分配变量,它会按我的意图工作。


解决方法:引用

事实证明,在引号中传递第二个参数允许通配符扩展适用于 "dir_*_map"

#!/usr/bin/env bash
ARGS1=$1    
ARGS2=$2

touch $ARGS1".extension"

for i in /$ARGS2/*.status
do
    grep -e "string" $i >> $ARGS1".extension"
done
Run Code Online (Sandbox Code Playgroud)

这是脚本的示例调用:

sh ~/path/to/script descriptor "dir_*_map"
Run Code Online (Sandbox Code Playgroud)

我不完全理解何时/为什么必须在引号中传递某些参数,但我认为它与 for 循环中的通配符扩展有关。

Cha*_*ffy 9

解决“为什么”

赋值,如var=foo*,不展开 globs —— 也就是说,当你运行时var=foo*,文字字符串foo*被放入变量foo,而不是匹配的文件列表foo*

相比之下,foo*在命令行上不加引号的使用扩展了 glob,用一个单独的名字列表替换它,每个名字都作为一个单独的参数传递

因此,除非不存在与该 glob 表达式匹配的文件,否则运行./yourscript foo*不会通过;相反,它变成了类似,每个参数都在命令行的不同位置。foo*$1./yourscript foo01 foo02 foo03

运行./yourscript "foo*"函数作为一种变通方法的原因是脚本内未加引号的扩展允许稍后扩展 glob。但是,这是不好的做法:glob 扩展与字符串拆分同时发生(这意味着依赖此行为会消除您传递包含在IFS.也可以解释为 globs(如果您有一个名为[1]的文件和一个名为 的文件1,则传递[1]将始终替换为1)。


惯用语

构建它的惯用方法是去掉shift第一个参数,然后迭代后续参数,如下所示:

#!/bin/bash
out_base=$1; shift

shopt -s nullglob                 # avoid generating an error if a directory has no .status

for dir; do                       # iterate over directories passed in $2, $3, etc
  for file in "$dir"/*.status; do # iterate over files ending in .status within those
      grep -e "string" "$file"    # match a single file
  done
done >"${out_base}.extension"
Run Code Online (Sandbox Code Playgroud)

如果您.status在单个目录中有许多文件,则可以通过使用尽可能多的参数find进行调用来提高所有这些效率grep,而不是grep在每个文件的基础上单独调用:

#!/bin/bash
out_base=$1; shift

find "$@" -maxdepth 1 -type f -name '*.status' \
  -exec grep -h -- /dev/null '{}' + \
  >"${out_base}.extension"
Run Code Online (Sandbox Code Playgroud)

上面的两个脚本都期望传递的 globs不会在调用 shell 上被引用。因此,用法的形式如下:

# being unquoted, this expands the glob into a series of separate arguments
your_script descriptor dir_*_map
Run Code Online (Sandbox Code Playgroud)

这比将 glob 传递给您的脚本(然后需要扩展它们以检索要使用的实际文件)要好得多;它适用于包含空格的文件名(另一种做法没有),以及名称本身就是 glob 表达式的文件。


其他一些注意事项:

  • 总是在扩展周围加上双引号!不这样做会导致应用字符串拆分和全局扩展(按此顺序)的附加步骤。如果您想要通配符,如 的情况"$dir"/*.status,则在通配符表达式开始之前结束引号。
  • for dir; do完全等同于for dir in "$@"; do,它迭代参数。不要错误地使用for dir in $*; dofor dir in $@; do代替!这些后面的调用将列表的每个元素与的第一个字符IFS(默认情况下,按顺序包含空格、制表符和换行符)结合起来,然后在其中IFS找到的任何字符上拆分结果字符串,然后展开每个组成部分结果列表作为一个glob。
  • /dev/null作为参数传递togrep是一种安全措施:它确保您在单参数和多参数情况之间没有不同的行为(例如,grep默认仅在传递多个参数时才在输出中打印文件名),并确保grep如果根本没有传递额外的文件名(这find不会在这里做,但xargs可以),你不能挂起试图从标准输入读取。
  • 为您自己的变量使用小写名称(而不是系统和外壳提供的变量,它们具有全大写的名称)符合 POSIX 指定的约定;请参阅POSIX 规范关于环境变量的第四段,记住环境变量和 shell 变量共享一个命名空间。