
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/file1
e /home/dave/sub2/file2
tem o prefixo /home/dave
, o /home/dave
será preservado enquanto os outros dois não. Ou seja, ele fará o oposto completo do sed
comando 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/dave
e/home/phil
seria 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/dave
na 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 removecoverings
comando:
#!/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/ample
e não /exa/mple
deveria 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 commonprefix
comando:
#!/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 = $_; }'
dá
/home/dave
/home/phil
A mágica está nos argumentos de linha de comando para perl: -e
nos permite fornecer um script na linha de comando, -n
itera sobre as linhas do arquivo (colocando cada linha em $_
) e -l
lida com novas linhas para nós.
O script funciona usando l
para rastrear o último prefixo visto. O BEGIN
bloco é 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 l
como 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