
我有一個根資料夾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