
Eu tenho uma pasta raiz Products
e 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}.json
mas 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.gz
arquivo executando o comando abaixo -
tar cvzf ./products.tgz Products
Pergunta:-
Eu obtive um novo design, conforme mostrado abaixo, onde cada subpasta dentro Products
da pasta raiz contém três pastas de ambiente dev
- stage
e 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 folder1
da subpasta existem mais três subpastas dev
e exatamente stage
a prod
mesma coisa para outras subpastas folder2
e folder3
. Cada um deles dev
e stage
a prod
subpasta dentro folder{number}
da subpasta terão arquivos que serão substituídos por eles.
Preciso gerar três tar.gz
arquivos diferentes agora - um para cada dev
e stage
a prod
partir da estrutura acima.
- Quaisquer arquivos que eu tenha dentro
dev
,stage
elesprod
substituirão seus arquivos de subpasta se estiverem presentes em sua subpasta (pasta1, pasta2 ou pasta3) também. - Portanto, se
files1.json
estiver presente nafolder1
subpasta e o mesmo arquivo também estiver presente em qualquer um delesdev
,stage
eprod
durante 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 stage
e outra para prod
onde 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.gz
e products-prod.gz
a partir do figure 2
qual terá dados parecidos figure 3
mas 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.gz
arquivos 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 folder2
e folder3
tem pastas de substituição de ambiente, mas elas não possuem nenhum arquivo, então nesse caso quero gerar arquivos vazios folder2
e folder3
també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:
- mostra a sintaxe das ferramentas GNU. Para BSD
find
você deve substituir-regextype posix-extended
por just-E
e para BSDtar
você deve substituir--no-recursion
por just-n
as well as--transform=s
(<- note o finals
) por just-s
- para simplificar a demonstração, o snippet assume ser executado a partir do diretório que contém
Products
e usa a$e
variável personalizada para o nome do diretório "ambientes" a ser arquivado, enquanto$r
é apenas uma variável auxiliar de nome abreviado para conter oProducts
nome - ele está entre parênteses, tornando-o um subshell, apenas para não poluir seu shell
$r
e$e
você deve executá-lo a partir da linha de comando - 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 ...; done
loop de shell e pronto. (possivelmente tirando os parênteses externos e cercando todo o for
loop).
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 find
comandos duplos alimentem tar
primeiro 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 é:
- (além dos parênteses externos e das variáveis auxiliares)
- o primeiro
find
comando produz apenas a lista de arquivos não específicos (e diretórios principais de acordo com sua atualização), enquanto o segundofind
produz apenas a lista de todos os arquivos específicos de ambientes - os dois
find
comandos estão entre parênteses sozinhos para que ambas as saídas alimentem o canal emtar
sequência tar
lê esse canal para obter os nomes dos arquivos e coloca esses arquivos no arquivo, ao mesmo tempo que--transform
altera seus nomes, eliminando o componente "ambientes" (se presente) do nome do caminho de cada arquivo- os dois
find
comandos 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 (paratar
consumo) 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:
- tudo o que dissemos anteriormente sobre as sintaxes GNU e BSD
find
etar
se aplica aqui também - 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
- Estou usando GNU
sed
aqui para lidar com E/S delimitada por nul (opção-z
), mas você pode facilmente substituir esses doissed
comandos por, por exemplo, umwhile 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, GNUgawk
pode fazer isso); veja abaixo uma substituição usando loops Bash - Eu uso um único
find
aqui, pois não estou confiando em nenhum comportamento implícito detar
- Os
sed
comandos manipulam a lista de nomes, abrindo caminho para ossort
comandos - especificamente, o primeiro
sed
move o nome dos "ambientes" no início do caminho, também prefixando-o com um0
nú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ício1
para o propósito de Ordenação - tal preparação normaliza a lista de nomes nos "olhos" dos
sort
comandos, 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 assort
definições das chaves do ' - o primeiro
sort
aplica uma ordenação baseada primeiro nos nomes dos arquivos, colocando assim nomes iguais adjacentes entre si, e depois pelo valor numérico de0
ou1
conforme marcado anteriormente pelosed
comando, garantindo assim que qualquer arquivo específico de "ambientes", quando presente, venha antes de sua contraparte não específica - o segundo
sort
se 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 - finalmente, um segundo
sed
desfaz o que foi feito pelo primeiro, remodelando assim os nomes dos arquivos paratar
arquivar
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 sed
comando 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 sed
comando:
(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 -c
peça posterior xargs
precisa ser transformada em bash -c
.
Responder2
Solução geral
- Faça uma cópia da árvore de diretórios. Vincule os arquivos para economizar espaço.
- Modifique a cópia. (No caso de hardlinks, você precisa saber o que pode fazer com segurança. Veja abaixo.)
- Arquive a cópia.
- Remova a cópia.
- 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.
Fazendo uma cópia
cd
para o diretório pai deProducts
. Este diretórioProducts
e tudo dentro dele devem pertencer a um único sistema de arquivos. Crie um diretório temporário e recrie-Products
o lá:mkdir -p tmp cp -la Products/ tmp/
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
dev
na 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 aargument 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
dev
e desnecessárioprod
,stage
;equalquer outro diretório nesta profundidade.Arquivando a cópia
# still in tmp/Products because of the previous step cd .. tar cvzf "products-$dname.tgz" Products
Removendo a cópia
# now in tmp because of the previous step rm -rf Products
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 stage
são codificadas dentro do script (mas podem ser facilmente alteradas)
Nota: esta é uma extensão --transform
e 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. tar
descerá 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.json
infelizmente -X
nã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 echo
antes tar
e 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