sed supressão de linha no local no sistema de arquivos completo?

11

Devido a um bug de aplicativo ainda não diagnosticado, tenho várias centenas de servidores com um disco cheio. Existe um arquivo que foi preenchido com linhas duplicadas - não um arquivo de log, mas um arquivo de ambiente de usuário com definições de variáveis (portanto, não posso simplesmente excluir o arquivo).

Eu escrevi um comando simples sed para verificar as linhas erroneamente adicionadas e excluí-las, e as testei em uma cópia local do arquivo. Funcionou como pretendido.

No entanto, quando tentei no servidor com o disco inteiro, obtive aproximadamente o seguinte erro (é da memória, não copiar e colar):

sed: couldn't flush /path/to/file/sed8923ABC: No space left on deviceServerHostname

Claro, eu sei que não há mais espaço. É por isso que estou tentando excluir coisas! (O comando sed que estou usando reduzirá um arquivo de linha de 4000+ para cerca de 90 linhas).

Meu comando sed é apenas sed -i '/myregex/d' /path/to/file/filename

Existe alguma maneira de aplicar este comando apesar do disco inteiro?

(Ele deve ser automatizado, já que preciso aplicá-lo a várias centenas de servidores como uma correção rápida.)

(Obviamente o bug do aplicativo precisa ser diagnosticado, mas enquanto isso os servidores não estão funcionando corretamente ...)

Atualização: A situação que enfrentei foi resolvida com a exclusão de outra coisa que descobri que poderia excluir, mas ainda assim gostaria da resposta para essa questão, que seria útil no futuro e para outras pessoas.

/tmp é um não-go; está no mesmo sistema de arquivos.

Antes de liberar espaço em disco, testei e descobri que podia excluir as linhas em vi abrindo o arquivo e executando :g/myregex/d e, em seguida, salvando as alterações com :wq . Parece que deve ser possível automatizar isso, sem recorrer a um sistema de arquivos separado para armazenar um arquivo temporário .... (?)

    
por Wildcard 22.12.2015 / 21:08

8 respostas

10

A opção -i realmente não sobrescreve o arquivo original. Ele cria um novo arquivo com a saída e o renomeia para o nome do arquivo original. Como você não tem espaço no sistema de arquivos para esse novo arquivo, ele falha.

Você precisará fazer isso sozinho no seu script, mas crie o novo arquivo em um sistema de arquivos diferente.

Além disso, se você acabou de excluir as linhas que correspondem a um expressão regular, use grep em vez de sed .

grep -v 'myregex' /path/to/filename > /tmp/filename && mv /tmp/filename /path/to/filename

Em geral, raramente é possível que os programas usem o mesmo arquivo como entrada e saída - assim que começar a gravar no arquivo, a parte do programa que está lendo o arquivo não verá mais o conteúdo original. Portanto, ele precisa copiar o arquivo original em algum lugar primeiro ou gravar em um novo arquivo e renomeá-lo quando estiver pronto.

Se você não quiser usar um arquivo temporário, tente armazenar em cache o conteúdo do arquivo na memória:

file=$(< /path/to/filename)
echo "$file" | grep -v 'myregex' > /path/to/filename
    
por 22.12.2015 / 21:32
4

É assim que sed funciona. Se usado com -i (no local de edição) sed cria um arquivo temporário com o novo conteúdo do arquivo processado. Quando terminar sed , substitui o arquivo de trabalho atual pelo temporário. O utilitário não edita o arquivo no local . Isso é exatamente o comportamento de todo editor.

É como se você executasse a seguinte tarefa em um shell:

sed 'whatever' file >tmp_file
mv tmp_file file

Neste ponto sed , tenta liberar os dados em buffer para o arquivo mencionado na mensagem de erro com a chamada do sistema fflush() :

For output streams, fflush() forces a write of all user-space buffered data for the given output or update stream via the stream's underlying write function.

Para o seu problema, vejo uma solução na montagem de um sistema de arquivos separte (por exemplo, tmpfs , se você tiver memória suficiente ou um dispositivo de armazenamento externo) e mova alguns arquivos lá, processe-os e mova-os de volta .

    
por 22.12.2015 / 21:29
3

Desde postar esta pergunta, aprendi que ex é um programa compatível com POSIX. É quase universalmente vinculado a vim , mas de qualquer forma, o seguinte é (eu acho) um ponto-chave sobre ex em relação aos sistemas de arquivos (retirado da especificação POSIX):

This section uses the term edit buffer to describe the current working text. No specific implementation is implied by this term. All editing changes are performed on the edit buffer, and no changes to it shall affect any file until an editor command writes the file.

"... deve afetar qualquer arquivo ..." Acredito que colocar algo no sistema de arquivos (em todos os casos, até mesmo um arquivo temporário) contaria como "afetando qualquer arquivo". Talvez? *

Um estudo cuidadoso das especificações POSIX para ex indicam algumas "dicas" sobre a sua uso portátil planejado quando comparado aos usos de scripts comuns de ex encontrados on-line (que estão repletos de comandos vim -específicos).

  1. A implementação de +cmd é opcional de acordo com o POSIX.
  2. Permitir várias opções -c também é opcional.
  3. O comando global :g "come" tudo até a próxima nova linha não escapada (e, portanto, o executa após cada correspondência encontrada para a regex, em vez de uma vez no final). Portanto, -c 'g/regex/d | x' apenas exclui uma instância e sai do arquivo.

Então, de acordo com o que pesquisei, o método compatível com POSIX para edição no local de um arquivo em um sistema de arquivos completo para excluir todas as linhas correspondentes a um regex específico é:

ex -sc 'g/myregex/d
x' /path/to/file/filename

Isso deve funcionar, desde que você tenha memória suficiente para carregar o arquivo em um buffer.

* Se você encontrar algo que indique o contrário, por favor mencione nos comentários.

    
por 07.01.2016 / 11:00
2

Use o cachimbo, Luke!

Leia o arquivo | filtro | escrever de volta

sed 's/PATTERN//' BIGFILE | dd of=BIGFILE conv=notrunc

neste caso, sed não cria um novo arquivo e apenas envia a saída canalizada para dd , que abre o mesmo arquivo . Claro que se pode usar grep em caso particular

grep -v 'PATTERN' BIGFILE | dd of=BIGFILE conv=notrunc

então Trunca o restante.

dd if=/dev/null of=BIGFILE seek=1 bs=BYTES_OF_SED_OUTPUT
    
por 15.01.2016 / 19:25
1

Como notado em outras respostas, sed -i funciona copiando o arquivo para um novo arquivo no mesmo diretório , fazendo alterações no processo e, em seguida, movendo o novo arquivo sobre o original. É por isso que não funciona. ed (o editor de linha original) funciona de uma maneira semelhante, mas, da última vez que verifiquei, ele usa /tmp para o arquivo de rascunho. Se o seu /tmp estiver em um sistema de arquivos diferente do que está cheio, ed pode fazer o trabalho por você.

Tente isso (no prompt do seu shell interativo):

$ ed /path/to/file/filename
P
g/myregex/d
w
q

O P (que é um capital P) não é estritamente necessário. Acontece ao solicitar; sem isso, você está trabalhando no escuro e algumas pessoas acham isso desconcertante. Os w e q são w rite e q uit.

ed is notorious for cryptic diagnostics.  If at any point it displays anything other that the prompt (which is *) or something that is clearly a confirmation of successful operation (especially if it contains a ?), do not write the file (with w).  Just quit (q).  If it doesn't let you out, try saying q again.

Se o diretório /tmp estiver no sistema de arquivos que está cheio (ou se o sistema de arquivos estiver cheio também), tente encontrar algum espaço em algum lugar. o caos mencionou a montagem de um tmpfs ou um dispositivo de armazenamento externo (por exemplo, um pen drive); mas, se você tem vários sistemas de arquivos, e eles não estão todos completos, você pode simplesmente usar um dos outros existentes. caos sugere copiar o (s) arquivo (s) para o outro sistema de arquivos, editá-los lá (com sed ) e, em seguida, copiá-los de volta. Neste ponto, essa pode ser a solução mais simples. Mas uma alternativa seria criar um diretório gravável em um sistema de arquivos que tem algum espaço livre, definir a variável de ambiente TMPDIR para apontar para esse diretório, e, em seguida, execute ed . (Divulgação: não tenho certeza se isso vai funcionar, mas não pode doer.)

Quando você obtiver ed funcionando, poderá automatizar isso fazendo

ed filename << EOF
g/myregex/d
w
q
EOF

em um script. Ou %código%, como sugerido por don_crissti.

    
por 22.12.2015 / 22:48
1

Você pode truncar o arquivo com bastante facilidade se puder obter a contagem de bytes em seu deslocamento e suas linhas ocorrerem de um ponto inicial até o final.

o=$(sed -ne'/regex/q;p' <file|wc -c)
dd if=/dev/null of=file bs="$o" seek=1

Ou então, se o seu ${TMPDIR:-/tmp} estiver em algum outro sistema de arquivos, talvez:

{   cut -c2- | sed "$script" >file
} <file <<FILE
$(paste /dev/null -)
FILE

Porque os (mais) shells colocam seus documentos aqui em um arquivo temporário deletado. É perfeitamente seguro, desde que o descritor <<FILE seja mantido do início ao fim e ${TMPDIR:-/tmp} tenha o máximo de espaço necessário.

Os shells que não usam arquivos temporários usam pipes e, portanto, não são seguros para uso dessa maneira. Esses shells são normalmente ash derivados como busybox , dash , BSD sh - zsh , bash , ksh e o shell Bourne, no entanto, todos usam arquivos temporários.

aparentemente eu escrevi um pequeno programa de shell em julho passado para fazer algo parecido com isso

Se /tmp não for viável, contanto que você possa ajustar o arquivo na memória algo como ...

sed 'H;$!d;x' <file | { read v &&
sed "$script" >file;}

... como um caso geral, pelo menos, garantiria que o arquivo fosse totalmente armazenado em buffer pelo primeiro processo sed antes de tentar truncar o arquivo de entrada / saída.

Uma solução mais direcionada e eficiente poderia ser:

sed '/regex/!H;$!d;x' <file|{ read v && cat >file;}

... porque não iria incomodar as linhas de buffer que você queria excluir de qualquer maneira.

Um teste do caso geral:

{   nums=/tmp/nums
    seq 1000000 >$nums
    ls -lh "$nums"
    wc -l  "$nums"
    sed 'H;$!d;x' <$nums | { read script &&  ### read always gets a blank
    sed "$script" >$nums;}
    wc -l  "$nums"
    ls -lh "$nums"
}
-rw-r--r-- 1 mikeserv mikeserv 6.6M Dec 22 20:26 /tmp/nums
1000000 /tmp/nums
1000000 /tmp/nums
-rw-r--r-- 1 mikeserv mikeserv 6.6M Dec 22 20:26 /tmp/nums
    
por 22.12.2015 / 21:35
0

Esta resposta pega ideias de esta outra resposta e esta outra resposta , mas baseia-se nelas, criando uma resposta mais geralmente aplicável:

num_bytes=$(sed '/myregex/d' /path/to/file/filename | wc -c)
sed '/myregex/d' /path/to/file/filename 1<> /path/to/file/filename
dd if=/dev/null of=/path/to/file/filename bs="$num_bytes" seek=1

A primeira linha executa o comando sed com saída escrito para saída padrão (e não para um arquivo); especificamente, para um pipe para wc para contar os caracteres. A segunda linha também executa o comando sed com saída escrito para saída padrão, que, neste caso, é redirecionado para o arquivo de entrada modo de sobrescrever leitura / gravação (sem truncamento), que é discutido aqui . Isso é algo perigoso de se fazer; é apenas seguro quando o comando de filtro nunca aumenta a quantidade de dados (texto); isto é, para cada n bytes que lê, escreve n ou menos bytes. Isso é verdade, é claro, para o comando sed '/myregex/d' ; para cada linha que lê, escreve exatamente a mesma linha ou nada. (Outros exemplos: s/foo/fu/ ou s/foo/bar/ seria seguro, mas s/fu/foo/ e s/foo/foobar/ não.)

Por exemplo:

$ cat filename
It was
a dark and stormy night.
$ sed '/was/d' filename 1<> filename
$ cat filename
a dark and stormy night.
night.

porque esses 32 bytes de dados:

I  t     w  a  s \n  a     d  a  r  k     a  n  d     s  t  o  r  m  y     n  i  g  h  t  . \n

foi substituído por esses 25 caracteres:

a     d  a  r  k     a  n  d     s  t  o  r  m  y     n  i  g  h  t  . \n

deixando os sete bytes night.\n restantes no final.

Finalmente, o comando dd procura o fim do novo, dados lavados (byte25 neste exemplo) e remove o resto do arquivo; isto é, trunca o arquivo nesse ponto.

Se, por algum motivo, o truque 1<> não funcionar, você pode fazer

sed '/myregex/d' /path/to/file/filename | dd of=/path/to/file/filename conv=notrunc

Além disso, observe que, enquanto tudo o que você estiver fazendo for remover linhas, tudo que você precisa é de grep -v myregex (como apontado por Barmar ).

    
por 01.02.2016 / 00:05
-3

sed -i 'd' / caminho / para / arquivo / nome do arquivo

    
por 22.12.2015 / 21:44