¿Cómo obtener los últimos N archivos en un directorio?

15

Tengo muchos archivos que están ordenados por nombre de archivo en un directorio. Deseo copiar los archivos finales N (por ejemplo, N = 4) a mi directorio de inicio. ¿Cómo debería hacerlo?

cp ./<the final 4 files> ~/

Sibbs Gambling
fuente
1
En todas las respuestas a continuación, es posible que deba agregar un "LC_ALL = ..." delante de los comandos que usan los rangos, de modo que sus rangos usen la configuración regional adecuada para usted (las configuraciones regionales pueden cambiar y el orden de los caracteres puede cambiar) mucho. Ej., en algunas localidades, [az] puede contener todas las letras del alfabeto excepto "Z", ya que las letras están ordenadas: aAbBcC .... zZ. Existen otras variaciones (letras acentuadas e incluso ordenamientos más exóticos). Una opción habitual es:LC_ALL=C
Olivier Dulac

Respuestas:

23

Esto se puede hacer fácilmente con las matrices bash / ksh93 / zsh:

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

Esto funciona para todos los nombres de archivos no ocultos, incluso si contienen espacios, pestañas, líneas nuevas u otros caracteres difíciles (suponiendo que haya al menos 4 archivos no ocultos en el directorio actual con bash).

Cómo funciona

  • a=(*)

    Esto crea una matriz acon todos los nombres de archivo. Los nombres de archivo devueltos por bash están ordenados alfabéticamente. (Supongo que esto es lo que quiere decir con "ordenado por nombre de archivo" ).

  • ${a[@]: -4}

    Esto devuelve los últimos cuatro elementos de la matriz a(siempre que la matriz contenga al menos 4 elementos con bash).

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

    Esto copia los últimos cuatro nombres de archivo a su directorio de inicio.

Para copiar y renombrar al mismo tiempo

Esto copiará los últimos cuatro archivos solo al directorio de inicio y, al mismo tiempo, antepondrá la cadena a_al nombre del archivo copiado:

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

Copie desde un directorio diferente y también cambie el nombre

Si usamos en a=(./some_dir/*)lugar de a=(*), entonces tenemos el problema de que el directorio se adjunta al nombre del archivo. Una solución es:

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

Otra solución es usar una subshell y cdel directorio en la subshell:

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

Cuando se completa el subshell, el shell nos devuelve al directorio original.

Asegurarse de que el pedido sea consistente

La pregunta pide archivos "ordenados por nombre de archivo". Ese orden, señala Olivier Dulac en los comentarios, variará de un lugar a otro. Si es importante tener resultados fijos independientes de la configuración de la máquina, lo mejor es especificar la configuración regional explícitamente cuando ase define la matriz . Por ejemplo:

LC_ALL=C a=(*)

Puede averiguar en qué localidad se encuentra actualmente ejecutando el localecomando.

John1024
fuente
9

Si está utilizando zsh, puede incluir entre paréntesis ()una lista de los llamados calificadores globales que seleccionan los archivos deseados. En su caso, eso sería

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

Aquí Onordena los nombres de los archivos alfabéticamente en orden inverso y [1,4]solo toma los primeros 4 de ellos.

Puede hacer esto más robusto seleccionando solo archivos simples (excluyendo directorios, tuberías, etc.) con ., y también agregando --un cpcomando para tratar los archivos cuyos nombres comienzan -correctamente, por lo que:

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

Agregue el Dcalificador si también desea considerar archivos ocultos (archivos de puntos):

cp -- *(D.On[1,4]) ~
jimmij
fuente
2
O: *([-4,-1]).
Stéphane Chazelas
2
@ StéphaneChazelas sí, dejemos en claro para los futuros lectores que ordenar alfabéticamente en la dirección normal ( on) es el comportamiento predeterminado, por lo que no es necesario especificar esto, y -frente a los números se cuentan los nombres de los archivos desde la parte inferior.
jimmij
7

Aquí hay una solución que usa comandos bash extremadamente simples.

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

Explicación:

find . -maxdepth 1 -type f

Encuentra todos los archivos en el directorio actual.

sort

Se ordena alfabéticamente.

tail -n 4

Mostrar solo las últimas 4 líneas.

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

Recorre cada línea que ejecuta el comando copiar.

gswong
fuente
6

Siempre que encuentre que el tipo de shell es agradable, puede hacer lo siguiente:

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

Esto es muy similar a la bashsolución de matriz específica ofrecida, pero debería ser portátil a cualquier shell compatible con POSIX. Algunas notas sobre ambos métodos:

  1. Es importante que usted dirige sus cpargumentos w / cp --o se obtiene uno de entre .un punto o /en la cabecera de cada nombre. Si no lo hace, se arriesga a un -guión principal en cpel primer argumento que puede interpretarse como una opción y provocar que la operación falle o que, de lo contrario, arroje resultados no deseados.

    • Incluso si trabaja con el directorio actual como directorio de origen, esto se hace fácilmente como ... set -- ./*o array=(./*).
  2. Es importante cuando se usan ambos métodos para garantizar que tenga al menos tantos elementos en su matriz arg como intente eliminar; lo hago aquí con una expansión matemática. Lo shiftúnico ocurre si hay al menos 4 elementos en la matriz arg, y luego solo aleja esos primeros argumentos que generan un excedente de 4.

    • Entonces, en un set 1 2 3 4 5caso, cambiará la 1distancia, pero en un set 1 2 3caso, no cambiará nada.
    • Por ejemplo: a=(1 2 3); echo "${a[@]: -4}"imprime una línea en blanco.

Si está copiando de un directorio a otro, puede usar pax:

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

... que aplicaría una sedsustitución de estilo a todos los nombres de archivos a medida que los copia.

mikeserv
fuente
4

Si solo hay archivos y sus nombres no contienen espacios en blanco o nueva línea (y no ha modificado $IFS) o caracteres globales (o algunos caracteres no imprimibles con algunas implementaciones de ls), y no comience con ., entonces puede hacer esto :

cp -- $(ls | tail -n 4) ~/
Hauke ​​Laging
fuente
Esto se llama sustitución de comando y es realmente útil. tldp.org/LDP/abs/html/commandsub.html#CSPARENS
iyrin
¡Gracias! Funciona. ¿Hay alguna posibilidad de anteponer rápidamente un prefijo a_a los cuatro archivos? Lo intenté echo "a_$(ls | tail -n 4)", pero solo antepone el primer archivo.
Sibbs Gambling
@SibbsGambling Mi enfoque no es adecuado para eso, pero la respuesta de John1024 se puede adaptar fácilmente a eso.
Hauke ​​Laging
55
En general, la lssalida de análisis se considera una forma incorrecta porque generalmente falla horriblemente en los nombres de archivo que contienen no solo espacios, sino también pestañas, líneas nuevas u otros caracteres válidos pero "difíciles".
HBruijn
2
@HaukeLaging Incluso si actualmente no es un problema (porque no tengo nombres de fuego "complicados" en mi directorio), podría tenerlo en 2 años, y de repente las cosas se me caen ...
glglgl
1

Esta es una solución bash simple sin ningún código de bucle. Copie los últimos 4 archivos del directorio actual al destino:

$ find . -maxdepth 1 -type f |tail -n -4|xargs cp -t "$destdir"
yasin_alm
fuente
1
¿Cómo se asegura de que findle da los archivos en el orden deseado? ¿Cómo se asegura de que los archivos que contienen caracteres de espacio en blanco (incluidas las nuevas líneas) en sus nombres se manejan correctamente?
Kusalananda