Bash 中双引号和不双引号数组有什么区别?

Bash 中双引号和不双引号数组有什么区别?

在跟踪 shellscript 中的错误时,我发现此代码片段中存在以下行为:

declare -a filelist
readarray filelist < <(ls -A)
readonly filelist
for file in "${filelist[@]}"; do
  sha256sum ${filelist[$file]} | head -c 64
done

当数组filelist不包含在双引号中时,命令成功。我一直在使用 ShellCheck 来尝试改进我的编码,它建议-

双引号可防止通配符和分词。

在这种情况下,我并不担心分词,但在许多其他情况下我担心,所以我试图保持我的代码一致。但是,当我双引号该数组时,该命令失败。将代码简化为单个元素可得出以下结果:

bash-5.0# sha256sum ${filelist[0]} | head -c 64
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

bash-5.0# sha256sum "${filelist[0]}" | head -c 64
sha256sum: can't open 'file1
': No such file or directory

显然,我可以......不使用双引号,因为在这种情况下,分词不是问题。但我想发帖,因为将来可能会这样。

我的问题有两个部分:

  1. 除了上述双引号数组之外,是否有“最佳实践”方法来防止分词?
  2. 数组中的单引号来自哪里? 编辑:没有单引号。单引号是显示无法打开的文件名的错误。

另外,出于好奇,为什么echo ${filelist[0]}不包含额外的换行符却echo "${filelist[0]}"包含呢?

答案1

引用数组扩展绝对没有问题。

当然,只要你知道并接受后果,不引用它也没有问题。任何未引用的扩展都会受到拆分和通配符的影响。并且,在您的代码中,${filelist[…]}需要进行 IFS 字符删除(如果字符串包含任何<space><tab>或 ,则进行拆分<newline>)。

这就是未加引号的扩展所做的事情,删除尾随的<newline>.

什么创造这个问题是您在使用时readarray没有从每个数组元素中删除尾随分隔符。这样做会保留反映在错误消息上的
尾随。<newline>

您可以使用的是:

readarray -t filelist < <(ls -A)

-t选项将删除每个文件名的所有尾随换行符。

-t 从读取的每行中删除尾部分隔符(默认换行符)。


但是您的代码还有一些其他问题。

  • 无需声明或清空数组filelist。默认情况下由 readarray 完成。在其他一些情况下也需要这样做。

  • 不需要解析 的输出ls,事实上,这是一个坏主意。获取数组中的文件列表的最简单方法很简单:

    filelist=( ./* )
    

    而且,为了让它变得更好,最好避免使用目录:

    for file in ./*; do
      [[ -f $file ]] && filelist+=( "$file" )
    done
    
  • 在循环中,$file应使用 var 的值:

    for file in "${filelist[@]}"; do
      sha256sum "$file" | head -c 64
    done
    

    除非你使用for file in "${!filelist[@]}"; dowhich将列出数组的。

  • 整个列表只需处理即可调用 sha256sum:

    sha256sum "${filelist[@]}" | cut -c -64
    

改进后的脚本是:

filelist=()              # declare filelist as an array and empty it.
for file in ./*; do
    if [[ -f $file ]]; then
        filelist+=( "$file" )
    fi
done
declare -r filelist      # declare filelist as readonly.
sha256sum "${filelist[@]}" | cut -c -64

答案2

在这种情况下我不担心分词

嗯,事实上,你是依靠它从数组条目中删除尾随换行符!

巴什的readarray( mapfile)默认情况下保留分隔符。手册页或命令行帮助似乎没有明确说明,但有一个选项消除分隔符,因此默认情况下它不会被删除:

-t     Remove a trailing delim (default newline) from each line read.

所以,数组中的实际字符串是。file1[newline]

如果没有引号,分词会删除尾随空格,从而修复换行符。但是,如果文件名中含有空格,分词就会像往常一样把它们弄乱。双引号数组可以防止这种情况。回答你的第一个问题,最佳实践是双引号,这里我们只是有一个不需要的额外换行符。

(双引号数组或是$@一种稍微令人困惑的例外情况,其中双引号字符串会产生多个单词,每个单词对应一个数组元素。)

您也可以${filelist[$file]}sha256sum命令行中使用。这是行不通的,file已经包含从数组接收的值,而不是索引。

作为最小的修改,这可能会起作用:

declare -a filelist
readarray -t filelist < <(ls -A)
readonly filelist
for file in "${filelist[@]}"; do
    sha256sum "$file" | head -c 64
done

(我认为明确的declare实际上也没有必要。)


上述问题与此无关ls上述问题与本身如果您将文件名存储在文件中,每行一个,并使用readarray/mapfile来读取它们而不使用该-t选项,您也会遇到同样的问题。 (或者,如果您阅读了 的输出find,但在这种情况下,您也许可以使用find -exec它。)

当然,这个无用的使用ls和 的某些版本ls可能会破坏输出中的文件名。 (我不认为 GNU ls 在输出到管道时会这样做。)

在 Bash 中,您可以用 glob 填充数组:

shopt -s dotglob
filelist=(*)
for file in *; do ...

或者只在 glob 上运行循环而不存储到数组:

shopt -s dotglob
for file in *; do ...

请注意,您确实需要shopt -s dotglob要得到*点文件,这取决于 shell。

答案3

基于您的代码片段的部分问题可能是您正在解析ls.这是危险的并且充满了无数问题,最好避免。

而不是

declare -a filelist
readarray filelist < <(ls -A)
readonly filelist
for file in "${filelist[@]}"; do

它更简单(也更安全!):

for file in *; do

在这种情况下:

for file in *; do
  sha256sum "${file}" | head -c 64
done

readarray当您调用它时,它也有助于保留传递给它的文字数据,包括换行符。因此,当您回显引用的值时,换行符将被保留。当您不引用它时,shell 将其作为令牌间空白来忽略。这也是sha256sum失败的原因。如果您有一个名为 的文件fooreadarray则传递一个值foo\n,该值与文件不对应。取消引用此“修复”问题,因为意外地丢弃了部分变量值。

相关内容