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

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

Tenho 11 arquivos com espaços no nome em uma pasta e quero copiar os 10 mais recentes. Eu costumava ls -t | head -n 10obter apenas os 10 arquivos mais recentes. Quando quero usar a expressão em uma instrução cp, recebo um erro informando que os arquivos não foram encontrados devido ao espaço no nome.

Ex.: cp: cannot stat ‘10’: No such file or directorypara um arquivo chamado10 abc.pdf

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

Como faço para que isso funcione?

Responder1

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 deve lidar com nomes de arquivos com seriedade), aqui está:

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 diversas partes:

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

Isso imprimirá os termos padrão do formulário

Timestamp Filename

onde timestamp é a data de modificação (obtida com -ra opção de date) em segundos, já que Epoch e Filename é uma versão entre aspas do nome do arquivoque pode ser reutilizado como entrada do shell(veja help printfe o %qespecificador de formato). Essas linhas são então classificadas (numericamente, em ordem inversa) com sorte apenas as 10 primeiras são mantidas.

Isso é então alimentado no whileloop. Os carimbos de data e hora são removidos com a file=${file# *}atribuição (isso elimina tudo, inclusive o primeiro espaço), então a linha aparentemente perigosa eval "file=$file"elimina os caracteres de escape introduzidos por printf %qe, finalmente, podemos copiar o arquivo com segurança.

Provavelmente não é a melhor abordagem ou implementação, mas é 100% seguro e garantido em relação a quaisquer nomes de arquivos possíveis e dá conta do recado. Porém, isso tratará arquivos, diretórios regulares, etc. Se você quiser restringir a arquivos normais, adicione [[ -f $f ]] || continuelogo após a for f in *; dolinha. Além disso, não considerará arquivos ocultos. Se você quiser arquivos ocultos (mas não .nem .., é claro), adicione shopt -s dotglob.


Outra solução 100% Bash é 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[@]}" )
}

Em seguida, classifique-os, guarde os 10 primeiros e copie-os para o seu destino:

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

Da mesma forma que o método anterior, este tratará arquivos regulares, diretórios, etc. da mesma forma e ignorará os arquivos ocultos.


Para outra abordagem: se você lstiver os argumentos ie q, poderá usar algo como estes:

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

Isso mostrará o inode do arquivo e findpoderá executar comandos em arquivos referenciados por seu inode.

A mesma coisa, isso também tratará diretórios, etc., não apenas arquivos normais. Eu realmente não gosto deste, pois depende muito do lsformato de saída de ...

Responder2

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"
}

É totalmente portátil e imprime uma matriz de nomes de arquivos entre aspas seguras, independentemente dos caracteres que eles possam retornar. No seu caso, como você só precisa largar, você só precisa executá-lo uma vez, como:

{   printf 'cp -t ~"/..."'
    less_oldest "${dir_of_11_files}/"*
} | sh

informação relacionada