Como armazenar em uma variável uma lista de arquivos que inclui a barra invertida quando necessário?

6

Pergunta principal:

Estou escrevendo um script para realizar um backup. Eu faço uma lista de arquivos da seguinte maneira:

LISTOFFILES=$(find ~ \( -name '*.[pP][dD][fF]' -o -name '*.[oO][dD][tT]' \))

A variável LISTOFFILES armazena mais ou menos algo como:

/home/user/file1.pdf /home/user/file2.odt /home/user/Python Tutorial.pdf

Meu próximo passo é:

tar cvf backup.tar $LISTOFFILES

A resposta do Bash é:

/home/user/file1.pdf
/home/user/file2.odt
/home/user/Python: Cannot stat: No such file or directory
tar: Tutorial.pdf

Eu entendo que o Bash assume que Python e Tutorial.pdf são arquivos diferentes.

Existe uma maneira de fazer uma lista de arquivos com barra invertida incluída com find ou devo mudar para usar ls com pipes?

Eu gostaria de ler suas dicas e conselhos.

Minha outra pergunta:

A resposta a esta pergunta não é realmente importante, mas uma dica pode ajudar

Como listar arquivos apenas com o nome do arquivo sem o caminho do arquivo?

Por exemplo: file em vez de /home/user/file .

Estou tentando com o comando find com as opções prune e path , mas não obtive êxito.

    
por omar 30.11.2011 / 05:53

3 respostas

7

Quanto à sua pergunta principal, você precisará adotar uma abordagem diferente. Seu método está colocando todos os arquivos em uma única string em $ LISTOFFILES. Então, quando você vai acessá-lo, você não está usando aspas, o que faz com que ele se divida em espaços e lhe dê o resultado que você está vendo.

A maneira mais simples de obter o resultado desejado é o seguinte:

find ~ \( -name '*.[pP][dD][fF]' -o -name '*.[oO][dD][tT]' \) -print0 | xargs -0 tar cvf backup.tar

O que fizemos lá foi executar o find e separar cada resultado com um caractere NULL (a parte -print0). A saída de find é então canalizada para xargs que lê em uma lista de arquivos separados pelo caracter NULL (o argumento -0 informa ao xargs para fazer isso), e passa essa lista de arquivos como argumentos para 'tar'.

Para sua segunda pergunta: Se você deseja obter apenas nomes de arquivos apenas do comando find , use printf

find /path/to -printf '%f\n'
file.txt

Se você quer dizer, em geral, não apenas com encontrar,  é mais fácil usar basename .

# basename /path/to/file.txt
file.txt

basename foi projetado para fazer exatamente isso e nada mais.

    
por 30.11.2011 / 07:14
3

Por que sua tentativa falhou e como corrigi-la

Você pode acumular uma lista de nomes de arquivos retornados por find , mas somente se não houver novas linhas em nomes de arquivos, e você precisa tomar precauções.

Quando você escreve uma substituição de comando sem aspas ou expansão variável, como $LISTOFFILES aqui, o shell executa divisão de palavras e globbing (expansão de wildcard) no resultado.

Regra: sempre coloque aspas duplas em torno de substituições de comandos e expansões de variáveis.
Exceção: se você entender por que precisa deixar as aspas duplas e por que é seguro fazê-lo.

Aqui, você precisa deixar as aspas duplas, porque a divisão de palavras é o que divide a string em bits separados por nova linha. Mas, por padrão, ele também se divide em outros caracteres de espaço em branco. Então você precisa fazer isso seguro e desligar também. Você realiza o primeiro configurando a IFS variable para uma única nova linha e a última invocando set -f .

IFS='
'
set -f
tar cvf backup.tar $LISTOFFILES

Observe que não há barras invertidas envolvidas. Barras invertidas são usadas quando você insere nomes de arquivos diretamente, não quando são produzidos por comandos como find .

A maneira usual de usar find

Em vez de analisar a saída de find , você deve fazer com que invoque o comando que deseja executar nos arquivos, por meio da ação -exec . Isso cuida do caso em que há tantos arquivos que o tamanho máximo da linha de comando é excedido: o programa é invocado tantos quantos forem necessários.

Aqui, isso é um pouco difícil, porque você não pode invocar tar -c várias vezes (seria iniciado novamente a partir de um novo arquivo vazio em cada chamada). Mas veja Preencha todos os PDFs em um diretório, mantendo a estrutura de diretório

Métodos mais fáceis

Se não houver muitos arquivos, e você estiver executando ksh93 ou bash ≥4 ou zsh, use o curinga ** para recorrer a subdiretórios. Você precisa definir algumas opções de shell primeiro:

set -o globstar                  # on ksh93
shopt -s globstar -s extglob     # on bash 4
setopt ksh_glob                  # on zsh
tar cvf backup.tar ~/**/*.@(PDF|pdf|ODT|odt)

No zsh, sem configurações específicas, você pode escrever esse padrão como ~/**/*.(PDF|pdf|ODT|odt) . Depois de setopt extglob , você pode escrevê-lo como ~/**/(#i)*.(pdf|odt) .

Ou você pode usar pax :

pax -w -x ustar -s '/\.pdf$/&/' -s '/\.PDF$/&/' -s '/\.odt$/&/' -s '/\.ODT$/&/' -s '/.*//' ~ >backup.tar

Se você não quiser o prefixo do caminho

Existem maneiras com tar e pax , mas a abordagem mais simples é mudar para o diretório que você deseja arquivar primeiro.

( IFS='
'; set -f; cd ~ && tar cvf /path/to/backup.tar $(find . …) )
( cd ~ && pax -w … . ) >backup.tar
    
por 01.12.2011 / 01:32
2

Primeira pergunta:

A resposta para sua primeira pergunta é quotes .

Em vez de fazer - > tar cvf backup.tar $LISTOFFILES

Faça isso - > tar cvf backup.tar "$LISTOFFILES"

Vamos dar uma olhada em um exemplo (eu peguei um dos seus arquivos para esta demo, mesmo que o arquivo não exista no meu computador, veja o erro que estou recebendo com e sem aspas) -

[jaypal:~] file="/home/user/Python Tutorial.pdf"

[jaypal:~] ls $file
ls: /home/user/Python: No such file or directory
ls: Tutorial.pdf: No such file or directory

O erro apareceu para dois arquivos /home/user/Python e Tutorial.pdf . Vamos colocar um quote em nosso variable e ver o que temos.

[jaypal:~] ls "$file"
ls: /home/user/Python Tutorial.pdf: No such file or directory

Erro exibido apenas uma vez para o arquivo /home/user/Python Tutorial.pdf

Segunda pergunta:

Para remover o /path/to/file , existem várias maneiras. Meu favorito seria awk , mas mostrarei um para sed também.

sed method:

[jaypal:~/Temp] echo "$filename"
/home/user/file1.pdf /home/user/file2.odt /home/user/Python Tutorial.pdf

[jaypal:~/Temp] echo "$filename" | sed 's@/home/user/@@g'
file1.pdf file2.odt Python Tutorial.pdf

Você pode incluí-lo em seu script como este -

LISTOFFILES=$(find ~ \( -name '*.[pP][dD][fF]' -o -name '*.[oO][dD][tT]' \) | sed 's@/home/user/@@g' )

A coisa a ser lembrada aqui é que estamos fazendo substitution . Estamos substituindo /home/user/ para nothing . Se você é find ing arquivos de mais de um diretório, então você pode fazer toneladas e toneladas de substituições (yikes!) ou faça o awk way.

awk method:

awk -F"/" '{print $NF}'

Sim, é isso. Trabalharia para arquivos você find em qualquer diretório ou seus subdiretórios.

Assim, seu script pode ter isso da seguinte maneira -

LISTOFFILES=$(find ~ \( -name '*.[pP][dD][fF]' -o -name '*.[oO][dD][tT]' \) | awk -F"/" '{print $NF}')

Outras dicas e truques:

Aqui estão algumas dicas aleatórias que podem ser úteis para você -

Para find nomes de arquivos insensitively , use iname em vez do que você tem agora. Confira isso -

[jaypal:~/Temp] find . -name "j*.txt"
./jP.txt

[jaypal:~/Temp] find . -iname "j*.txt"
./jj.TXT
./jP.txt

Outras formas de remover o /path/to/file são usando basename . Eu darei um exemplo e você poderá incorporá-lo em seu roteiro conforme se sentir confortável.

[jaypal:~/Temp] filename="/usr/bin/java.pdf"

[jaypal:~/Temp] echo "$filename"
/usr/bin/java.pdf

[jaypal:~/Temp] basename "$filename"
java.pdf

Espero que isso ajude!

    
por 30.11.2011 / 06:16