我有一个根文件夹Products
,然后里面有一堆子文件夹。到目前为止,每个子文件夹都有一堆文件。为了简单起见,我想出了子文件夹名称为folder{number}
和文件名称为,files{number}.json
但一般来说它们有不同的名称。
一般来说,根文件夹内有 20 个不同的子文件夹,每个子文件夹最多包含大约 30 个文件。
(图1)
Products
├── folder1
│ ├── files1.json
│ ├── files2.json
│ └── files3.json
├── folder2
│ ├── files4.json
│ ├── files5.json
│ └── files6.json
└── folder3
├── files10.json
├── files7.json
├── files8.json
└── files9.json
tar.gz
现在我通过运行以下命令将所有这些压缩到一个文件中 -
tar cvzf ./products.tgz Products
问题:-
我得到了如下所示的新设计,其中Products
根文件夹内的每个子文件夹都包含三个环境文件夹 - dev
、stage
和prod
。
(图2)
Products
├── folder1
│ ├── dev
│ │ └── files1.json
│ ├── files1.json
│ ├── files2.json
│ ├── files3.json
│ ├── prod
│ │ └── files1.json
│ └── stage
│ └── files1.json
├── folder2
│ ├── dev
│ │ └── files5.json
│ ├── files4.json
│ ├── files5.json
│ ├── files6.json
│ ├── prod
│ │ └── files5.json
│ └── stage
│ └── files5.json
└── folder3
├── files10.json
├── files7.json
├── files8.json
└── files9.json
例如 - 在folder1
子文件夹内还有另外三个子文件夹 和dev
,stage
而其他子文件夹和prod
则完全相同。每个,以及子文件夹内的子文件夹都将包含被覆盖的文件。folder2
folder3
dev
stage
prod
folder{number}
我现在需要生成三个不同的tar.gz
文件 - 每个文件一个dev
,stage
并且prod
来自上述结构。
- 无论我里面有什么文件
dev
,stage
如果prod
它们的子文件夹文件也存在于它们的子文件夹(folder1、folder2 或folder3)中,它们都会覆盖它们。 - 因此,如果
files1.json
存在于folder1
子文件夹中,并且相同的文件也存在于任何一个中dev
,stage
那么prod
在打包时,我需要使用其环境文件夹中存在的任何内容并覆盖其子文件夹文件,否则只需使用其子文件夹中存在的任何内容文件夹。
最后,我将有 3 个不同的结构,如下所示 - 一个用于dev
,一个用于,stage
另一个用于prod
文件夹 1 (或 2 和 3)将相应地拥有我在其环境中作为第一选择的文件,因为它们被覆盖,而其他文件是没有被覆盖。
(图3)
Products
├── folder1
│ ├── files1.json
│ ├── files2.json
│ └── files3.json
├── folder2
│ ├── files4.json
│ ├── files5.json
│ └── files6.json
└── folder3
├── files10.json
├── files7.json
├── files8.json
└── files9.json
我需要生成和products-dev.gz
,其中将包含类似但特定于每个环境的数据。唯一的区别是每个子文件夹文件夹 1(2 或 3)将具有从其特定环境文件夹中作为首选覆盖的文件,其余文件将仅从其子文件夹中使用。products-stage.gz
products-prod.gz
figure 2
figure 3
这可以通过一些linux命令来完成吗?我唯一的困惑是如何覆盖特定子文件夹内的特定环境文件,然后tar.gz
在其中生成 3 个不同的文件。
更新:
还要考虑以下情况:
Products
├── folder1
│ ├── dev
│ │ ├── files1.json
│ │ └── files5.json
│ ├── files1.json
│ ├── files2.json
│ ├── files3.json
│ ├── prod
│ │ ├── files10.json
│ │ └── files1.json
│ └── stage
│ └── files1.json
├── folder2
│ ├── dev
│ ├── prod
│ └── stage
└── folder3
├── dev
├── prod
└── stage
正如您所看到的folder2
,并且folder3
具有环境覆盖文件夹,但它们没有任何文件,因此在这种情况下,我想在每个环境特定文件中生成空folder2
文件。folder3
tar.gz
答案1
可以有很多方法,尽管所有方法都需要某种复杂性才能处理覆盖情况。
作为一行,虽然有点长,但您可以在一次迭代中这样做,即一个“环境”目录:
(r=Products; e=stage; (find -- "$r" -regextype posix-extended -maxdepth 2 \( -regex '^[^/]+(/[^/]+)?' -o ! -type d \) -print0; find -- "$r" -mindepth 1 -path "$r/*/$e/*" -print0) | tar --null --no-recursion -czf "$r-$e.tgz" -T- --transform=s'%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%')
分解以更好地观察它:
(
r=Products; e=stage
(
find -- "$r" -regextype posix-extended -maxdepth 2 \( -regex '^[^/]+(/[^/]+)?' -o ! -type d \) -print0
find -- "$r" -mindepth 1 -path "$r/*/$e/*" -print0
) \
| tar --null --no-recursion -czf "$r-$e.tgz" -T- \
--transform=s'%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%'
)
注意事项:
- 它显示了 GNU 工具的语法。对于 BSD,
find
您必须替换-regextype posix-extended
为 just-E
,对于 BSD,tar
您必须替换--no-recursion
为 just-n
as well--transform=s
(<- 注意最后的s
)为 just-s
- 为了简化演示,片段假设从包含的目录运行
Products
,并使用自定义$e
变量作为要存档的“环境”目录的名称,而$r
只是一个短命名的帮助程序变量来包含Products
名称 - 它被括在括号内,使其成为一个子 shell,以免污染您的 shell,如果
$r
您$e
从命令行运行它 - 它不复制也不链接/引用原始文件,它处理任何有效的文件名,它没有内存限制,并且可以处理任意数量的名称;唯一的假设是关于目录层次结构的前两级,因为任何直接位于第一级之下的目录都被视为“环境”目录,因此被忽略(除了中指示的目录
$e
)
您只需将该片段包含在for e in dev prod stage; do ...; done
shell 循环中即可。 (可能去掉最外面的括号并包围整个for
循环)。
好处是它相当短并且相对简单。
缺点是它总是存档全部这被覆盖文件(即基本文件),技巧在于双find
命令首先提供tar
要覆盖的文件,因此在提取过程中它们将被覆盖文件(即“环境”特定文件)覆盖。这会导致更大的存档在创建和提取过程中花费更多时间,并且可能是不可取的,具体取决于这种“开销”是否可以忽略不计。
散文中描述的管道是:
- (除了最外面的括号和辅助变量)
- 第一个
find
命令仅生成非特定文件(以及根据您的更新的引导目录)的列表,而第二个命令find
仅生成所有特定于环境的文件的列表 - 这两个命令本身位于括号内,以便它们的输出按顺序
find
输入管道tar
tar
读取这样的管道以获得文件的名称,并将这些文件放入存档中,同时--transform
通过从每个文件的路径名中删除“环境”组件(如果存在)来命名它们的名称- 这两个
find
命令是分开的,而不是只有一个,并且它们一个接一个地运行,以便在特定于环境的文件之前生成(用于tar
使用)非特定文件,这启用了我之前描述的技巧
为了避免包括在内的开销总是全部我们需要额外的复杂性才能真正清除被覆盖的文件。一种方法可能如下所示:
# still a pipeline, but this time I won't even pretend it to be a one-liner
(
r=Products; e=stage; LC_ALL=C
find -- "$r" -regextype posix-extended \( -path "$r/*/$e/*" -o \( -regex '^([^/]+/){2}[^/]+' ! -type d \) -o -regex '^[^/]+(/[^/]+)?' \) -print0 \
| sed -zE '\%^(([^/]+/){2})([^/]+/)%s%%0/\3\1%;t;s%^%1//%' \
| sort -zt/ -k 3 -k 1,1n \
| sort -zut/ -k 3 \
| sed -zE 's%^[01]/(([^/]+/)|/)(([^/]+/?){2})%\3\2%' \
| tar --null --no-recursion -czf "$r-$e.tgz" -T- \
--transform=s'%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%'
)
有几点需要注意:
- 我们之前所说的关于 GNU 和 BSD 语法的所有内容
find
也tar
适用于这里 - 与之前的解决方案一样,除了对目录层次结构的前两级的假设之外,它没有任何约束
- 我
sed
在这里使用 GNU 来处理空分隔的 I/O(选项-z
),但是您可以轻松地将这两个sed
命令替换为while read ...
shell 循环(需要 Bash 版本 3 或更高版本)或您有信心的其他语言唯一的建议是您使用的工具能够处理空分隔的 I/O(例如 GNUgawk
可以做到这一点);请参阅下面使用 Bash 循环的替换 - 我在这里使用一个单一的
find
,因为我不依赖于任何隐含的行为tar
- 命令
sed
操作名称列表,为sort
命令铺平道路 - 具体来说,第一个
sed
将“环境”名称移动到路径的开头,并在其前面添加一个辅助0
编号,只是为了使其在非环境文件之前排序,因为我在后者前面添加了前导前缀,1
目的是排序 - 这种准备规范了命令“眼睛”中的名称列表
sort
,使所有名称不带“环境”名称,并且所有名称在开头都具有相同数量的斜杠分隔字段,这对于sort
的键定义很重要 - 第一个
sort
应用首先基于文件名进行排序,从而将相同的名称彼此相邻,然后按命令先前标记的数字值0
或,从而保证任何“环境”特定文件(如果存在)都会出现在其非特定对应物之前1
sed
- 文件名上的第二个
sort
合并(选项-u
)仅留下第一个重复名称,由于之前的重新排序,该名称始终是“环境”特定文件(如果存在) - 最后,第二个
sed
撤销第一个所做的事情,从而重塑文件名以tar
进行存档
如果您有兴趣探索如此长的管道的中间部分,请记住它们都与无- 分隔名称,因此在屏幕上显示效果不佳。您可以将任何一个中间输出(即至少去掉tar
)传递给礼貌者tr '\0' '\n'
以显示人性化的输出,只需记住带有换行符的文件名将在屏幕上跨越两行。
可以进行一些改进,当然可以通过使其成为完全参数化的函数/脚本,或者例如通过自动检测“环境”目录的任何任意名称,如下所示:
重要的:请注意注释,因为交互式 shell 可能无法很好地接受它们
(
export r=Products LC_ALL=C
cd -- "$r/.." || exit
# make arguments out of all directories lying at the second level of the hierarchy
set -- "$r"/*/*/
# then expand all such paths found, take their basenames only, uniquify them, and pass them along xargs down to a Bash pipeline the same as above
printf %s\\0 "${@#*/*/}" \
| sort -zu \
| xargs -0I{} sh -c '
e="${1%/}"
echo --- "$e" ---
find -- "$r" -regextype posix-extended \( -path "$r/*/$e/*" -o \( -regex '\''^([^/]+/){2}[^/]+'\'' ! -type d \) -o -regex '\''^[^/]+(/[^/]+)?'\'' \) -print0 \
| sed -zE '\''\%^(([^/]+/){2})([^/]+/)%s%%0/\3\1%;t;s%^%1//%'\'' \
| sort -zt/ -k 3 -k 1,1n \
| sort -zut/ -k 3 \
| sed -zE '\''s%^[01]/(([^/]+/)|/)(([^/]+/?){2})%\3\2%'\'' \
| tar --null --no-recursion -czf "$r-$e.tgz" -T- \
--transform=s'\''%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%'\''
' packetizer {}
)
sed
使用 Bash 循环替换第一个命令的示例:
(IFS=/; while read -ra parts -d $'\0'; do
if [ "${#parts[@]}" -gt 3 ]; then
env="${parts[2]}"; unset parts[2]
printf 0/%s/%s\\0 "$env" "${parts[*]}"
else
printf 1//%s\\0 "${parts[*]}"
fi
done)
对于第二个sed
命令:
(IFS=/; while read -ra parts -d $'\0'; do
printf %s "${parts[*]:2:2}" "/${parts[1]:+${parts[1]}/}" "${parts[*]:4}"
printf \\0
done)
两个片段都需要周围的括号,以便直接替换sed
上面管道中各自的命令,当然sh -c
后面的部分xargs
需要转换为bash -c
.
答案2
通用解决方案
- 制作目录树的副本。硬链接文件以节省空间。
- 修改副本。 (如果是硬链接,您需要知道可以安全地做什么。请参见下文。)
- 将副本存档。
- 删除副本。
- 如果需要,重复(以不同方式修改)。
例子
限制:
- 此示例使用非 POSIX 选项(在 Debian 10 上测试),
- 它对目录树做了一些假设,
- 如果文件太多,它可能会失败。
将其视为概念证明,根据您的需求进行调整。
复印
cd
到 的父目录Products
。该目录Products
及其中的所有内容都应属于单个文件系统。创建一个临时目录并Products
在其中重新创建:mkdir -p tmp cp -la Products/ tmp/
修改副本
两个目录树中的文件是硬链接的。如果你修改他们的内容那么你将改变原始数据。修改目录保存的信息的操作是安全的,如果在其他树中执行,它们不会更改原始数据。这些都是:
- 删除文件,
- 重命名文件,
- 移动文件(这包括使用 移动一个文件到另一个文件上
mv
), - 创建完全独立的文件。
在您的情况下,对于在正确深度命名的每个目录,
dev
将其内容向上移动一级:cd tmp/Products dname=dev find . -mindepth 2 -maxdepth 2 -type d -name "$dname" -exec sh -c 'cd "$1" && mv -f -- * ../' sh {} \;
笔记:
mv -- * ../
很容易出现argument list too long
,- 默认情况下
*
不匹配点文件。
然后删除目录:
find . -mindepth 2 -maxdepth 2 -type d -exec rm -rf {} +
请注意,这会删除现在为空
dev
且不需要的prod
,stage
;和此深度的任何其他目录。存档副本
# still in tmp/Products because of the previous step cd .. tar cvzf "products-$dname.tgz" Products
删除副本
# now in tmp because of the previous step rm -rf Products
重复
返回到正确的目录并重新开始,这次使用
dname=stage
;等等。
示例脚本(快速但肮脏)
#!/bin/bash
dir=Products
[ -d "$dir" ] || exit 1
mkdir -p tmp
for dname in dev prod stage; do
(
cp -la "$dir" tmp/
cd "tmp/$dir"
[ "$?" -eq 0 ] || exit 1
find . -mindepth 2 -maxdepth 2 -type d -name "$dname" -exec sh -c 'cd "$1" && mv -f -- * ../' sh {} \;
find . -mindepth 2 -maxdepth 2 -type d -exec rm -rf {} +
cd ..
[ "$?" -eq 0 ] || exit 1
tar cvzf "${dir,,}-$dname.tgz" "$dir"
rm -rf "$dir" || exit 1
) || exit "$?"
done
答案3
我使这一点更加通用,并在不实际更改源目录的情况下处理不平凡的文件名
Products
作为参数给出。关键字dev prod stage
在脚本内硬编码(但可以轻松更改)
注意:这是 GNU 特定的--transform
和-print0
-z
扩展
运行脚本
./script Products
#!/bin/sh
# environment
subdirs="dev prod stage"
# script requires arguments
[ -n "$1" ] || exit 1
# remove trailing /
while [ ${i:-0} -le $# ]
do
i=$((i+1))
dir="$1"
while [ "${dir#"${dir%?}"}" = "/" ]
do
dir="${dir%/}"
done
set -- "$@" "$dir"
shift
done
# search string
for sub in $subdirs
do
[ -n "$search" ] && search="$search -o -name $sub" || search="( -name $sub"
done
search="$search )"
# GNU specific zero terminated handling for non-trivial directory names
excludes="$excludes $(find -L "$@" -type d $search -print0 | sed -z 's,[^/]*/,*/,g' | sort -z | uniq -z | xargs -0 printf '--exclude=%s\n')"
# for each argument
for dir in "$@"
do
# for each environment
[ -e "$dir" ] || continue
for sub in $subdirs
do
# exclude other subdirs
exclude=$(echo "$excludes" | grep -v "$sub")
# # exclude files that exist in subdir (at least stable against newlines and spaces in file names)
# include=$(echo "$excludes" | grep "$sub" | cut -d= -f2)
# [ -n "$include" ] && files=$(find $include -mindepth 1 -maxdepth 1 -print0 | tr '\n[[:space:]]' '?' | sed -z "s,/$sub/,/," | xargs -0 printf '--exclude=%s\n')
# exclude="$exclude $files"
# create tarball archive
archive="${dir##*/}-${sub}.tgz"
[ -f "$archive" ] && echo "WARNING: '$archive' is overwritten"
tar --transform "s,/$sub$,," --transform "s,/$sub/,/," $exclude -czhf "$archive" "$dir"
done
done
您可能会注意到存档内有重复项。tar
将递归地下降目录,恢复时更深的文件将覆盖父目录中的文件
但是,这需要针对一致行为进行更多测试(对此不确定)。正确的方法是排除files1.json
+files5.json
不幸的-X
是不适用于--null
如果您不信任该行为或不希望存档中出现重复文件,您可以为简单文件名添加一些排除项。取消注释上面的代码tar
。文件名中允许使用换行符和空格,但将在排除模式中使用通配符排除?
,这理论上可以排除比预期更多的文件(如果存在与该模式匹配的类似文件)
您可以在echo
前面放置一个tar
,您将看到脚本生成以下命令
tar --transform 's,/dev$,,' --transform 's,/dev/,/,' --exclude=*/*/prod --exclude=*/*/stage -czhf Products-dev.tgz Products
tar --transform 's,/prod$,,' --transform 's,/prod/,/,' --exclude=*/*/dev --exclude=*/*/stage -czhf Products-prod.tgz Products
tar --transform 's,/stage$,,' --transform 's,/stage/,/,' --exclude=*/*/dev --exclude=*/*/prod -czhf Products-stage.tgz Products