为何Bash脚本中read读入的通配符会被展开为目录列表?
问题
我编写了一个处理数据文件的脚本,初始代码如下:
proc(){ echo $1 ; } while read -r line ; do proc "$line"; done <data.csv | less
运行后意外发现输出中混入了当前目录的文件列表,这对应数据文件里包含星号(*)的行。我想搞清楚这个展开是在命令行解释器、read命令还是文件流中发生的?
我目前用以下方法处理,但想知道是否有直接阻止展开的办法:
cat data.csv | tr '*' '_' | while read -r line ; do proc "$line"; done | less
另外,能不能用重定向而非管道来过滤输入?还有哪些特殊字符(如[、]、?、-这类)需要替换?
补充示例数据文件(data.csv):
gfh fg hfgh fgh fg hgh 7 4 674 547767 56 7 56756 ghdghh gh fg h fgh fg hf gh 8 678 678 * 67 867 8 678 gfh fg hfgh fgh fg hgh 7 4 674 547767 56 7 56756 ghdghh gh fg h fgh fg hf gh 8 678 678 67 867 8 678
示例输出:
gfh fg hfgh fgh fg hgh 7 4 674 547767 56 7 56756 ghdghh gh fg h fgh fg hf gh 8 678 678 data.csv file1.txt file2.txt file3.txt 67 867 8 678 gfh fg hfgh fgh fg hgh 7 4 674 547767 56 7 56756 ghdghh gh fg h fgh fg hf gh 8 678 678 67 867 8 678
更新1:只有当星号两侧有空格时才会被当作通配符处理,说明存在分词操作。
更新2:直接调用echo "$line"时不会发生展开:
while read -r line ; do echo "$line"; done <data.csv
解答
一、展开现象的原因
这个通配符展开是**命令行解释器(shell)**触发的,和read命令、文件流无关,具体流程如下:
read -r line会完整读取包含*的行,-r参数保证反斜杠不会被转义,此时$line中的*是原始字符。- 调用
proc "$line"时,$line被双引号包裹,会作为一个整体传给proc的参数$1。 - 问题出在
proc函数内的echo $1——这里$1没有加双引号,shell会先对$1做分词(按空格拆分内容),再对拆分后的每个部分执行路径名扩展(通配符展开),当某个部分是单独的*时,就会被替换成当前目录的所有文件列表。
你更新中提到直接echo "$line"不会展开,就是因为双引号阻止了shell的分词和通配符展开操作。
二、直接阻止展开的方法
最直接的修复方式是给proc函数里的echo参数加上双引号,确保变量被当作整体处理:
proc(){ echo "$1" ; } while read -r line ; do proc "$line"; done <data.csv | less
只要在引用变量时始终用双引号包裹,就能避免意外的分词和通配符展开,原始的*会被完整保留。
三、用重定向替代管道过滤输入
可以借助bash的进程替换特性,用重定向替代管道实现过滤,写法如下:
proc(){ echo "$1" ; } while read -r line ; do proc "$line"; done < <(tr '*' '_' <data.csv) | less
<(command)会创建临时文件描述符,将tr的输出作为while循环的输入,本质是重定向逻辑,还能避免管道带来的子shell变量作用域问题。
四、需要注意的特殊字符
除了*,shell中会触发路径名扩展的特殊字符还有:
?:匹配任意单个字符[]:匹配括号内的任意一个字符(如[abc]匹配a、b或c)[!]/[^]:匹配不在括号内的任意一个字符(如[!0-9]匹配非数字字符)
但不需要逐个替换这些字符——只要在引用变量时始终用双引号包裹,就能从根源上避免展开问题,这比逐个替换特殊字符更稳妥。
内容的提问来源于stack exchange,提问作者bitrat
相关产品推荐
相关产品推荐

