Wie erstelle ich mehrere Tar.gz-Dateien, indem ich für jede Umgebung bestimmte Dateien überschreibe?

Wie erstelle ich mehrere Tar.gz-Dateien, indem ich für jede Umgebung bestimmte Dateien überschreibe?

Ich habe einen Stammordner Productsund darin eine Reihe von Unterordnern. Jeder dieser Unterordner enthält derzeit eine Reihe von Dateien. Nur der Einfachheit halber habe ich mir die Namen der Unterordner folder{number}und der Dateien ausgedacht, files{number}.jsonaber im Allgemeinen haben sie unterschiedliche Namen.

Im Allgemeinen habe ich 20 verschiedene Unterordner im Stammordner und jeder Unterordner hat maximal etwa 30 Dateien.

(Abbildung 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

Jetzt komprimiere ich das alles in eine tar.gzDatei, indem ich den folgenden Befehl ausführe -

tar cvzf ./products.tgz Products

Frage:-

Ich habe ein neues Design, wie unten gezeigt, bei dem jeder Unterordner im ProductsStammordner drei Umgebungsordner enthält – dev, stageund prod.

(Figur 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

Beispiel: Innerhalb folder1eines Unterordners gibt es drei weitere Unterordner devund stagegenau proddasselbe gilt für die anderen Unterordner folder2und folder3. Jeder dieser Unterordner devund stagejeder prodUnterordner innerhalb folder{number}eines Unterordners enthält Dateien, die für sie überschrieben werden.

Ich muss tar.gzjetzt drei verschiedene Dateien generieren – eine für jede devund stageaus prodder obigen Struktur.

  • Welche Dateien ich auch immer darin habe dev, stagesie prodüberschreiben ihre Unterordnerdateien, wenn sie auch in deren Unterordner (Ordner1, Ordner2 oder Ordner3) vorhanden sind.
  • Wenn also im Unterordner files1.jsonvorhanden ist und dieselbe Datei auch in einem der Ordner vorhanden ist , muss ich beim Verpacken alles verwenden, was in deren Umgebungsordner vorhanden ist, und die Unterordnerdateien überschreiben, andernfalls verwende ich einfach das, was in deren Unterordnern vorhanden ist.folder1devstageprod

Am Ende werde ich drei verschiedene Strukturen wie diese haben – eine für dev, eine für stageund eine weitere für , prodwobei Ordner1 (oder 2 und 3) entsprechend dem, was ich in ihrer Umgebung als erste Präferenz habe, da sie überschrieben werden, und andere Dateien, die nicht überschrieben werden, Dateien enthalten werden.

(Figur 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

Und ich muss generieren products-dev.gz, products-stage.gzund products-prod.gzdaraus figure 2werden Daten wie diese figure 3, aber spezifisch für jede Umgebung, stammen. Der einzige Unterschied besteht darin, dass jeder Unterordner Ordner1 (2 oder 3) Dateien enthält, die für sie als erste Präferenz aus ihrem jeweiligen Umgebungsordner überschrieben werden, und der Rest wird nur aus seinem Unterordner verwendet.

Ist dies mit einigen Linux-Befehlen möglich? Ich bin mir nur nicht sicher, wie ich bestimmte Umgebungsdateien in einem bestimmten Unterordner überschreiben und dann drei verschiedene tar.gzDateien darin generieren kann.

Aktualisieren:

Bedenken Sie auch Fälle wie die folgenden:

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

Wie Sie sehen folder2, folder3gibt es Ordner, die die Umgebung überschreiben, aber sie enthalten keine Dateien. In diesem Fall möchte ich leere folder2und folder3auch in jeder Umgebung spezifische tar.gzDateien generieren.

Antwort1

Es gibt viele Möglichkeiten, doch alle erfordern eine gewisse Komplexität, um den Override-Fall behandeln zu können.

Als Einzeiler, wenn auch etwas lang, könnten Sie es für eine Iteration, also ein „Umgebungen“-Verzeichnis, wie folgt machen:

(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%')

zur besseren Übersicht aufgeschlüsselt:

(
    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%'
)

Zu beachten:

  1. es zeigt die Syntax der GNU-Tools. Für BSD findmüssen Sie -regextype posix-extendeddurch just ersetzen -Eund für BSD tarmüssen Sie --no-recursiondurch just ersetzen -n, ebenso wie --transform=s(<- beachten Sie das letzte s) durch just-s
  2. der Einfachheit halber wird davon ausgegangen, dass das Snippet aus dem Verzeichnis ausgeführt wird, das enthält Products, und verwendet die benutzerdefinierte $eVariable für den Namen des zu archivierenden Verzeichnisses „environments“, während $r nur eine Hilfsvariable mit Kurznamen ist, die den ProductsNamen enthält
  3. es ist in Klammern eingeschlossen, was es zu einer Subshell macht, nur um Ihre Shell nicht zu verunreinigen, $rund $ewenn Sie es von der Kommandozeile aus ausführen
  4. es kopiert nicht die Originaldateien und stellt auch keine Verknüpfungen/Referenzen zu ihnen her, es verarbeitet alle gültigen Dateinamen, hat keine Speicherbeschränkungen und kann mit einer beliebigen Anzahl von Namen umgehen; die einzige Annahme betrifft die ersten beiden Ebenen der Verzeichnishierarchie, da alle Verzeichnisse direkt unter der ersten Ebene als „Umgebungsverzeichnis“ betrachtet und somit ignoriert werden (mit Ausnahme des in angegebenen Verzeichnisses $e).

Sie können diesen Code-Schnipsel einfach in eine for e in dev prod stage; do ...; doneShell-Schleife einschließen und loslegen. (Evtl. die äußersten Klammern entfernen und stattdessen die gesamte forSchleife einschließen.)

Der Vorteil ist, dass es ziemlich kurz und letztendlich relativ einfach ist.

Der Nachteil ist, dass immer auch archiviert wirdalleDieüberschriebenDateien (also die Basisdateien), der Trick besteht lediglich darin, dass die doppelten findBefehle tarzuerst die zu überschreibenden Dateien einspeisen und diese daher während der Extraktion von den überschreibenden Dateien (also den „umgebungsspezifischen“) überschrieben werden. Dies führt dazu, dass ein größeres Archiv sowohl bei der Erstellung als auch bei der Extraktion mehr Zeit benötigt, und kann unerwünscht sein, je nachdem, ob dieser „Overhead“ vernachlässigbar ist oder nicht.

Die in Prosa beschriebene Pipeline lautet:

  1. (außer den äußersten Klammern und den Hilfsvariablen)
  2. Der erste findBefehl erzeugt nur die Liste der nichtspezifischen Dateien (und der führenden Verzeichnisse gemäß Ihrem Update), während der zweite findnur die Liste aller umgebungsspezifischen Dateien erzeugt
  3. die beiden findBefehle stehen in Klammern, so dass ihre Ausgänge nacheinander die Pipe tarversorgen
  4. tarliest diese Pipe, um die Namen der Dateien zu erhalten, und legt diese Dateien im Archiv ab, während --transformihre Namen gleichzeitig mit -ing versehen werden, indem die Komponente „environs“ (falls vorhanden) aus dem Pfadnamen jeder Datei entfernt wird
  5. die beiden findBefehle sind getrennt, anstatt nur einer zu sein, und sie werden nacheinander ausgeführt, so dass die nichtspezifischen Dateien (zum tarVerwenden) vor den umgebungsspezifischen Dateien erstellt werden, was den Trick ermöglicht, den ich zuvor beschrieben habe

Um den Mehraufwand beim Einbinden zu vermeidenimmer alleBei den Dateien benötigen wir zusätzliche Komplexität, um die überschriebenen Dateien wirklich zu löschen. Eine Möglichkeit könnte wie folgt aussehen:

# 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%'
)

Einige Dinge sind zu beachten:

  1. alles, was wir zuvor über GNU- und BSD-Syntaxen gesagt haben, gilt findauch tarhier
  2. wie die vorherige Lösung hat sie keinerlei Einschränkungen außer der Annahme über die ersten beiden Ebenen der Verzeichnishierarchie
  3. Ich verwende sedhier GNU, um mit durch Nullen getrennten E/A-Vorgängen umzugehen (Option -z), aber Sie können diese beiden sedBefehle problemlos ersetzen durch z. B. eine while read ...Shell-Schleife (Bash Version 3 oder höher wäre erforderlich) oder eine andere Sprache, mit der Sie sich sicher fühlen. Die einzige Empfehlung ist, dass das von Ihnen verwendete Tool in der Lage ist, mit durch Nullen getrennten E/A-Vorgängen umzugehen (z. B. können GNUs gawkdies); siehe unten für einen Ersatz durch Bash-Schleifen
  4. Ich verwende findhier nur eine einzige, da ich mich nicht auf implizites Verhalten vontar
  5. Die sedBefehle manipulieren die Liste der Namen und ebnen den Weg für die sortBefehle
  6. Insbesondere verschiebt die erste sedden Namen der „Umgebungen“ an den Anfang des Pfads und stellt ihm außerdem eine Hilfsnummer voran, 0damit er vor den Dateien sortiert wird, die keine Umgebungen sind, da ich diesen letzteren 1zum Zweck der Sortierung ein führendes voranstelle
  7. eine solche Vorbereitung normalisiert die Liste der Namen in den "Augen" der sortBefehle, so dass alle Namen ohne den Namen "Umgebungen" sind und alle die gleiche Anzahl von durch Schrägstriche getrennten Feldern am Anfang haben, was für sortdie Schlüsseldefinitionen von wichtig ist
  8. Die erste sortSortierung basiert zunächst auf den Dateinamen, wobei gleiche Namen nebeneinander angeordnet werden und dann nach dem numerischen Wert 0oder 1wie zuvor durch den Befehl markiert sed. Dadurch wird sichergestellt, dass jede „umgebungsspezifische“ Datei, sofern vorhanden, vor ihrem nichtspezifischen Gegenstück steht.
  9. Die zweite Option fügt die Dateinamen sortzusammen (Option ), so dass nur der erste der doppelten Namen übrig bleibt, der aufgrund der vorherigen Neuordnung immer eine „umgebungsspezifische“ Datei ist, wenn vorhanden-u
  10. Schließlich macht ein zweiter Schritt seddas rückgängig, was der erste getan hat, und formt so die Dateinamen für tardas Archiv neu.

Wenn Sie neugierig sind, die Zwischenstücke einer so langen Pipeline zu erkunden, denken Sie daran, dass sie alle mitNull-getrennte Namen und werden daher auf dem Bildschirm nicht gut angezeigt. Sie können jede der Zwischenausgaben (also zumindest das entfernen tar) an eine Hilfsquelle weiterleiten tr '\0' '\n', um eine benutzerfreundliche Ausgabe anzuzeigen. Denken Sie jedoch daran, dass Dateinamen mit Zeilenumbrüchen auf dem Bildschirm zwei Zeilen umfassen.

Es könnten einige Verbesserungen vorgenommen werden, sicherlich indem man es zu einer vollständig parametrisierten Funktion/einem vollständig parametrisierten Skript macht oder beispielsweise jeden beliebigen Namen für „Umgebungs“-Verzeichnisse automatisch erkennt, wie unten:

Wichtig: Achten Sie auf die Kommentare, da diese von einer interaktiven Shell möglicherweise nicht gut angenommen werden

(
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 {}
)

Beispielhafter Ersatz für den ersten sedBefehl durch eine Bash-Schleife:

(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)

Für den zweiten sedBefehl:

(IFS=/; while read -ra parts -d $'\0'; do
    printf %s "${parts[*]:2:2}" "/${parts[1]:+${parts[1]}/}" "${parts[*]:4}"
    printf \\0
done)

Beide Snippets erfordern die umgebenden Klammern, um als Drop-In-Ersatz für die jeweiligen sed Befehle in der obigen Pipeline zu dienen, und natürlich muss der sh -cTeil danach xargsin umgewandelt werden bash -c.

Antwort2

Allgemeine Lösung

  1. Erstellen Sie eine Kopie des Verzeichnisbaums. Verknüpfen Sie die Dateien mit einem Hardlink, um Platz zu sparen.
  2. Ändern Sie die Kopie. (Bei Hardlinks müssen Sie wissen, was Sie sicher tun können. Siehe unten.)
  3. Archivieren Sie die Kopie.
  4. Entfernen Sie die Kopie.
  5. Bei Bedarf wiederholen (mit unterschiedlichen Änderungen).

Beispiel

Einschränkungen:

  • Dieses Beispiel verwendet Nicht-POSIX-Optionen (getestet unter Debian 10),
  • es macht einige Annahmen über den Verzeichnisbaum,
  • es kann fehlschlagen, wenn zu viele Dateien vorhanden sind.

Betrachten Sie es als Proof of Concept und passen Sie es an Ihre Bedürfnisse an.

  1. Erstellen einer Kopie

    cdzum übergeordneten Verzeichnis von Products. Dieses Verzeichnis Productsund alles darin sollte zu einem einzigen Dateisystem gehören. Erstellen Sie ein temporäres Verzeichnis und erstellen Sie Productsdort Folgendes neu:

    mkdir -p tmp
    cp -la Products/ tmp/
    
  2. Ändern der Kopie

    Dateien in den beiden Verzeichnisbäumen sind fest verknüpft. Wenn Sie derenInhaltdann ändern Sie die Originaldaten. Operationen, die Informationen in Verzeichnissen ändern, sind sicher, sie ändern die Originaldaten nicht, wenn sie im anderen Baum ausgeführt werden. Diese sind:

    • Dateien entfernen,
    • Umbenennen von Dateien,
    • Verschieben von Dateien (hierzu gehört auch das Verschieben einer Datei über eine andere Datei mit mv),
    • Erstellen völlig unabhängiger Dateien.

    devVerschieben Sie in Ihrem Fall den Inhalt jedes Verzeichnisses mit der richtigen Tiefe um eine Ebene nach oben:

    cd tmp/Products
    dname=dev
    find . -mindepth 2 -maxdepth 2 -type d -name "$dname" -exec sh -c 'cd "$1" && mv -f -- * ../' sh {} \;
    

    Anmerkungen:

    • mv -- * ../Ist anfällig für argument list too long,
    • stimmt standardmäßig *nicht mit Dotfiles überein.

    Entfernen Sie dann Verzeichnisse:

    find . -mindepth 2 -maxdepth 2 -type d -exec rm -rf {} +
    

    Beachten Sie, dass hierdurch das jetzt leere devund nicht mehr benötigte prod, stage; entfernt wird.Undjedes andere Verzeichnis in dieser Tiefe.

  3. Archivieren der Kopie

    # still in tmp/Products because of the previous step
    cd ..
    tar cvzf "products-$dname.tgz" Products
    
  4. Entfernen der Kopie

    # now in tmp because of the previous step
    rm -rf Products
    
  5. Wiederholen

    Gehen Sie zurück in das richtige Verzeichnis und beginnen Sie erneut, diesmal mit dname=stage; und so weiter.


Beispielskript (schnell und schmutzig)

#!/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

Antwort3

Ich habe das etwas allgemeiner gemacht und arbeite an nicht-trivialen Dateinamen, ohne die Quellverzeichnisse tatsächlich zu ändern

Productswird als Argument angegeben. Schlüsselwörter dev prod stagesind im Skript fest codiert (können aber leicht geändert werden)

Hinweis: Dies ist GNU-spezifisch --transformund -print0 -zeine Erweiterung

Führen Sie das Skript aus
./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

Möglicherweise bemerken Sie Duplikate im Archiv. tarwird rekursiv Verzeichnisse absteigen, bei der Wiederherstellung werden die tieferen DateienüberschreibenDateien im übergeordneten Verzeichnis

Dies muss jedoch noch weiter auf konsistentes Verhalten getestet werden (da bin ich mir nicht sicher). Der richtige Weg wäre exlude files1.json+ funktioniert files5.jsonleider nicht mit-X--null

Wenn Sie diesem Verhalten nicht vertrauen oder keine doppelten Dateien in Archiven möchten, können Sie einige Ausschlüsse für einfache Dateinamen hinzufügen.Kommentar entfernender obige Code tar. Zeilenumbrüche und Leerzeichen sind in Dateinamen zulässig, werden aber mit Platzhaltern ?im Ausschlussmuster ausgeschlossen, was theoretisch mehr Dateien als erwartet ausschließen könnte (wenn es ähnliche Dateien gibt, die diesem Muster entsprechen).

Sie können ein echovor setzen tarund Sie werden sehen, dass das Skript die folgenden Befehle generiert

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

verwandte Informationen