Como gerar vários arquivos tar.gz substituindo arquivos específicos para cada ambiente?

Como gerar vários arquivos tar.gz substituindo arquivos específicos para cada ambiente?

Eu tenho uma pasta raiz Productse várias subpastas dentro dela. Cada uma dessas subpastas possui vários arquivos a partir de agora. Apenas para simplificar, criei subpastas com nomes como folder{number}e nomes de arquivos como, files{number}.jsonmas em geral eles têm nomes diferentes.

Em geral, tenho 20 subpastas diferentes dentro da pasta raiz e cada subpasta possui no máximo 30 arquivos.

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

Agora estou compactando tudo isso em um tar.gzarquivo executando o comando abaixo -

tar cvzf ./products.tgz Products

Pergunta:-

Eu obtive um novo design, conforme mostrado abaixo, onde cada subpasta dentro Productsda pasta raiz contém três pastas de ambiente dev- stagee prod.

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

Por exemplo - Dentro folder1da subpasta existem mais três subpastas deve exatamente stagea prodmesma coisa para outras subpastas folder2e folder3. Cada um deles deve stagea prodsubpasta dentro folder{number}da subpasta terão arquivos que serão substituídos por eles.

Preciso gerar três tar.gzarquivos diferentes agora - um para cada deve stagea prodpartir da estrutura acima.

  • Quaisquer arquivos que eu tenha dentro dev, stageeles prodsubstituirão seus arquivos de subpasta se estiverem presentes em sua subpasta (pasta1, pasta2 ou pasta3) também.
  • Portanto, se files1.jsonestiver presente na folder1subpasta e o mesmo arquivo também estiver presente em qualquer um deles dev, stagee proddurante o empacotamento, preciso usar o que estiver presente na pasta de ambiente e substituir os arquivos da subpasta, caso contrário, apenas use o que estiver presente em sua subpasta pasta(s).

No final terei 3 estruturas diferentes como esta - uma para dev, uma para stagee outra para prodonde a pasta1 (ou 2 e 3) terá os arquivos de acordo com o que tenho em seu ambiente como primeira preferência, já que eles são substituídos e outros arquivos que são não substituído.

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

E preciso gerar products-dev.gz, products-stage.gze products-prod.gza partir do figure 2qual terá dados parecidos figure 3mas específicos para cada ambiente. A única diferença é que cada subpasta pasta1 (2 ou 3) terá arquivos que serão substituídos por eles como primeira preferência de sua pasta de ambiente específica e o restante será usado apenas em sua subpasta.

Isso é possível através de alguns comandos do Linux? A única confusão que tenho é como substituir arquivos de ambiente específicos dentro de uma subpasta específica e, em seguida, gerar três tar.gzarquivos diferentes neles.

Atualizar:

Considere também casos como os abaixo:

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

Como você pode ver folder2e folder3tem pastas de substituição de ambiente, mas elas não possuem nenhum arquivo, então nesse caso quero gerar arquivos vazios folder2e folder3também específicos de cada ambiente .tar.gz

Responder1

Pode haver muitas maneiras, embora todas exijam algum tipo de complexidade para lidar com o caso de substituição.

Como uma linha única, embora um pouco longa, você poderia fazer assim para uma iteração, ou seja, um diretório de "ambientes":

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

dividido para observá-lo melhor:

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

Coisas a serem observadas:

  1. mostra a sintaxe das ferramentas GNU. Para BSD findvocê deve substituir -regextype posix-extendedpor just -Ee para BSD tarvocê deve substituir --no-recursionpor just -nas well as --transform=s(<- note o final s) por just-s
  2. para simplificar a demonstração, o snippet assume ser executado a partir do diretório que contém Productse usa a $evariável personalizada para o nome do diretório "ambientes" a ser arquivado, enquanto $r é apenas uma variável auxiliar de nome abreviado para conter o Productsnome
  3. ele está entre parênteses, tornando-o um subshell, apenas para não poluir seu shell $re $evocê deve executá-lo a partir da linha de comando
  4. ele não copia nem vincula/refere-se aos arquivos originais, lida com qualquer nome de arquivo válido, não tem restrições de memória e pode lidar com qualquer quantidade de nomes; a única suposição é sobre os dois primeiros níveis da hierarquia de diretórios, pois qualquer diretório diretamente abaixo do primeiro nível é considerado um diretório de "ambientes" e, portanto, ignorado (exceto aquele indicado em $e)

Você poderia simplesmente colocar esse trecho em um for e in dev prod stage; do ...; doneloop de shell e pronto. (possivelmente tirando os parênteses externos e cercando todo o forloop).

A vantagem é que, afinal, é bastante curto e relativamente simples.

A desvantagem é que ele sempre arquiva tambémtodososubstituídoarquivos (ou seja, os básicos), o truque é apenas que os findcomandos duplos alimentem tarprimeiro os arquivos a serem substituídos e, portanto, durante a extração, eles serão substituídos pelos arquivos substituídos (ou seja, os específicos dos "ambientes"). Isto faz com que um arquivo maior demore mais tempo tanto durante a criação como durante a extração, e pode ser indesejável dependendo se essa "sobrecarga" pode ser insignificante ou não.

Esse pipeline descrito em prosa é:

  1. (além dos parênteses externos e das variáveis ​​auxiliares)
  2. o primeiro findcomando produz apenas a lista de arquivos não específicos (e diretórios principais de acordo com sua atualização), enquanto o segundo findproduz apenas a lista de todos os arquivos específicos de ambientes
  3. os dois findcomandos estão entre parênteses sozinhos para que ambas as saídas alimentem o canal em tarsequência
  4. tarlê esse canal para obter os nomes dos arquivos e coloca esses arquivos no arquivo, ao mesmo tempo que --transformaltera seus nomes, eliminando o componente "ambientes" (se presente) do nome do caminho de cada arquivo
  5. os dois findcomandos são separados em vez de serem apenas um, e são executados um após o outro, para que os arquivos não específicos sejam produzidos (para tarconsumo) antes dos arquivos específicos dos ambientes, o que possibilita o truque que descrevi anteriormente

Para evitar a sobrecarga de incluirsempre tudoos arquivos, precisamos de complexidade adicional para realmente limpar os arquivos substituídos. Uma maneira pode ser como abaixo:

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

Várias coisas a serem observadas:

  1. tudo o que dissemos anteriormente sobre as sintaxes GNU e BSD finde tarse aplica aqui também
  2. como a solução anterior, não tem nenhuma restrição além da suposição sobre os dois primeiros níveis da hierarquia de diretórios
  3. Estou usando GNU sedaqui para lidar com E/S delimitada por nul (opção -z), mas você pode facilmente substituir esses dois sedcomandos por, por exemplo, um while read ...loop de shell (seria necessário o Bash versão 3 ou superior) ou outro idioma que você sinta confiança com, a única recomendação é que a ferramenta que você usa seja capaz de lidar com E/S delimitada por nul (por exemplo, GNU gawkpode fazer isso); veja abaixo uma substituição usando loops Bash
  4. Eu uso um único findaqui, pois não estou confiando em nenhum comportamento implícito detar
  5. Os sedcomandos manipulam a lista de nomes, abrindo caminho para os sortcomandos
  6. especificamente, o primeiro sedmove o nome dos "ambientes" no início do caminho, também prefixando-o com um 0número auxiliar apenas para classificá-lo antes dos arquivos que não são de ambientes, já que estou prefixando estes últimos com um início 1para o propósito de Ordenação
  7. tal preparação normaliza a lista de nomes nos "olhos" dos sortcomandos, fazendo com que todos os nomes sem o nome dos "ambientes" e todos tenham a mesma quantidade de campos delimitados por barras no início, o que é importante para as sortdefinições das chaves do '
  8. o primeiro sortaplica uma ordenação baseada primeiro nos nomes dos arquivos, colocando assim nomes iguais adjacentes entre si, e depois pelo valor numérico de 0ou 1conforme marcado anteriormente pelo sedcomando, garantindo assim que qualquer arquivo específico de "ambientes", quando presente, venha antes de sua contraparte não específica
  9. o segundo sortse aglutina (opção -u) nos nomes dos arquivos deixando apenas o primeiro dos nomes duplicados, que devido à reordenação anterior é sempre um arquivo específico de "ambientes" quando presente
  10. finalmente, um segundo seddesfaz o que foi feito pelo primeiro, remodelando assim os nomes dos arquivos para tararquivar

Se você estiver curioso para explorar as partes intermediárias de um pipeline tão longo, lembre-se de que todas elas funcionam comnulo-nomes delimitados e, portanto, não aparecem bem na tela. Você pode canalizar qualquer uma das saídas intermediárias (ou seja, retirando pelo menos o tar) para uma cortesia tr '\0' '\n'para mostrar uma saída amigável, apenas lembre-se de que nomes de arquivos com novas linhas ocuparão duas linhas na tela.

Várias melhorias poderiam ser feitas, certamente tornando-o uma função/script totalmente parametrizado, ou por exemplo detectando automaticamente qualquer nome arbitrário para diretórios de "ambientes", como abaixo:

Importante: preste atenção aos comentários pois eles podem não ser bem aceitos por um shell interativo

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

Exemplo de substituição do primeiro sedcomando por um loop 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)

Para o segundo sedcomando:

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

Ambos os trechos exigem os parênteses circundantes para serem substitutos imediatos de seus respectivos sed comandos no pipeline acima e, claro, a sh -cpeça posterior xargsprecisa ser transformada em bash -c.

Responder2

Solução geral

  1. Faça uma cópia da árvore de diretórios. Vincule os arquivos para economizar espaço.
  2. Modifique a cópia. (No caso de hardlinks, você precisa saber o que pode fazer com segurança. Veja abaixo.)
  3. Arquive a cópia.
  4. Remova a cópia.
  5. Repita (modificando de forma diferente) se necessário.

Exemplo

Limitações:

  • este exemplo usa opções não POSIX (testadas no Debian 10),
  • faz algumas suposições sobre a árvore de diretórios,
  • pode falhar se houver muitos arquivos.

Trate-o como uma prova de conceito, ajuste-o às suas necessidades.

  1. Fazendo uma cópia

    cdpara o diretório pai de Products. Este diretório Productse tudo dentro dele devem pertencer a um único sistema de arquivos. Crie um diretório temporário e recrie- Productso lá:

    mkdir -p tmp
    cp -la Products/ tmp/
    
  2. Modificando a cópia

    Os arquivos nas duas árvores de diretórios têm links físicos. Se você modificar seuscontenteentão você alterará os dados originais. As operações que modificam informações mantidas em diretórios são seguras, pois não alterarão os dados originais se realizadas na outra árvore. Estes são:

    • removendo arquivos,
    • renomear arquivos,
    • mover arquivos (isso inclui mover um arquivo sobre outro arquivo com mv),
    • criando arquivos totalmente independentes.

    No seu caso, para cada diretório nomeado devna profundidade correta, mova seu conteúdo um nível acima:

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

    Notas:

    • mv -- * ../é propenso a argument list too long,
    • por padrão *não corresponde a dotfiles.

    Em seguida, remova os diretórios:

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

    Observe que isso remove o agora vazio deve desnecessário prod, stage;equalquer outro diretório nesta profundidade.

  3. Arquivando a cópia

    # still in tmp/Products because of the previous step
    cd ..
    tar cvzf "products-$dname.tgz" Products
    
  4. Removendo a cópia

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

    Volte para o diretório correto e recomece, desta vez com dname=stage; e assim por diante.


Script de exemplo (rápido e sujo)

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

Responder3

Tornei isso um pouco mais genérico e trabalhei em nomes de arquivos não triviais sem realmente alterar os diretórios de origem

Productsé dado como argumento. palavras-chave dev prod stagesão codificadas dentro do script (mas podem ser facilmente alteradas)

Nota: esta é uma extensão --transforme específica do GNU-print0 -z

execute o script
./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

Você pode notar duplicatas dentro do arquivo. tardescerá recursivamente os diretórios, ao restaurar os arquivos mais profundossubstituirarquivos no diretório pai

No entanto, isso precisa de mais alguns testes em relação ao comportamento consistente (não tenho certeza sobre isso). a maneira correta seria exlude files1.json+ files5.jsoninfelizmente -Xnão funciona com--null

se você não confia nesse comportamento ou não deseja arquivos duplicados em arquivos, você pode adicionar algumas exclusões para nomes de arquivos simples.remova o comentárioo código acima tar. novas linhas e espaços em branco são permitidos em nomes de arquivos, mas serão excluídos com curinga ?no padrão de exclusão, o que poderia, em teoria, excluir mais arquivos do que o esperado (se houver arquivos semelhantes que correspondam a esse padrão)

você pode colocar um echoantes tare verá que o script gera os seguintes comandos

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

informação relacionada