Fazendo o script BASH `for` manipular nomes de arquivos com espaços (ou solução alternativa)

12

Enquanto uso o BASH há vários anos, minha experiência com scripts BASH é relativamente limitada.

Meu código é como abaixo. Ele deve pegar toda a estrutura de diretórios de dentro do diretório atual e replicá-la para $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

O problema é que, aqui está uma amostra da minha estrutura de arquivos:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Observe os espaços: -S E forutiliza parâmetros palavra por palavra, para que a saída do meu script seja algo como isto:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Mas eu preciso que ele pegue nomes de arquivos inteiros (uma linha de cada vez) da saída de find. Eu também tentei findcolocar aspas duplas em torno de cada nome de arquivo. Mas isso não ajuda.

for DIR in `find . -type d -printf "\"%P\"\040"`

E saída com esta linha alterada:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Agora, eu preciso de uma maneira que possa iterar dessa maneira, porque também desejo executar um comando mais complicado envolvendo gstreamercada arquivo em uma estrutura semelhante a seguir. Como devo fazer isso?

Edit: Eu preciso de uma estrutura de código que me permita executar várias linhas de código para cada diretório / arquivo / loop. Desculpe se eu não era clara.

Solução: inicialmente tentei:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

Isso funcionou bem na maior parte do tempo. No entanto, descobri mais tarde que uma vez que os resultados de tubos no circuito durante a execução em um subnível, quaisquer variáveis definidas no circuito foram mais tarde indisponíveis que fez implementar um contador de erros bastante difícil. Minha solução final ( desta resposta no SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

Isso mais tarde me permitiu incrementar condicionalmente variáveis ​​dentro do loop, que permaneceriam disponíveis posteriormente no script.

Samuel Jaeschke
fonte
Por que você desejaria cada vez mais um espaço no nome do arquivo?
precisa
Verdade, não é minha preferência. Embora, para remover espaços, você precisa lidar com arquivos com espaços em primeiro lugar;)
Samuel Jaeschke
1
Na verdade, os nomes de arquivos devem permitir espaços. Eu permitiria qualquer coisa, exceto /caracteres imprimíveis. Mas tudo é permitido, exceto /e, \0portanto, você deve permitir.
precisa

Respostas:

11

Você precisa findinserir o whileloop em um loop.

find ... | while read -r dir
do
    something with "$dir"
done

Além disso, você não precisará usar -printfneste caso.

Você pode fazer essa prova de arquivos com novas linhas em seus nomes, se desejar, usando um delimitador de nulos (que é o único caractere que não pode aparecer em um caminho de arquivo * nix):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

Você também encontrará o uso em $()vez de reticulares mais versátil e fácil. Eles podem ser aninhados com muito mais facilidade e a citação pode ser feita com muito mais facilidade. Este exemplo artificial ilustrará estes pontos:

echo "$(echo "$(echo "hello")")"

Tente fazer isso com retalhos.

Pausado até novo aviso.
fonte
2
Além disso "$dir", é preferível usar "${dir}"- é fácil dizer a diferença entre $ {dir} name e $ {dirname}, mas $ dirname poderia ser interpretado de qualquer maneira.
James Polley
O importante aqui é que readlê uma linha inteira ${dir}, para que o IFS não importe.
31411 James Polley
1
Obrigado por encontrar o erro de digitação $ / ". As chaves não são necessárias se não houver nada após o nome da variável.
Pausado até novo aviso.
4
Isso manipulará nomes de caminho com espaços (U + 0020), mas ainda assim falhará ao tratar adequadamente nomes de caminho com feeds de linha (U + 000A). Prefiro find … -print0 | xargs -0 …porque o delimitador usado corresponde exatamente ao único caractere que não é permitido nos nomes de caminho POSIX: NUL (U + 0000).
31410 Chris Johnsen
2
Perfeito! É mesmo o que eu procurava. Nunca me ocorreu que você pudesse conseguir while. @ Chris Johnsen: É verdade, mas mesmo os programas de ripar músicas não tendem a colocar feeds de linha em seus nomes de arquivos. E se o fizerem, eu quero saber (ou seja: algo der errado) e se livrar deles imediatamente ...
Samuel Jaeschke
8

Veja esta resposta que escrevi há alguns dias para um exemplo de script que lida com nomes de arquivos com espaços.

Existe uma maneira um pouco mais complicada (mas mais concisa) de alcançar o que você está tentando fazer:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0diz ao find para separar os argumentos com um nulo; o -0 a xargs diz para ele esperar argumentos separados por nulos. Isso significa que ele lida bem com os espaços.

-I {}diz ao xargs para substituir a string {}pelo nome do arquivo. Isso também implica que apenas um nome de arquivo deve ser usado por linha de comando (o xargs normalmente inclui o número que couber na linha)

O resto deve ser óbvio.

James Polley
fonte
A sugestão de Dennis Williamson é, no entanto (além dos erros de digitação), muito mais legível e, portanto, preferível em quase todos os aspectos.
31411 James Polley
Funciona, para o mkdir, mas desculpe, eu deveria ter sido mais claro - desejo executar uma série de comandos para cada arquivo. Veja, para minha rotina semelhante mais tarde, desejo gerar um nome de arquivo de saída com base no nome do arquivo de entrada (que envolve retirar a extensão .ogg e adicionar .mp3) e, em seguida, usar essas múltiplas variáveis ​​no meu pipline ao invocar o gst-launch.
Samuel Jaeschke
5

O problema que você está enfrentando é que a instrução for está respondendo à localização como argumentos separados. O delimitador de espaço. Você precisa usar a variável IFS do bash para não dividir no espaço.

Aqui está um link que explica como fazer isso.

A variável interna IFS

Uma maneira de contornar esse problema é alterar a variável interna IFS (Internal Field Separator) do Bash para que ele divida os campos por algo diferente do espaço em branco padrão (espaço, guia, nova linha), neste caso, vírgula.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Defina sua localização para gerar seu delimitador de campo após o% P e defina seu IFS adequadamente. Escolhi ponto-e-vírgula, pois é pouco provável que você encontre seus nomes de arquivo.

A outra alternativa é chamar mkdir diretamente da localização, via -execvocê pode pular o loop for completamente. Isso se você não precisar fazer nenhuma análise adicional.

Darren Hall
fonte
E se o nome do arquivo contiver o IFS? Então você tem que escolher um diferente. Mas então, e se ...
Pausou até novo aviso.
3
Você pode escolher /no POSIX e :nos sistemas de arquivos DOS. Existem caracteres ilegais para diferentes sistemas de arquivos que você pode escolher para o IFS. Qualquer coisa mais complicada e é melhor você usar perl.
Darren Hall
2
O problema com o uso de / é que ele é o delimitador de diretório e findretorna nomes de arquivos com caminhos, incluindo uma barra. Tente alterar o ponto-e-vírgula no seu script para uma barra e o eco imprimirá o diretório e o nome do arquivo em linhas separadas.
Pausado até novo aviso.
Isso também parece bastante útil. Eu optei pelo pipe while, mas isso também parece bastante viável. Sim, em minha estrutura semelhante mais tarde eu precisei fazer uma análise mais aprofundada. (O nome do arquivo de entrada seria .ogg, que seria passado como filesrcno pipeline gst, mas um final equivalente em .mp3 baseado no diretório de saída seria gerado e também passado para o pipeline como filesink, e é claro que isso precisa ser feito para cada arquivo, juntamente com alguns echopara o usuário.)
Samuel Jaeschke
4

Se o corpo do seu loop for mais do que um único comando, é possível usar o xargs para conduzir um script de shell:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Certifique-se de incluir o traço final (ou algum outro 'palavra') se o reservatório é da variedade Bourne / POSIX (ele é usado para definir $ 0 no script shell). Além disso, é preciso ter cuidado com a citação, pois o script do shell está sendo escrito dentro de uma cadeia de caracteres citada, em vez de diretamente no prompt.

Chris Johnsen
fonte
Outro conceito interessante. Graças - Eu tenho certeza que vou encontrar um uso para isso mais tarde :)
Samuel Jaeschke
1

na sua pergunta atualizada você tem

mkdir -p \"${OUTPATH}${DIR}\"

isso deve ser

mkdir -p "${OUTPATH}${DIR}"
user23307
fonte
Obrigado. Fixo. Também estava lendo para FILENAME em vez de DIR - copiar e colar: P
Samuel Jaeschke
1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'
Vouze
fonte
0

ou para tornar a coisa toda muito menos complicada:

% rsync -av --include='*/' --exclude='*' SRC DST

isso replica a estrutura de diretórios do SRC no horário de verão.

akira
fonte
Não, preciso de uma estrutura iterativa como essa, que me permita executar várias linhas de código para cada arquivo. "Agora, eu preciso de alguma maneira que possa iterar dessa maneira, porque também desejo executar um comando mais complicado envolvendo o gstreamer em cada arquivo em uma estrutura semelhante a seguir." Desculpe se eu não era clara.
Samuel Jaeschke
o comando que eu dei resolve o problema que você pediu, não importa se isso é apenas parte de um "pipeline" maior do seu lado. para outra pessoa com o problema descrito na pergunta, a abordagem rsync funcionará. assim, não há necessidade de se desculpar potencial unclearity :)
akira
Sim. Não, quero dizer que eu estaria usando um semelhante while... do... doneestrutura mais tarde para fazer o processamento similar de encontrar, o que exigiria várias linhas de código para ser executado em cada arquivo (modificar corda, eco, gst-launch, etc. ) e rsyncnão conseguiria isso. Foi por isso que especifiquei que precisava executar um conjunto de comandos mais complicado dentro de uma estrutura semelhante. Meu script usa essa estrutura de loop duas vezes, então, para a pergunta, postei a que apresentava menos crude no meio.
Samuel Jaeschke
0

Se você possui o GNU Parallel http: // www.gnu.org/software/parallel/ instalado, você pode fazer isso:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Assista ao vídeo de introdução do GNU Parallel para saber mais: http://www.youtube.com/watch?v=OpaiGYxkSuQ

Ole Tange
fonte