Como posso escapar do espaço em branco em uma lista de loop bash?

121

Eu tenho um script de shell bash que percorre todos os diretórios filho (mas não arquivos) de um determinado diretório. O problema é que alguns dos nomes de diretório contêm espaços.

Aqui está o conteúdo do meu diretório de teste:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

E o código que percorre os diretórios:

for f in `find test/* -type d`; do
  echo $f
done

Aqui está a saída:

teste / Baltimore
teste / cereja
Colina
teste / Edison 
teste / novo
Iorque
Cidade
test / Filadélfia

Cherry Hill e New York City são tratadas como 2 ou 3 entradas separadas.

Tentei citar os nomes de arquivos, assim:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

mas sem sucesso.

Deve haver uma maneira simples de fazer isso.


As respostas abaixo são ótimas. Mas para tornar isso mais complicado - nem sempre quero usar os diretórios listados no meu diretório de teste. Às vezes, quero passar os nomes de diretório como parâmetros da linha de comando.

Peguei a sugestão de Charles de definir o IFS e criei o seguinte:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

e isso funciona bem, a menos que haja espaços nos argumentos da linha de comando (mesmo que esses argumentos sejam citados). Por exemplo, chamar o script assim: test.sh "Cherry Hill" "New York City"produz a seguinte saída:

cereja
Colina
Novo
Iorque
Cidade
MCS
fonte
re: edit, list="$@"descarta completamente a lista do valor original, recolhendo-o em uma string. Por favor, siga as práticas em minha resposta exatamente como fornecidas - tal tarefa não é incentivada em nenhum lugar; se você quiser passar uma lista de argumentos da linha de comando para um programa, colete-os em uma matriz e expanda-a diretamente.
Charles Duffy

Respostas:

105

Primeiro, não faça dessa maneira. A melhor abordagem é usar find -execcorretamente:

# this is safe
find test -type d -exec echo '{}' +

A outra abordagem segura é usar a lista terminada por NUL, embora isso exija que você encontre suporte -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Você também pode preencher uma matriz de find e passar essa matriz mais tarde:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Se a sua descoberta não suportar -print0, o resultado será inseguro - o abaixo não se comportará como desejado se existirem arquivos contendo novas linhas em seus nomes (o que, sim, é legal):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Se alguém não usar um dos itens acima, uma terceira abordagem (menos eficiente em termos de tempo e uso de memória, pois lê toda a saída do subprocesso antes de dividir palavras) é usar uma IFSvariável que não contém o caractere de espaço. Desative globbing ( set -f) para impedir que cadeias contendo caracteres globos, como [], *ou ?sejam expandidas:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Finalmente, para o caso de parâmetro da linha de comando, você deve usar matrizes se o seu shell suportar (por exemplo, é ksh, bash ou zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

manterá a separação. Observe que a citação (e o uso de $@e não $*) é importante. As matrizes também podem ser preenchidas de outras maneiras, como expressões glob:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Charles Duffy
fonte
1
não sabia sobre esse sabor '+' para -exec. doce
Johannes Schaub - litb
1
tho parece que também pode, como xargs, colocar apenas os argumentos no final do comando fornecido: / às vezes me incomoda
Johannes Schaub - litb 19/11/08
Eu acho que -exec [name] {} + é uma extensão GNU e 4.4-BSD. (Pelo menos, ele não aparece no Solaris 8, e eu não acho que foi em AIX 4.3 também.) Eu acho que o resto de nós pode ser preso com tubulação para xargs ...
Michael Ratanapintha
2
Eu nunca vi a sintaxe $ '\ n' antes. Como isso funciona? (Eu teria pensado que ou IFS = '\ n' ou IFS = "\ n" iria funcionar, mas também não faz.)
MCS
1
@crosstalk está definitivamente no Solaris 10, eu apenas o usei.
Nick
26
find . -type d | while read file; do echo $file; done

No entanto, não funciona se o nome do arquivo contiver novas linhas. A descrição acima é a única solução que eu sei quando você realmente deseja ter o nome do diretório em uma variável. Se você quiser apenas executar algum comando, use xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '
Johannes Schaub - litb
fonte
Não há necessidade de xargs, consulte find -exec ... {} +
Charles Duffy
4
@ Charles: para um grande número de arquivos, o xargs é muito mais eficiente: gera apenas um processo. A opção -exec bifurca um novo processo para cada arquivo, que pode ser uma ordem de magnitude mais lenta.
Adam Rosenfield
1
Eu gosto mais de xargs. Esses dois parecem essencialmente fazer o mesmo, enquanto o xargs tem mais opções, como rodar em paralelo
Johannes Schaub - litb 19/11/08
2
Adam, não que '+' um agregue o maior número possível de nomes de arquivos e depois execute. mas não vai ter tais funções puras como correr em paralelo :)
Johannes Schaub - litb
2
Observe que se você quiser fazer algo com os nomes de arquivos, precisará citá-los. Por exemplo:find . -type d | while read file; do ls "$file"; done
David Moles
23

Aqui está uma solução simples que lida com guias e / ou espaços em branco no nome do arquivo. Se você precisar lidar com outros caracteres estranhos no nome do arquivo, como novas linhas, escolha outra resposta.

O diretório de teste

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

O código para entrar nos diretórios

find test -type d | while read f ; do
  echo "$f"
done

O nome do arquivo deve ser citado ( "$f") se usado como argumento. Sem aspas, os espaços atuam como separador de argumentos e vários argumentos são dados ao comando invocado.

E a saída:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
cbliard
fonte
obrigado, isso funcionou para o alias que eu estava criando para listar quanto espaço cada diretório da pasta atual está usando, ele estava sufocando em alguns diretórios com espaços na encarnação anterior. Isso funciona no zsh, mas algumas das outras respostas não o fizeram:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid
2
Se você estiver usando o zsh, também poderá fazer o seguinte:alias duc='du -sh *(/)'
cbliard
@ cbliard Isso ainda é buggy. Tente executá-lo com um nome de arquivo com, digamos, uma sequência de tabulação ou vários espaços; você notará que isso altera qualquer um deles para um único espaço, porque você não está citando seu eco. E depois há o caso de nomes de arquivos contendo novas linhas ...
Charles Duffy
@CharlesDuffy Eu tentei com sequências de guias e vários espaços. Funciona com aspas. Eu também tentei com novas linhas e não funciona. Eu atualizei a resposta de acordo. Obrigado por apontar isto.
cbliard
1
@ cbliard Certo - adicionar aspas ao seu comando de eco era o que eu estava conseguindo. Quanto às novas linhas, você pode fazer isso funcionar usando find -print0e IFS='' read -r -d '' f.
Charles Duffy
7

Isso é extremamente complicado no Unix padrão, e a maioria das soluções falha em novas linhas ou em algum outro personagem. No entanto, se você estiver usando o conjunto de ferramentas GNU, poderá explorar a findopção -print0e usarxargs com a opção correspondente -0(menos-zero). Existem dois caracteres que não podem aparecer em um nome de arquivo simples; essas são barras e NUL '\ 0'. Obviamente, a barra aparece nos nomes dos caminhos, portanto a solução GNU de usar um NUL '\ 0' para marcar o final do nome é engenhosa e à prova de idiotas.

Jonathan Leffler
fonte
4

Por que não colocar

IFS='\n'

na frente do comando for? Isso altera o separador de campos de <Espaço> <Guia> <Nova Linha> para apenas <Nova Linha>

oshunluvr
fonte
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
fonte
1
-d $'\0'é precisamente o mesmo que -d ''- porque o bash usa cadeias terminadas em NUL, o primeiro caractere de uma cadeia vazia é uma NUL e, pelo mesmo motivo, as NULs não podem ser representadas dentro das cadeias C.
Charles Duffy
4

eu uso

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Isso não seria suficiente?
Ideia retirada de http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

Murpel
fonte
grande dica: isso é muito útil para as opções a um osascript de linha de comando (OS X AppleScript), onde os espaços dividem um argumento em vários parâmetros, onde apenas um é destinado
tim
Não, não é suficiente. É ineficiente (devido ao uso desnecessário de $(echo ...)), não manipula nomes de arquivos com expressões globais corretamente, não manipula nomes de arquivos que contêm $'\b'ou $ '\ n' caracteres corretamente e, além disso, converte várias execuções de espaço em branco em caracteres de espaço único no saída devido a cotação incorreta.
22613 Charles Duffy
4

Não armazene listas como strings; armazene-os como matrizes para evitar toda essa confusão de delimitadores. Aqui está um exemplo de script que irá operar em todos os subdiretórios de teste ou na lista fornecida em sua linha de comando:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Agora vamos tentar isso em um diretório de teste com uma ou duas curvas inseridas:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City
Gordon Davisson
fonte
1
Olhando para trás - realmente havia uma solução com o POSIX sh: você poderia reutilizar a "$@"matriz anexando-a set -- "$@" "$f".
Charles Duffy
4

Você pode usar o IFS (separador de campo interno) temporariamente usando:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS

incrível
fonte
Por favor, forneça uma explicação.
Steve K
Se o IFS especificou qual é o símbolo separador, o nome do arquivo com espaço em branco não seria truncado.
amazingthere
$ IFS = $ OLD_IFS no final deve ser: IFS = $ OLD_IFS
Michel Donais
3

ps se for apenas sobre o espaço na entrada, algumas aspas duplas funcionaram sem problemas para mim ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
hardbutnot
fonte
2

Para adicionar ao que Jonathan disse: use a -print0opção para findem conjunto com xargso seguinte:

find test/* -type d -print0 | xargs -0 command

Isso executará o comando commandcom os argumentos adequados; diretórios com espaços neles serão citados corretamente (ou seja, serão passados ​​como um argumento).

Adam Rosenfield
fonte
1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

O código acima irá converter arquivos .mov para .avi. Os arquivos .mov estão em pastas diferentes e os nomes das pastas têm espaços em branco . Meu script acima irá converter os arquivos .mov para o arquivo .avi na mesma pasta. Não sei se isso ajuda as pessoas.

Caso:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Felicidades!

Sony George
fonte
echo "$name" | ...não funciona se nameestá -n, e como ele se comporta com nomes com seqüências de escape de barra invertida depende da sua implementação - o POSIX torna o comportamento echonesse caso explicitamente indefinido (enquanto o POSIX estendido por XSI faz a expansão do comportamento definido por padrão de seqüências de escape de barra invertida) e sistemas GNU - incluindo Bash - sem POSIXLY_CORRECT=1pausa o padrão POSIX através da implementação -e(ao passo que a especificação exige echo -epara imprimir -e. na saída) printf '%s\n' "$name" | ...é mais seguro.
Charles Duffy
1

Também tinha que lidar com espaços em branco em nomes de caminho. O que finalmente fiz foi usar uma recursão e for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}
Gilles 'SO- parar de ser mau'
fonte
1
Não use a functionpalavra-chave - ela torna seu código incompatível com o POSIX sh, mas não tem outro objetivo útil. Você pode apenas definir uma função com recursedir() {, adicionando os dois parênteses e removendo a palavra-chave da função, e isso será compatível com todos os shells compatíveis com POSIX.
22813 Charles Duffy
1

Converta a lista de arquivos em uma matriz Bash. Isso usa a abordagem de Matt McClure para retornar uma matriz de uma função Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html O resultado é uma maneira para converter qualquer entrada com várias linhas em uma matriz Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Essa abordagem parece funcionar mesmo quando caracteres ruins estão presentes e é uma maneira geral de converter qualquer entrada em uma matriz Bash. A desvantagem é que, se a entrada for longa, você poderá exceder os limites de tamanho da linha de comando do Bash ou consumir grandes quantidades de memória.

As abordagens nas quais o loop que está trabalhando na lista também têm a lista em questão têm a desvantagem de que ler stdin não é fácil (como pedir entrada ao usuário) e o loop é um novo processo, então você pode estar se perguntando por que variáveis você definir dentro do loop não estará disponível depois que o loop terminar.

Também não gosto de configurar o IFS, ele pode atrapalhar outro código.

Steve Zobell
fonte
Se você usar IFS='' read, na mesma linha, a configuração IFS estará presente apenas para o comando de leitura e não escapará. Não há motivo para não gostar de configurar o IFS dessa maneira.
Charles Duffy
1

Bem, eu vejo muitas respostas complicadas. Eu não quero passar a saída do utilitário find ou escrever um loop, porque o find tem a opção "exec" para isso.

Meu problema era que eu queria mover todos os arquivos com extensão dbf para a pasta atual e alguns deles continham espaço em branco.

Eu lidei com isso assim:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Parece muito simples para mim

Tebe
fonte
0

acabei de descobrir que existem algumas semelhanças entre a minha pergunta e a sua. Aparentemente, se você deseja passar argumentos para comandos

test.sh "Cherry Hill" "New York City"

para imprimi-los em ordem

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

observe que o $ @ está entre aspas duplas, algumas notas aqui

Jeffrey04
fonte
0

Eu precisava do mesmo conceito para compactar sequencialmente vários diretórios ou arquivos de uma determinada pasta. Eu resolvi usar o awk para analisar a lista de ls e evitar o problema de espaço em branco no nome.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

O que você acha?

Hìr0
fonte
Eu acho que isso não funcionará corretamente se os nomes de arquivos tiverem novas linhas. Talvez você deva tentar.
user000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Johan Kasselman
fonte
-3

Para mim, isso funciona, e é praticamente "limpo":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
fonte
4
Mas isso é pior. As aspas duplas ao redor da localização fazem com que todos os nomes de caminho sejam concatenados como uma única sequência. Altere o eco para ls para ver o problema.
NVRAM
-4

Só tinha um problema de variante simples ... Converta arquivos de .flv digitados em .mp3 (bocejo).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

encontre recursivamente todos os arquivos flash do usuário do Macintosh e transforme-os em áudio (cópia, sem transcodificação) ... é como o descrito acima, observando que a leitura em vez de apenas 'para o arquivo ' escapará.

Mark Washeim
fonte
2
O readdepois iné mais uma palavra na lista que você está repetindo. O que você postou é uma versão ligeiramente quebrada do que o solicitante tinha, o que não funciona. Você pode ter a intenção de postar algo diferente, mas provavelmente está coberto por outras respostas aqui de qualquer maneira.
Gilles 'SO- stop be evil'