Convertendo 'para file' em 'find' para que meu script possa ser aplicado recursivamente

7

Eu tenho trabalhado no meu primeiro script bash por algum tempo agora (eu acabei de começar o UNIX esta semana).

Eu tenho essa ideia de executar um script bash para verificar algumas condições e usar ffmpeg para converter todos os vídeos no meu diretório de qualquer formato para .mkv e está funcionando muito bem!

O problema é que eu não sabia que um loop for file in não funciona recursivamente ( link )

Mas eu mal entendo "piping" e estou ansioso para ver um exemplo e esclarecer algumas incertezas.

Eu tenho este cenário em mente que acho que me ajudaria muito a entender.

Suponha que eu tenha este trecho de script:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

O que ele faz é, para o diretório atual, procurar por *.mkv *avi *mp4 *flv *ogg *mov e, em seguida, declarar a saída para ter sua extensão como .mkv , depois apagar o arquivo original, então a saída deve ser salva na mesma pasta o vídeo original está em.

  1. Como posso converter isso para executar recursivamente? se eu usar find , onde declarar a variável $file ? E onde você deve declarar $target ? Todos os find são apenas one-liners? Eu realmente preciso passar o arquivo para uma variável $file , porque ainda precisarei executar a verificação de condição.

  2. E, supondo que (1) seja bem-sucedido, como ter certeza de que o requisito "a saída deve ser salva na mesma pasta em que o vídeo original está" está satisfeito?

Sinto muito por investigações muito longas. Mas estou realmente disposto a aprender. Espero que alguém possa me dar um exemplo ou trecho para que eu possa começar em algum lugar.

    
por The Wolf 16.04.2015 / 17:52

4 respostas

5

Você tem esse código:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

que é executado no diretório atual. Para transformá-lo em um processo recursivo, você tem algumas opções. O mais fácil (IMO) é usar find como você sugeriu. A sintaxe para find é muito "un-UNIX-like", mas o princípio aqui é que cada argumento pode ser aplicado com as condições AND ou OR. Aqui, vamos dizer " Se este nome-de-arquivo corresponder a OU-que-nome do arquivo corresponde Então imprima-o ". Os padrões de nome de arquivo são citados para que o shell não possa obtê-los (lembre-se de que o shell é responsável por expandir todos os padrões não citados, portanto, se você tivesse um padrão sem aspas de *.mp4 e tivesse janeeyre.mp4 em seu diretório atual , o shell substituiria *.mp4 pela correspondência e find veria -name janeeyre.mp4 em vez do desejado -name *.mp4 ; fica pior se *.mp4 corresponder a vários nomes ...). Os colchetes são prefixados com \ também para evitar que o shell tente executá-los como marcadores de subshell (poderíamos citar os colchetes, se preferir: '(' ).

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print

A saída disso precisa ser alimentada na entrada de um loop while que processa cada arquivo por vez:

while IFS= read file    ## IFS= prevents "read" stripping whitespace
do
    target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

Agora, tudo o que resta é unir as duas partes com um pipe | , de modo que a saída do find se torne a entrada do loop while .

Enquanto você está testando este código, eu recomendo que você prefixar tanto ffmpeg quanto rm com echo para que você possa ver o que seria executado - e com quais caminhos.

Aqui está o resultado final, incluindo as declarações echo que recomendo testar:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print |
    while IFS= read file    ## IFS= prevents "read" stripping whitespace
        do
            target="${file%.*}.mkv"
            echo ffmpeg -i "$file" "$target" && echo rm -rf "$file"
        done
    
por 16.04.2015 / 18:15
6

Com o POSIX, ache:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \
          -name '*ogg' -o -name '*mov' \) -exec sh -c '
  for file do
    target="${file%.*}.mkv"
    echo ffmpeg -i "$file" "$target"
  done' sh {} +

Substitua echo pelo comando que você deseja usar.

Se você encontrou o GNU find ou o BSD, você pode usar -regex :

find . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'
    
por 16.04.2015 / 18:10
2

Exemplo de fragmento sem tubulação (assume que você está dando o caminho como argumento):

#!/bin/bash

backup_dir=/backup/

OIFS="$IFS"
IFS=$'\n'

files="$(find "$1" -type f -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv')"

for f in $files; do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir" 
done

IFS="$OIFS"

O shell lê a variável IFS , que é definida como ( space , tab , newline ) por padrão. Em seguida, ele olha para cada caractere na saída de find . Então, se ele encontrar space , ele acha que é o final do nome do arquivo (arquivos contendo espaços, por exemplo, "Sin City.avi", serão tratados como dois arquivos "Sin" e "City.avi"). Então, com o IFS = $ '\ n' estamos dizendo para dividir a entrada em newlines . E finalmente restauramos o antigo (padrão) IFS que é salvo na variável $OIFS .
Ou como sugerido nos comentários pode ser melhor abordagem poderia ser:

#!/bin/bash

backup_dir=/backup/

find "$1" -type f \( -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv' \) -print0 | while IFS= read -r -d '' f
do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir"
done
    
por 16.04.2015 / 18:11
1

Bem-vindo ao Unix:)

Para responder a algumas de suas perguntas menores, as respostas para a pergunta principal não foram:

O shell script certamente tem algumas arestas, pois muitas coisas quebram em nomes de arquivos com espaços. E quase tudo quebra em nomes de arquivos com novas linhas (felizmente, ninguém faz isso de propósito). Nomes de arquivos contendo caracteres glob como [ , ] e * às vezes também são um problema. Às vezes, não vale a pena escrever código shell de difícil leitura até os padrões do BashGuide da wooledge , para uso próprio ou para uma única vez em que você sabe que seus nomes de arquivos não são estranhos.

where to declare the variable:

As variáveis do shell não precisam ser declaradas. No bash, você pode usar shopt -o nounset para torná-lo um erro para referenciar e unSET a variável, mas isso não é exatamente a mesma coisa que não declarado. Desativar uma variável pode ser útil. Em uma função de shell, é uma boa prática declarar todos os seus temporários com local foo bar baz; , para que você não crie lixo no ambiente do shell com variáveis, ou pior, pise na variável do chamador com o mesmo nome.

I barely understand "piping".

Ao trabalhar com o shell, muita passagem de dados acontece imprimindo os dados no stdout. Pipes enviam esses dados para outro programa, que os lê no stdin (e geralmente imprime algo no stdout). Você pode capturar a saída em variáveis do shell usando a substituição de comandos, $() . por exemplo. %código%. (isso vai quebrar em nomes de arquivos com espaços neles, como um monte de código de shell, se você não for cuidadoso. Use for i in $( locate foo | grep bar );do echo "$i"; done se você quiser escrever scripts que são confiáveis.) read imprime, locate lê e imprime e o shell lê a saída de grep . (O shell obtém suas mãos na saída de grep iniciando grep com sua saída conectada ao lado de entrada de um tubo que o shell criou. O shell lê o lado de saída do tubo.)

Um pipe é apenas uma forma de os programas funcionarem como se estivessem gravando em um arquivo, mas na verdade estão escrevendo em um buffer pequeno. Uma leitura de processo de um canal terá seu retorno de chamada do sistema grep quando houver dados disponíveis, o que só acontece quando algo é gravado na outra extremidade do canal.

Os read(2) , | e alguns outros elementos de sintaxe do shell são como você diz ao shell como configurar os programas de conexão de encanamento entre si e com o shell.

É fácil aprender maus idiomas para programação de shell, porque muitas das coisas óbvias e formas antigas de fazer coisas escondem armadilhas que quebram nomes estranhos de arquivos. Veja por exemplo o link .

É melhor aprender maneiras seguras de criar scripts desde o início, em vez de aprender maneiras de quebrar nomes estranhos, contanto que eles não sejam muito desajeitados para digitar. :)

Muitos utilitários GNU têm uma opção -0, para usar ASCII NUL (o byte 0, não pode estar presente em nomes de arquivos ou texto) como um separador de registro. Isso permite que você canalize dados entre $() e find , por exemplo, sem qualquer possibilidade de ter uma "linha" de saída de localização transformada em várias linhas de entrada de classificação. Isso acaba não sendo muito útil quando você deseja obter os dados em uma variável do shell, porque o bash não tem como ler as linhas sort -delimited. (Eu não acho que seja um valor válido para o IFS.)

De qualquer forma, evitar que o shell trate dados como código é a razão para sempre citar tudo o que você pode, a menos que você realmente queira dividir as palavras. Se você quiser fazer seu cérebro doer olhando código de shell complexo, basta olhar para o código bash-completion. (Ele manipula a conclusão programável que faz coisas inteligentes, como concluir ls --colo => --color ou apenas preencher arquivos * .zip para descompactar.) set -x e a guia de acesso: P. (defina + x para desativar o rastreio de execução.)

re: seu loop for: com *.mkv como um dos seus padrões, você terá source = dest para esses arquivos de entrada. ffmpeg solicitará que você sobrescreva o arquivo de saída para cada um.

Além disso, você realmente precisa transcodificar o áudio? -c:a copy pode ser uma boa ideia. Taxa de bits de vídeo geralmente é um grande negócio. E você pode querer usar -preset slow (ou slower , ou mesmo veryslow ) para obter mais qualidade por taxa de bits, ao custo de mais uso da CPU. Há também -crf 20 (padrão 23). link . Você provavelmente já sabia disso, e deixou de fora porque não era relevante para o bash scripting, mas apenas no caso de ...: P -c:v libx264 é o padrão quando saindo para o mkv, então é bom.

    
por 16.04.2015 / 23:47