Use saída da cabeça para copiar arquivos com espaços

2

Eu tenho 11 arquivos com espaços em seu nome em uma pasta e quero copiar o mais novo 10. Usei ls -t | head -n 10 para obter apenas os 10 arquivos mais recentes. Quando eu quero usar a expressão em uma instrução cp, recebo um erro de que os arquivos não puderam ser encontrados por causa do espaço no nome.

Por exemplo: cp: cannot stat ‘10’: No such file or directory para um arquivo chamado 10 abc.pdf

A instrução cp: cp $(ls -t | head -n 10) ~/...

Como faço para que isso funcione?

    
por Marius K. 23.06.2014 / 18:22

2 respostas

2

Se você estiver usando o Bash e quiser um método 100% seguro (o que, eu acho, você quer, agora que aprendeu da maneira mais difícil que você deve lidar com nomes de arquivos a sério), aqui vai:

shopt -s nullglob
while IFS= read -r file; do
    file=${file#* }
    eval "file=$file"
    cp "$file" Destination
done < <(
    for f in *; do
        printf '%s %q\n' "$(date -r "$f" +'%s')" "$f"
    done | sort -rn | head -n10
)

Vamos dar uma olhada em suas várias partes:

for f in *; do
    printf '%s %q\n' "$(date -r "$f" +'%s')" "$f"
done

Isso será impresso nos termos stdout do formulário

Timestamp Filename

em que timestamp é a data de modificação (obtida com a opção -r para date ) em segundos, uma vez que Epoch e Filename são uma versão com aspas do nome de arquivo que pode ser reutilizado como entrada de shell (consulte help printf e o especificador de formato %q ). Essas linhas são então ordenadas (numericamente, na ordem inversa) com sort , e somente as 10 primeiras são mantidas.

Isso é então alimentado no loop while . Os timestamps são removidos com a designação file=${file# *} (isso elimina tudo até e incluindo o primeiro espaço), então a linha aparentemente perigosa eval "file=$file" se livra dos caracteres de escape introduzidos por printf %q , e finalmente podemos seguramente copie o arquivo.

Provavelmente não é a melhor abordagem ou implementação, mas 100% garantido como seguro em relação a qualquer nome de arquivo possível e conclui o trabalho. No entanto, isso tratará arquivos, diretórios e etc. regulares da mesma forma. Se você deseja restringir a arquivos regulares, adicione [[ -f $f ]] || continue logo após a linha for f in *; do . Além disso, não irá considerar arquivos ocultos. Se você quiser arquivos ocultos (mas não . nem .. , é claro), adicione shopt -s dotglob .

Outra solução de Bash 100% é usar o Bash diretamente para classificar os arquivos. Aqui está uma abordagem usando um quicksort:

quicksort() {
    # quicksorts the filenames passed as positional parameters
    # wrt modification time, newest first
    # return array is quicksort_ret
    if (($#==0)); then
        quicksort_ret=()
        return
    fi
    local pivot=$1 oldest=() newest=() f
    shift
    for f; do
        if [[ $f -nt $pivot ]]; then
            newest+=( "$f" )
        else
            oldest+=( "$f" )
        fi
    done
    quicksort "${oldest[@]}"
    oldest=( "${quicksort_ret[@]}" )
    quicksort "${newest[@]}"
    quicksort_ret+=( "$pivot" "${oldest[@]}" )
}

Depois, classifique-os, mantenha os primeiros 10 e copie-os para o seu destino:

$ shopt -s nullglob
$ quicksort *
$ cp -- "${quicksort_ret[@]:0:10}" Destination

Igual ao método anterior, isso tratará arquivos, diretórios e etc. regulares da mesma forma e ignorará arquivos ocultos.

Para outra abordagem: se o seu ls tiver os argumentos i e q , você poderá usar algo ao longo destas linhas:

ls -tiq | head -n10 | cut -d ' ' -f1 | xargs -I X find -inum X -exec cp {} Destination \; -quit

Isto mostrará o inode do arquivo, e find pode executar comandos nos arquivos referenciados por seu inode.

A mesma coisa, isso também tratará diretórios, etc., não apenas arquivos regulares. Eu realmente não gosto desse, já que ele depende muito do formato de saída do ls

    
por 23.06.2014 / 19:16
0

Para qualquer grupo de arquivos em um diretório, isso sempre deixará de fora o mais antigo:

less_oldest() {
    while [ -e "$1" ] ; do
        for f do [ "$f" -ot "$1" ] && break
        done && { set -- "$@" "$1"
            shift ; continue
        } ; shift ; break
    done
    printf '///%s///\n' "$@" |
        sed "s|'"'|&"&"&|g;'"s|///|'|g"
}

Ele é totalmente portátil e imprime uma matriz de nomes de arquivos com aspas seguras, independentemente de quais caracteres eles possam retornar. No seu caso, porque você só precisa dro pone, você só precisa executá-lo uma vez como:

{   printf 'cp -t ~"/..."'
    less_oldest "${dir_of_11_files}/"*
} | sh
    
por 23.06.2014 / 19:36