Como encontrar os caminhos comuns em uma lista de caminhos/arquivos

Como encontrar os caminhos comuns em uma lista de caminhos/arquivos

Prelúdio:

Dada uma entrada classificada de uma lista de caminhos/arquivos, como encontrar seus caminhos comuns?

Traduzindo para o termo técnico, se estiver alimentando a entrada classificada do stdin, como escolher o prefixo adequado mais curto do stdin?

Aqui o "prefixo" tem o significado normal, por exemplo, a string 'abcde' tem um prefixo 'abc'. Aqui está meu exemplo de entrada

$ echo -e '/home/dave\n/home/dave/file1\n/home/dave/sub2/file2'
/home/dave
/home/dave/file1
/home/dave/sub2/file2

Este é um exemplo pararemover prefixo adequado sucessivodo stdin, usando o comando sed:

$ echo -e '/home/dave\n/home/dave/file1\n/home/dave/sub2/file2' | sed "N; /^\(.*\)\n\1\//D; P; D" 
/home/dave/file1
/home/dave/sub2/file2

Pergunta:

Minha pergunta é comopreservar o prefixo adequadoem vez disso, e remova todas as linhas que possuem esse prefixo. Seno ambos /home/dave/file1e /home/dave/sub2/file2tem o prefixo /home/dave, o /home/daveserá preservado enquanto os outros dois não. Ou seja, ele fará o oposto completo do sedcomando acima.

Mais informações:

  • A entrada já estaria classificada
  • Se eu tivesse /home/dave /home/dave/file1 /home/phil /home/phil/file2( echo -e '/home/dave\n/home/dave/file1\n/home/dave/sub2/file2\n/home/phil\n/home/phil/file2'), esperaria /home/davee /home/philseria a resposta.

Aplicativo:

Tenho dois volumes de disco contendo conteúdo semelhante. Quero copiar o que está na v1, mas faltando na v2, para outro volume de disco, v3. Usando find, sort, e comm, consigo obter uma lista do que copiar, mas preciso limpar ainda mais essa lista. Ou seja, enquanto tiver /home/davena lista, não preciso dos outros dois.

Obrigado!

Responder1

Esta resposta usa Python. Como o OP queria remover os diretórios cobertos por seus pais, como eu considerava uma possibilidade, comecei a escrever um programa diferente para remover coberturas:

Exemplo:

$ echo -e '/home/dave\n/home/dave/file1\n/home/dave/sub2/file2\n/home/phil\n/home/phil/file1' | removecoverings 
/home/phil
/home/dave

Código do removecoveringscomando:

#!/usr/bin/env python2

import sys

def list_startswith(a, b):
    if not len(a) >= len(b):
        return False
    return all(x == y for x,y in zip(a[:len(b)],b))

def removecoverings(it):
    g = list(it)
    g.sort(key=lambda v: len(v.split('/')), reverse=True)
    o = []
    while g:
        c = g.pop()
        d = []
        for v in g:
            if list_startswith(v.split('/'), c.split('/')):
                d.append(v)
        for v in d:
            g.remove(v)
        o.append(c)
    return o

for o in removecoverings(l.strip() for l in sys.stdin.readlines()):
    print o

Esta resposta usa Python. Ele também faz um prefixo comum em termos de componente, em vez de string. Melhor para caminhos como o prefixo comum de /ex/amplee não /exa/mpledeveria ser . Isto pressupõe que o que se deseja é o máximo prefixo comum e não uma lista de prefixos com suas coberturas removidas. Se você tem e espera em vez de . Esta não é a resposta que você estaria procurando.//ex/home/dave /home/dave/file1 /home/phil /home/phil/file2/home/dave /home/phil/home

Exemplo:

$ echo -e '/home/dave\n/home/dave/file1\n/home/dave/sub2/file2' | commonprefix 
/home/dave

Código do commonprefixcomando:

#!/usr/bin/env python2

import sys

def commonprefix(l):
    # this unlike the os.path.commonprefix version
    # always returns path prefixes as it compares
    # path component wise
    cp = []
    ls = [p.split('/') for p in l]
    ml = min( len(p) for p in ls )

    for i in range(ml):

        s = set( p[i] for p in ls )         
        if len(s) != 1:
            break

        cp.append(s.pop())

    return '/'.join(cp)

print commonprefix(l.strip() for l in sys.stdin.readlines())

Responder2

Dado que a entrada está classificada, o pseudocódigo seria:

$seen = last_line;
if current_line begins exactly as $seen then next
else { output current_line; $seen = current_line }

Traduzindo para código Perl (Sim, Perl, a linguagem de script mais bonita de todas):

perl -e '
my $l = "\n";
while (<>) {
    if ($_ !~ /^\Q$l/) {
        print;
        chomp;
        $l = $_;
    }
}
'

Crédito:Ben Bacarisse @bsb.me.uk, de comp.lang.perl.misc. Obrigado Ben, funciona muito bem!

Responder3

E a versão única da resposta do xpt. Novamente, assumindo entrada classificada:

perl -lne 'BEGIN { $l="\n"; }; if ($_ !~ /^\Q$l/) { print $_; $l = $_; }'

Execute na entrada de exemplo

/home/dave
/home/dave/file1
/home/dave/sub2/file2
/home/phil
/home/phil/file2 

usando

echo -e '/home/dave\n/home/dave/file1\n/home/dave/sub2/file2\n/home/phil\n/home/phil/file2' | perl -lne 'BEGIN { $l="\n"; }; if ($_ !~ /^\Q$l/) { print $_; $l = $_; }'

/home/dave
/home/phil

A mágica está nos argumentos de linha de comando para perl: -enos permite fornecer um script na linha de comando, -nitera sobre as linhas do arquivo (colocando cada linha em $_) e -llida com novas linhas para nós.

O script funciona usando lpara rastrear o último prefixo visto. O BEGINbloco é executado antes da primeira linha ser lida e inicializa a variável com uma string que não será vista (sem novas linhas). A condicional é executada em cada linha do arquivo (mantida por $_). A condicional é executada em todas as linhas do arquivo e diz "se a linha não tiver o valor atual de lcomo prefixo, imprima a linha e salve-a como o valor de l." Devido aos argumentos da linha de comando, isso é essencialmente idêntico ao outro script.

O problema é que ambos os scripts assumem que o prefixo comum existe como sua própria linha, portanto, não encontre o prefixo comum para entradas como

/home/dave/file1
/home/dave/file2

informação relacionada