Como encontrar o próximo sufixo de arquivo disponível (file_a.txt file_b.txt etc)

6

Meu sistema cria um novo arquivo de texto toda vez que um determinado evento ocorre.
Os arquivos devem ser nomeados file_a.txt file_b.txt file_c.txt etc.

Em um script de shell Bash, como descobrir qual nome de arquivo deve ser usado em seguida?

Por exemplo, se file_a.txt e file_b.txt existirem, mas não file_c.txt , o próximo nome de arquivo disponível será file_c.txt .

Esse pode ser um número, se for mais fácil.
Comecei a projetar um algoritmo, mas provavelmente há uma maneira mais fácil?

Observação: os arquivos são removidos a cada dia, portanto, a probabilidade de atingir z é zero. Portanto, depois de z , qualquer estratégia é aceitável: aa , usando números inteiros ou até usando UUIDs.

    
por Nicolas Raoul 22.06.2015 / 13:51

5 respostas

1

Aqui está uma maneira crua (sem verificação de erros) para fazer isso puramente no bash:

#helper function to convert a number to the corresponding character
chr() {
  [ "$1" -lt 256 ] || return 1
  printf "\$(printf '%03o' "$1")"
}

#helper function to convert a character to the corresponding integer
ord() {
  LC_CTYPE=C printf '%d' "'$1"
}

#increment file
fn_incr(){

  #first split the argument into its constituent parts

  local fn prefix letter_and_suffix letter suffix next_letter
  fn=$1
  prefix=${fn%_*}
  letter_and_suffix=${fn#${prefix}_}
  letter=${letter_and_suffix%%.*}
  suffix=${letter_and_suffix#*.}

  #increment the letter part
  next_letter=$(chr $(($(ord "$letter") + 1)))

  #reassemble
  echo "${prefix}_${next_letter}.${suffix}"
}

Exemplo de uso:

fn_incr foo_bar_A.min.js
#=> foo_bar_B.min.js

Fazê-lo no bash com índices de letras múltiplas exigiria um código mais longo. Você sempre pode fazer isso em um executável diferente, mas você pode querer incrementar os nomes de arquivos em lotes, ou então a sobrecarga de inicialização do executável pode atrasar seu programa de maneira inaceitável. Tudo depende do seu caso de uso.

Usar inteiros antigos simples pode ser a melhor escolha aqui, já que você não terá que gerenciar manualmente como o 9 ++ transborda para a esquerda.

chr() e ord() foram descaradamente roubados de Script Bash para obter valores ASCII para o alfabeto

    
por 22.06.2015 / 14:52
1

Se você não se importa, no Linux (mais precisamente, com GNU coreutils ):

tmpfile=$(TMPDIR=. mktemp --backup=numbered)
… # create the content
mv --backup=numbered -- "$tmpfile" file.txt

Isso usa o esquema de nomes de backup da GNU : file.txt , file.txt.~1~ , file.txt.~2~ ,…

Outra forma relativamente compacta, com números que podem ser colocados em um local mais conveniente, é aproveitar qualificadores glob do zsh para encontrar o arquivo mais recente e calcular o próximo arquivo com alguns expansão de parâmetros .

latest=(file_<->.txt(n[-1]))
if ((#latest == 0)); then
  next=file_1.txt
else
  latest=$latest[1]
  next=${${latest%.*}%%<->}$((${${latest%.*}##*[^0-9]}+1)).${latest##*.}
fi
mv -- $tmpfile $next

Com qualquer shell POSIX, você terá mais facilidade se usar um número com zeros à esquerda. Tome cuidado para que um literal inteiro com um zero à esquerda seja analisado como octal.

move_to_next () {
  shift $(($#-2))
  case ${1%.*} in
    *\*) mv -- "$2" file_0001.txt;;
    *)
      set -- "${1%.*}" "${1##*.}" "$2"
      set -- "${1%_*}" "$((1${1##*_}+1)).$2" "$3";;
      mv -- "$3" "${1}_${2#1}";;
  esac
}
move_to_next file_[0-9]*.txt "$tmpfile"
    
por 23.06.2015 / 02:08
0

Tente:

perl -le 'print $ARGV[-1] =~ s/[\da-zA-Z]+(?=\.)/++($i=$&)/er' file*.txt

Isso fornecerá file_10.txt após file_9.txt , file_g.txt após file_f.txt , file_aa.txt após file_z.txt , mas não file_ab.txt após file_aa.txt ou file_11.txt após file_10.txt porque file* shell glob classificará file_z.txt após file_aa.txt e file_9.txt após file_10.txt .

Esse último você pode contornar com zsh usando file*.txt(n) em vez de file*.txt .

Ou você pode definir uma ordem de classificação numérica em zsh , com base naqueles aa , abc sendo reconhecidos como números na base 36:

b36() REPLY=$((36#${${REPLY:r}#*_}))
perl ... file_*.txt(no+b36)

(note que o pedido é ... 7, 8, 9, a / A, b / B ..., z / Z, 10, 11 ... então você não quer misturar file_123.txt e file_aa.txt ).

    
por 22.06.2015 / 16:32
0

Isto produz o próximo nome de arquivo seqüencial. O ID pode ter qualquer comprimento e pode ser numérico ou alfabético. Este exemplo é preparado para usar um ID alfa, sendo o primeiro ID a

pfix='file_'
sfix='.txt' 
idbase=a        # 1st alpha id when no files exist - use a decimal number for numeric id's 
idpatt='[a-z]'  # alpha glob pattern - use '[0-9]' for numeric id's
shopt -s extglob
idhigh=$( ls -1 "$pfix"+($idpatt)"$sfix" 2>/dev/null |
             awk  'length>=l{ l=length; 
                   id=substr($0,'${#pfix}'+1,length-'${#pfix}-${#sfix}') } 
                   END{ print id }' )
[[ -z $idhigh ]] && echo "$pfix$idbase$sfix" ||
   perl -E '$x="'$idhigh'"; $x++; print "'${pfix}'"."$x"."'${sfix}'\n"'

Se não houver nenhum arquivo correspondente, a saída será:

file_a.txt

Se o arquivo de correspondência mais alto for file_zzz.txt , a saída será:

file_aaaa.txt
    
por 22.06.2015 / 15:32
0

Esse problema pode ser solucionado com facilidade em python usando vários blocos de construção do iterador disponíveis no módulo itertools

from os.path import isfile
from string import ascii_lowercase
from itertools import dropwhile, imap, chain, product, repeat, count
next(dropwhile(isfile, imap('file_{}.txt'.format, 
    imap(''.join, chain.from_iterable(
    product(ascii_lowercase, repeat=x) for x in count(1))))))
    
por 23.06.2015 / 22:29