Script de shell para mover arquivos mais antigos?

14

Como escrevo um script para mover apenas os 20 arquivos mais antigos de uma pasta para outra? Existe uma maneira de pegar os arquivos mais antigos em uma pasta?

user11598
fonte
Incluindo ou excluindo subdiretórios? E isso deve ser feito recursivamente (em uma árvore de diretórios)?
maxschlepzig
2
Muitos (a maioria?) * Sistemas de arquivos nix não armazenam a data de criação, portanto você não pode determinar o arquivo mais antigo com certeza. Os atributos normalmente disponíveis são atime(último acesso), ctime(última alteração de permissão) e mtime(última modificação) ... por exemplo. ls -te da descoberta printf "%T" uso mtime... Parece, de acordo com este link , que minhas ext4partições são capazes de lidar com uma data de criação, mas lse finde statnão têm as opções apropriadas (ainda) ...
Peter.O

Respostas:

13

A análise da saída de nãols é confiável .

Em vez disso, use findpara localizar os arquivos e sortordená-los por carimbo de data / hora. Por exemplo:

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    # do something with $file here
done < <(find . -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

O que tudo isso está fazendo?

Primeiro, os findcomandos localizam todos os arquivos e diretórios no diretório atual ( .), mas não nos subdiretórios do diretório atual ( -maxdepth 1), depois imprimem:

  • Um carimbo de data / hora
  • Um espaço
  • O caminho relativo para o arquivo
  • Um caractere NULL

O registro de data e hora é importante. O %T@especificador de formato para -printfdivide em T, que indica "Hora da última modificação" do arquivo (mtime) e @que indica "Segundos desde 1970", incluindo segundos fracionários.

O espaço é apenas um delimitador arbitrário. O caminho completo para o arquivo é para que possamos consultá-lo mais tarde, e o caractere NULL é um terminador, pois é um caractere ilegal em um nome de arquivo e, portanto, nos permite saber com certeza que chegamos ao final do caminho para o arquivo Arquivo.

Eu incluí 2>/dev/nullpara que os arquivos que o usuário não tem permissão para acessar sejam excluídos, mas as mensagens de erro sobre eles sendo excluídos sejam suprimidas.

O resultado do findcomando é uma lista de todos os diretórios no diretório atual. A lista é canalizada para a sortqual é instruído a:

  • -z Trate NULL como o caractere terminador de linha em vez de nova linha.
  • -n Classificar numericamente

Como os segundos desde 1970 sempre aumentam, queremos o arquivo cujo carimbo de data e hora foi o menor número. O primeiro resultado de sortserá a linha que contém o menor carimbo de data e hora numerado. Tudo o que resta é extrair o nome do arquivo.

Os resultados do find, sortencanamento é passado através de substituição processo para whileonde se lê como se fosse um arquivo em stdin. whilepor sua vez, invoca readpara processar a entrada.

No contexto read, definimos a IFSvariável como zero, o que significa que o espaço em branco não será interpretado inadequadamente como um delimitador. readé contada -r, que desativa a expansão fuga, e -d $'\0', o que torna o delimitador NULL fim-de-linha, combinando a saída do nosso find, sortpipeline.

O primeiro pedaço de dados, que representa o caminho do arquivo mais antigo, precedido por seu carimbo de data e hora e um espaço, é lido na variável line. Em seguida, a substituição de parâmetro é usada com a expressão #*, que simplesmente substitui todos os caracteres desde o início da string até o primeiro espaço, incluindo o espaço, por nada. Isso retira o registro de data e hora da modificação, deixando apenas o caminho completo para o arquivo.

Nesse ponto, o nome do arquivo está armazenado $filee você pode fazer o que quiser com ele. Quando você terminar de fazer algo com $filea whileinstrução, o loop readserá executado e o comando será executado novamente, extraindo o próximo pedaço e o próximo nome do arquivo.

Não existe uma maneira mais simples?

Não. Maneiras mais simples são problemáticas.

Se você usar o ls -tpipe para headou tail(ou qualquer outra coisa ), interromperá os arquivos com novas linhas nos nomes dos arquivos. Se você, mv $(anything)então, arquivos com espaço em branco no nome causarão falhas. Se você, mv "$(anything)"então, os arquivos com novas linhas à direita no nome causarão interrupções. Se você readnão, -d $'\0'então você quebrará arquivos com espaços em branco em seus nomes.

Talvez em casos específicos, você tenha certeza de que uma maneira mais simples é suficiente, mas nunca deve escrever suposições como essa nos scripts, se puder evitar fazê-lo.

Solução

#!/usr/bin/env bash

# move to the first argument
dest="$1"

# move from the second argument or .
source="${2-.}"

# move the file count in the third argument or 20
limit="${3-20}"

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    echo mv "$file" "$dest"
    let limit-=1
    [[ $limit -le 0 ]] && break
done < <(find "$source" -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

Ligue como:

move-oldest /mnt/backup/ /var/log/foo/ 20

Para mover os 20 arquivos mais antigos de /var/log/foo/para /mnt/backup/.

Observe que estou incluindo arquivos e diretórios. Para arquivos, adicione apenas -type fa findchamada.

obrigado

Obrigado ao enzotib e ao Павел Танков pelas melhorias nesta resposta.

Sorpigal
fonte
A classificação não deve ser usada -n. Pelo menos na minha versão, ele não classifica os números decimais corretamente. Você precisa remover o ponto na data ou usar -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz, datas ISO ou outra coisa.
L0b0
@ l0b0: Esta limitação é conhecida por mim. Presumo que seja suficiente não exigir esse nível de granularidade (isto é, a classificação além do .deve ser irrelevante para você.) Seria mais claro dizer sort -z -n -t. -k1.
precisa saber é o seguinte
@ l0b0: Sua solução exibe o mesmo bug, independentemente: %TStambém mostra uma "parte fracionária" que estaria no formato 00.0000000000, para que você também perca granularidade. O GNU recente sortpode resolver esse problema usando -Vuma "classificação de versão", que manipulará esse tipo de ponto flutuante conforme o esperado.
Sorpigal
Não, porque eu faço uma classificação de sequência em "AAAA-MM-DDThh: mm: ss" em vez de uma classificação numérica. Cordas tipo não se preocupa com decimais, assim que deve funcionar até o ano 10000 :)
l0b0
@ l0b0: Uma ordenação de string %T@também funcionaria, pois é preenchida com zero.
precisa saber é o seguinte
4

É mais fácil no zsh, onde você pode usar o Om qualificador glob para classificar correspondências por data (a mais antiga primeiro) e o [1,20]qualificador para manter apenas as 20 primeiras correspondências:

mv -- *(Om[1,20]) target/

Adicione o Dqualificador se você quiser incluir arquivos de ponto também. Adicione .se você deseja corresponder apenas arquivos regulares e não diretórios.

Se você não possui o zsh, aqui está um liner de Perl (você pode fazê-lo em menos de 80 caracteres, mas com uma despesa adicional em clareza):

perl -e '@files = sort {-M $b <=> -M $a} glob("*"); foreach (@files[0..1]) {rename $_, "target/$_" or die "$_: $!"}'

Com apenas ferramentas POSIX ou até bash ou ksh, classificar arquivos por data é uma dor. Você pode fazer isso facilmente ls, mas analisar a saída de lsé problemático; portanto, isso só funciona se os nomes de arquivos contiverem apenas caracteres imprimíveis que não sejam novas linhas.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Gilles 'SO- parar de ser mau'
fonte
4

Combine a ls -tsaída com tailou head.

Exemplo simples, que funciona apenas se todos os nomes de arquivos contiverem apenas caracteres imprimíveis, exceto espaços em branco e \[*?:

 mv $(ls -1tr | head -20) other_folder
ktf
fonte
1
Adicione a opção -A ao ls:ls -1Atr
Arcege 15/10
1
-1, perigoso. Aqui, deixe-me elaborar um exemplo: touch $'foo\n*'. O que acontece se você executar o mv "$ (ls)" com esse arquivo sentado lá?
Sorpigal 16/01
1
@Sorpigal Sério? É uma espécie de fraco para dizer "Deixe-me vir para cima com um exemplo que você disse especificamente não vai funcionar Hey olhar, ele não funciona."
Michael Mrozek
1
@Sorpigal Não é uma má idéia, funciona em 99% dos casos. A resposta é "se você possui arquivos com nomes normais, isso funciona. Se você é uma pessoa insana que incorpora novas linhas em seus nomes de arquivos, isso não acontece". Isso é completamente correta
Michael Mrozek
1
@MichaelMrozek: É uma má idéia e é ruim porque às vezes falha. Se você tem a opção de fazer o que falha às vezes e o que não falha, deve optar pela opção que não funciona (e a que é ruim). Faça o que quiser de maneira interativa, mas em um arquivo de script e, ao dar conselhos, faça-o corretamente.
Sorpigal
2

Você pode usar o GNU find para isso:

find -maxdepth 1 -type f -printf '%T@ %p\n' \
  | sort -k1,1 -g | head -20 | sed 's/^[0-9.]\+ //' \
  | xargs echo mv -t dest_dir

Onde find imprime o tempo de modificação (em segundos de 1970) e o nome de cada arquivo do diretório atual, a saída é classificada de acordo com o primeiro campo, os 20 mais antigos são filtrados e movidos para dest_dir. Remova o echose você testou a linha de comando.

maxschlepzig
fonte
2

Ninguém (ainda) postou um exemplo do bash que atende a caracteres de nova linha incorporados (qualquer coisa incorporada) no nome do arquivo, então aqui está um. Move os 3 arquivos regulares mais antigos (mdate)

move=3
find . -maxdepth 1 -type f -name '*' \
 -printf "%T@\t%p\0" |sort -znk1 | { 
  while IFS= read -d $'\0' -r file; do
      printf "%s\0" "${file#*$'\t'}"
      ((--move==0)) && break
  done } |xargs -0 mv -t dest

Este é o trecho de dados de teste

# make test files with names containing \n, \t and "  "
rm -f '('?[1-4]'  |?)'
for f in $'(\n'{1..4}$'  |\t)' ;do sleep .1; echo >"$f" ;done
touch -d "1970-01-01" $'(\n4  |\t)'
ls -ltr '('?[1-4]'  |'?')'; echo
mkdir -p dest

Aqui está o trecho dos resultados da verificação

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
fonte
+1, única resposta útil antes de mine (e é sempre bom ter dados de teste.)
Sorpigal
0

É mais fácil fazer com o GNU find. Uso-o todos os dias no meu DVR Linux para excluir gravações do meu sistema de vigilância por vídeo com mais de um dia.

Aqui está a sintaxe:

find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;

Lembre-se de que finddefine um dia como 24 horas a partir do momento da execução. Portanto, os arquivos modificados pela última vez às 23:00 não serão excluídos à 01:00.

Você pode até combinar findcom cron, para que as exclusões possam ser agendadas automaticamente executando o seguinte comando como root:

crontab -e << EOF
@daily /usr/bin/find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;
EOF

Você sempre pode obter mais informações findconsultando sua página de manual:

man find
Jonathan Frank
fonte
0

Como as outras respostas não se encaixam no meu objetivo e no das perguntas, esse shell é testado no CentOS 7:

oldestDir=$(find /yourPath/* -maxdepth 0 -type d -printf '%T+ %p\n' | sort | head -n 1 | tr -s ' ' | cut -d ' ' -f 2)
echo "$oldestDir"
rm -rf "$oldestDir"
Spektakulatius
fonte