Bash 创建 JSON 文件清单

Bash to create a JSON File Manifest

我有一个 bash 脚本来输出带有 MD5 哈希值的文件清单作为 JSON,如下所示:

{
 "files": [
    {
      "md5": "f30ae4b2e0d2551b5962995426be0c3a",
      "path": "assets/asset_1.png"
    },
    {
      "md5": "ca8492fdc3547af31afeeb8656619ef0",
      "path": "assets/asset_2.png"
    },
  ]
}

它将return一个除.gdz之外的所有文件的列表。

我使用的命令是:

echo "{\"files\": [$(find . -type f -print | grep -v \.gdz$ | xargs md5sum | sed 's/\.\///' | xargs printf "{\"md5\": \"%s\", \"name\": \"%s\"}," | sed 's/,$//')]}" > files.json

但是,当我 运行 在生产中使用它时,它有时会切换 MD5 散列和文件路径。我不知道这是为什么,有人知道吗?

您可以考虑使用 bash 和 GNU 工具 findmd5sum 尝试这个。该脚本使用以 NUL 结尾的路径名并转义相关字符。即使文件名包含换行符,它也应该工作。

#!/bin/bash

comma=
printf '{\n  "files": [\n'
while IFS= read -d '' -r line; do
    md5=${line:0:32}
    path=${line:34}
    path=${path//'\'/'\'}
    path=${path//'"'/'\"'}
    path=${path//$'\b'/'\b'}
    path=${path//$'\f'/'\f'}
    path=${path//$'\n'/'\n'}
    path=${path//$'\r'/'\r'}
    path=${path//$'\t'/'\t'}
    printf '%s%4s{\n%6s"md5": "%s",\n%6s"path": "%s"\n%4s}' \
        "$comma" '' '' "${md5}" '' "${path}" ''
    comma=$',\n'
done < <(find . -type f ! -name '*.gdz' -exec md5sum -z {} +)
printf '\n  ]\n}\n'

或者,也使用 GNU sed(这个版本可能更快):

#!/bin/bash

printf '{\n  "files": ['
find . -type f ! -name '*.gdz' -exec md5sum -z {} + |
sed -Ez '
s/\/\\/g
s/"/\"/g
s/\x08/\b/g
s/\f/\f/g
s/\n/\n/g
s/\r/\r/g
s/\t/\t/g
s/(.{32})..(.*)/\
    {\
      "md5": "",\
      "path": ""\
    }/
$!s/$/,/' | tr -d '[=11=]'

printf '\n  ]\n}\n'

我相信这两个脚本都很健壮,假设有最新的 GNU 工具。

在 shell 中稳健地执行此操作有点痛苦;你必须担心文件名中的空格(这会破坏你当前的代码),正确编码和转义你的 JSON 字符串(如果你有一个文件名中包含引号怎么办?)等

一个快速的 perl 脚本,执行相同的操作,将要扫描的目录作为命令行参数传递:

#!/usr/bin/env perl
use warnings;
use strict;
use File::Find;
use Digest::MD5;
use JSON::PP; # Or JSON::XS if installed

my @hashes;

find(\&wanted, @ARGV);
print JSON::PP->new->ascii->encode({files => \@hashes});

sub wanted {
    if (-f $_ && $_ !~ /\.gdz$/) {
        my $name = $File::Find::name;
        $name =~ s!^\./!!;
        open my $f, "<:raw", $_ or
            die "Couldn't open $name: $!\n";
        push @hashes, { path => $name,
                        md5 => Digest::MD5->new()->addfile($f)->hexdigest
        };
        close $f;
    }
}

你可以 运行 md5sum 所有匹配的文件,然后用 jq 做剩下的:

find . -type f -not -name '*.gdz' -exec md5sum -z {} + \
    | jq --slurp --raw-input '
        {
            files: split("\u0000")
                | map(split(" "))
                | map([
                    .[0],
                    (.[2:] | join(" "))
                ])
                | map({md5: .[0], path: .[1]})
        }'

find 命令的输出是 运行ning md5sum 在所有匹配文件上一次的输出,输出记录由空字节分隔。

然后 jq 执行以下操作(并且几乎肯定可以优化):

  • --slurp--raw-input 在任何处理之前读取整个输入
  • 在最外层,我们构建一个以files为键的对象
  • split("\u0000") 从空字节分隔的输入记录创建一个数组
  • map(split(" ")) 将每个数组元素转换为按空格拆分的数组
  • map([ .[0], (.[2:] | join(" ")) ]) – 为了允许文件名中有空格,我们为每条记录创建一个数组,其中第一个元素是 md5 哈希值,第二个元素是其余元素的串联,即文件名; [2:] 因为我们要跳过两个空格
  • map({md5: .[0], path: .[1]}) 将每个二元素数组转换为具有所需键的对象

尝试使用并非专为它设计的工具来创建 JSON 是一项非常容易出错的任务。请使用专用工具正确创建您想要的JSON。我强烈推荐 :

xidel -se '
  {
    "files":array{
      for $x in file:list(.,true())[not(ends-with(.,"/")) and not(ends-with(.,"gdz"))]
      return {
        "md5":substring(system(x"md5sum {$x}"),1,32),
        "path":$x
      }
    }
  }
'
  • file:list(.,true()) returns 当前目录中的所有文件和目录(并且将可选参数 $recursive 设置为 true() 所有后代目录也包括在内)。
  • [not(ends-with(.,"/")) and not(ends-with(.,"gdz"))] 通过删除目录和“gdz”文件过滤 file:list() 的输出。
  • system(x"md5sum {$x}") returns md5sum 其标准输出结果为字符串(并且 substring(..,1,32) 显然 returns 前 32 个字符)。
    x"..{..}.." 是一个扩展字符串,其中 x"There are {1+2+3} elements" 例如计算为“有 6 个元素”。