Como obter os últimos N arquivos em um diretório?


15

Eu tenho muitos arquivos que são ordenados pelo nome do arquivo em um diretório. Desejo copiar os arquivos N (digamos, N = 4) finais para o meu diretório pessoal. Como devo fazer isso?

cp ./<the final 4 files> ~/


1
Em todas as respostas abaixo, pode ser necessário adicionar um "LC_ALL = ..." na frente dos comandos usando os intervalos, para que os intervalos deles usem o local correto para você (os locais podem mudar e a ordem dos caracteres pode mudar) Ex: em alguns locais, [az] pode conter todas as letras do alfabeto, exceto "Z", pois as letras são ordenadas: aAbBcC .... zZ. Existem outras variações (letras acentuadas e pedidos ainda mais exóticos Uma escolha usual é:LC_ALL=C
Olivier Dulac

Respostas:


23

Isso pode ser feito facilmente com matrizes bash / ksh93 / zsh:

a=(*)
cp -- "${a[@]: -4}" ~/

Isso funciona para todos os nomes de arquivos não ocultos, mesmo que contenham espaços, guias, novas linhas ou outros caracteres difíceis (supondo que haja pelo menos 4 arquivos não ocultos no diretório atual bash).

Como funciona

  • a=(*)

    Isso cria uma matriz acom todos os nomes de arquivo. Os nomes de arquivo retornados pelo bash são classificados em ordem alfabética. (Presumo que seja isso que você quer dizer com "ordenado pelo nome do arquivo". )

  • ${a[@]: -4}

    Isso retorna os últimos quatro elementos da matriz a(desde que a matriz contenha pelo menos 4 elementos bash).

  • cp -- "${a[@]: -4}" ~/

    Isso copia os quatro últimos nomes de arquivos para o diretório inicial.

Para copiar e renomear ao mesmo tempo

Isso copiará os últimos quatro arquivos apenas para o diretório inicial e, ao mesmo tempo, acrescentará a sequência a_ao nome do arquivo copiado:

a=(*)
for fname in "${a[@]: -4}"; do cp -- "$fname" ~/a_"$fname"; done

Copie de um diretório diferente e também renomeie

Se usarmos em a=(./some_dir/*)vez de a=(*), temos o problema do diretório sendo anexado ao nome do arquivo. Uma solução é:

a=(./some_dir/*) 
for f in "${a[@]: -4}"; do cp "$f"  ~/a_"${f##*/}"; done

Outra solução é usar um subshell e cdo diretório no subshell:

(cd ./some_dir && a=(*) && for f in "${a[@]: -4}"; do cp -- "$f" ~/a_"$f"; done) 

Quando o subshell é concluído, o shell retorna ao diretório original.

Certificando-se de que o pedido seja consistente

A pergunta pede arquivos "ordenados pelo nome do arquivo". Olivier Dulac ressalta que a ordem nos comentários varia de um local para outro. Se for importante ter resultados fixos independentemente das configurações da máquina, é melhor especificar explicitamente o código do idioma quando a matriz aestiver definida. Por exemplo:

LC_ALL=C a=(*)

Você pode descobrir em qual localidade está atualmente executando o localecomando


9

Se você estiver usando zshvocê pode colocar entre parênteses ()uma lista dos chamados qualificadores glob que selecionar os arquivos desejados. No seu caso, isso seria

cp *(On[1,4]) ~/

Aqui, Onclassifica os nomes dos arquivos em ordem alfabética na ordem inversa e [1,4]leva apenas os 4 primeiros.

Você pode tornar isso mais robusto selecionando apenas arquivos simples (excluindo diretórios, pipes etc.) com .e também anexando --ao cpcomando para tratar os arquivos cujos nomes começam -corretamente, portanto:

cp -- *(.On[1,4]) ~

Adicione o Dqualificador se você também quiser considerar arquivos ocultos (arquivos de ponto):

cp -- *(D.On[1,4]) ~

2
Ou: *([-4,-1]).
Stéphane Chazelas

2
@ StéphaneChazelas sim, vamos esclarecer para futuros leitores que classificar em ordem alfabética na direção normal ( on) é o comportamento padrão, então não há necessidade de especificar isso e, -na frente dos números, os nomes dos arquivos são contados na parte inferior.
jimmij

7

Aqui está uma solução usando comandos bash extremamente simples.

find . -maxdepth 1 -type f | sort | tail -n 4 | while read -r file; do cp "$file" ~/; done

Explicação:

find . -maxdepth 1 -type f

Localiza todos os arquivos no diretório atual.

sort

Classifica em ordem alfabética.

tail -n 4

Mostrar apenas as últimas 4 linhas.

while read -r file; do cp "$file" ~/; done

Loops sobre cada linha executando o comando copy.


6

Contanto que você considere o tipo de shell agradável, basta:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
cp "$@" /path/to/target/dir

Isso é muito semelhante à bashsolução de matriz específica oferecida, mas deve ser portátil para qualquer shell compatível com POSIX. Algumas notas sobre os dois métodos:

  1. É importante que você levar seus cpargumentos w / cp --ou você recebe uma das formas de .um ponto ou /à frente de cada nome. Se você não conseguir fazer isso, corre o risco de um -traço principal no cpprimeiro argumento que pode ser interpretado como uma opção e causar falha na operação ou gerar resultados não intencionais.

    • Mesmo trabalhando com o diretório atual como o diretório de origem, isso é feito facilmente como ... set -- ./*ou array=(./*).
  2. É importante ao usar os dois métodos para garantir que você tenha pelo menos tantos itens em sua matriz arg quanto tentar remover - eu faço isso aqui com uma expansão matemática. O shiftúnico acontece se houver pelo menos 4 itens na matriz arg - e apenas afastar os primeiros args que produzem um excedente de 4.

    • Portanto, em um set 1 2 3 4 5caso, isso mudará a 1distância, mas em umset 1 2 3 caso, nada mudará.
    • Por exemplo: a=(1 2 3); echo "${a[@]: -4}"imprime uma linha em branco.

Se você estiver copiando de um diretório para outro, poderá usar pax:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
pax -rws '|.*/|new_prefix|' "$@" /path/to/target/dir

... que aplicaria uma sedsubstituição de estilo a todos os nomes de arquivos à medida que os copia.


4

Se houver apenas arquivos e seus nomes não contiverem espaços em branco ou nova linha (e você não tiver modificado $IFS) ou caracteres glob (ou alguns caracteres não imprimíveis com algumas implementações de ls), e não começar com ., então você pode fazer isso :

cp -- $(ls | tail -n 4) ~/

Isso é chamado de substituição de comando e é realmente útil. tldp.org/LDP/abs/html/commandsub.html#CSPARENS
iyrin

Obrigado! Funciona. Existe alguma maneira de anexar rapidamente um prefixo a_a TODOS os quatro arquivos? Eu tentei echo "a_$(ls | tail -n 4)", mas ele apenas precede o primeiro arquivo.
Sibbs Gambling

@SibbsGambling Minha abordagem não é adequada para isso, mas a resposta de John1024 pode ser facilmente adaptada a isso.
Hauke ​​Laging

5
Em geral, a lssaída de análise é considerada incorreta porque normalmente falha horrivelmente em nomes de arquivos que contêm não apenas espaços, mas também guias, novas linhas ou outros caracteres válidos, mas "difíceis".
precisa saber é o seguinte

2
@HaukeLaging Mesmo que atualmente não seja um problema (porque eu não tenho nomes de fogo "complicados" no meu diretório), talvez eu tenha daqui a dois anos e, de repente, as coisas caem em meus pés ...
glglgl

1

Esta é uma solução simples do bash sem nenhum código de loop. Copie os 4 últimos arquivos do diretório atual para o destino:

$ find . -maxdepth 1 -type f |tail -n -4|xargs cp -t "$destdir"

1
Como você garante que findos arquivos sejam entregues na ordem desejada? Como você garante que os arquivos que contêm caracteres de espaço em branco (incluindo novas linhas) em seus nomes sejam manipulados corretamente?
Kusalananda
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.