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

11

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

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

No entanto, quando tentei no servidor com o disco completo, recebi aproximadamente o seguinte erro (é da memória, não copia e cola):

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

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

Meu sedcomando é apenassed -i '/myregex/d' /path/to/file/filename

Existe uma maneira de aplicar este comando apesar do disco completo?

(Ele deve ser automatizado, pois preciso aplicá-lo a várias centenas de servidores como uma soluçã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 excluindo outra coisa que descobri que poderia excluir, mas ainda assim gostaria de responder a essa pergunta, 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 era possível excluir as linhas viabrindo o arquivo e executando :g/myregex/de, em seguida, salve as alterações com êxito :wq. Parece que deve ser possível automatizar isso, sem recorrer a um sistema de arquivos separado para armazenar um arquivo temporário .... (?)

Curinga
fonte
Veja também: unix.stackexchange.com/q/75889/135943
Wildcard
1
sed -icria uma cópia temporária para operar. Eu suspeito que edseria melhor para isso, embora eu não esteja familiarizado o suficiente para proibir uma solução real #
Eric Renouf
2
Com edvocê correr: printf %s\\n g/myregex/d w q | ed -s infilemas tenha em mente algumas implementações também utilizam arquivos temporários como sed(você pode tentar busybox ed - afaik ele não cria um arquivo temporário)
don_crissti
1
@Wildcard - não confiavelmente w / echo. use printf. e sedadicione alguns caracteres que você soltar na última linha para evitar perder espaços em branco à direita. Além disso, seu shell precisa ser capaz de lidar com o arquivo inteiro em uma única linha de comando. esse é o seu risco - teste primeiro. bashé especialmente ruim nisso (eu acho que é para fazer w / stack space?) e pode ficar doente com você a qualquer momento. os dois sedsão recomendados pelo menos para usar o buffer de pipe do kernel entre eles, mas o método é bastante semelhante. sua subcomando de comando também truncará filese o sed w / in é ou não bem-sucedido.
mikeserv
1
@Wildcard - tente sed '/regex/!H;$!d;x' <file|{ read v && cat >file;}e, se funcionar, leia o resto da minha resposta. '
mikeserv

Respostas:

10

A -iopção realmente não substitui 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 em seu script, mas crie o novo arquivo em um sistema de arquivos diferente.

Além disso, se você estiver apenas excluindo linhas que correspondem a uma regexp, poderá usar em grepvez 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 que a entrada e a 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 terminar.

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
Barmar
fonte
1
Ele preserva permissões, propriedade e timestamps? Talvez rsync -a --no-owner --no-group --remove-source-files "$backupfile" "$destination"a partir daqui
Hastur
@Hastur - você quer dizer que sed -iisso preserva essas coisas?
mikeserv
2
@Hastur sed -inão preserva nenhuma dessas coisas. Eu apenas tentei com um arquivo que não possuo, mas localizado em um diretório que possuo, e ele me permitiu substituir o arquivo. A substituição pertence a mim, não ao proprietário original.
Barmar
1
@ RalphRönnquist Para ter certeza, você precisaria fazê-lo em duas etapas:var=$(< FILE); echo "$FILE" | grep '^"' > FILE
Barmar
1
@ Barmar - você não funciona - você nem sabe que abriu a entrada com êxito. O muito menos que poderia fazer é v=$(<file)&& printf %s\\n "$v" >file, mas você não precisa nem usar &&. O solicitante está falando sobre executá-lo em um script - automatizando a substituição de um arquivo por uma parte dele. você deve pelo menos validar, pode abrir com êxito entrada e saída. Além disso, o shell pode explodir.
mikeserv
4

É assim que sedfunciona. Se usado com -i(edição no local) sedcria um arquivo temporário com o novo conteúdo do arquivo processado. Quando concluído sed, substitui o arquivo de trabalho atual pelo temporário. O utilitário não edita o arquivo no local . Esse é 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 no arquivo mencionado na mensagem de erro com a fflush()chamada do sistema:

Para fluxos de saída, fflush()força uma gravação de todos os dados em buffer no espaço do usuário para a saída especificada ou atualiza o fluxo por meio da função de gravação subjacente do fluxo.


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

caos
fonte
3

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

Esta seção usa o termo buffer de edição para descrever o texto de trabalho atual. Nenhuma implementação específica está implícita neste termo. Todas as alterações de edição são executadas no buffer de edição e nenhuma alteração deve afetar nenhum arquivo até que um comando do editor grave o arquivo.

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

Um estudo cuidadoso das especificações POSIX paraex indicar algumas "dicas" sobre seu uso portátil pretendido, quando comparado aos usos comuns de script exencontrados on-line (que estão repletos de vimcomandos específicos).

  1. A implementação +cmdé opcional de acordo com o POSIX.
  2. Permitir várias -copções também é opcional.
  3. O comando global :g"come" tudo até a próxima nova linha não escapada (e, portanto, executa-o 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.

Portanto, de acordo com o que pesquisei, o método compatível com POSIX para editar no local 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, mencione nos comentários.

Curinga
fonte
2
mas ex escreve para tmpfiles ... sempre. seu spec'd escreve seus buffers no disco periodicamente. existem ainda comandos spec'd para localizar os buffers de arquivo tmp no disco.
mikeserv
@Wildcard Obrigado por compartilhar, eu liguei novamente em uma postagem semelhante na SO . Eu suponho que ex +g/match/d -scx fileseja compatível com POSIX também?
kenorb
@kenorb, não exatamente, de acordo com minha leitura das especificações - veja meu ponto 1 na resposta acima. A citação exata do POSIX é "O utilitário ex deve estar em conformidade com as Diretrizes de sintaxe do utilitário XBD, exceto pelo uso não especificado de '-', e esse '+' pode ser reconhecido como um delimitador de opção e também '-'".
Curinga
1
Não posso provar isso, exceto pelo apelo ao bom senso, mas acredito que você está lendo mais sobre essa declaração a partir da especificação do que realmente existe. Sugiro que a interpretação mais segura é que nenhuma alteração no buffer de edição afetará qualquer arquivo que existia antes do início da sessão de edição ou que o usuário nomeou. Veja também meus comentários sobre minha resposta.
G-Man diz 'Reinstate Monica'
@ G-Man, na verdade acho que você está certo; minha interpretação inicial foi provavelmente uma ilusão. No entanto, desde que a edição do arquivo vi funcionou em um sistema de arquivos completo, acredito que na maioria dos casos funcionaria extambém - embora talvez não seja para um arquivo gigantesco. sed -inão funciona em um sistema de arquivos completo, independentemente do tamanho do arquivo.
Curinga
2

Use o cachimbo, Luke!

Ler arquivo | filtro | Escreva de volta

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

nesse caso sed, não cria um novo arquivo e apenas envia a saída canalizada para a ddqual abre o mesmo arquivo . Claro que se pode usar grepem casos particulares

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

depois trunque o restante.

dd if=/dev/null of=BIGFILE seek=1 bs=BYTES_OF_SED_OUTPUT
Leben Gleben
fonte
1
Você notou a parte "sistema de arquivos completo" da pergunta?
Curinga
1
@Wildcard, sedsempre usa arquivos temporários? grepde qualquer maneira não
Leben Gleben
Isso parece uma alternativa ao spongecomando. Sim, sedcom -isempre cria arquivos lilke "seduyUdmw" com 000 direitos.
Pablo A
1

Conforme observado em outras respostas, sed -ifunciona copiando o arquivo para um novo arquivo no mesmo diretório , fazendo alterações no processo e movendo o novo arquivo sobre o original. É por isso que não funciona.  ed(o editor de linha original) funciona de maneira um pouco semelhante, mas, da última vez que verifiquei, ele é usado /tmppara o arquivo inicial. Se você /tmpestiver em um sistema de arquivos diferente do que está cheio, edpode fazer o trabalho por você.

Tente isso (no prompt do shell interativo):

$ ed / caminho / para / arquivo / nome do arquivo
P
g / myregex / d
W
q

O P(que é um P maiúsculo ) não é estritamente necessário. Ativa a solicitação; sem ele, você está trabalhando no escuro e algumas pessoas acham isso desconcertante. O we qsão w rito e q uit.

edé notório por diagnósticos enigmáticos. Se a qualquer momento exibir algo diferente do prompt (que é *) ou algo que seja claramente uma confirmação de operação bem-sucedida ( especialmente se contiver a ?), não escreva o arquivo (com w). Apenas saia ( q). Se não deixar você sair, tente dizer qnovamente.

Se o seu /tmpdiretório estiver no sistema de arquivos cheio (ou se o sistema de arquivos estiver cheio também), tente encontrar algum espaço em algum lugar. o caos mencionado na montagem de um tmpfs ou de um dispositivo de armazenamento externo (por exemplo, uma unidade flash); mas, se você tiver vários sistemas de arquivos e eles não estiverem todos cheios, poderá simplesmente usar um dos outros existentes. o caos sugere copiar o (s) arquivo (s) para o outro sistema de arquivos, editando-o (com sed) e copiando-o de volta. Nesse 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 tenha algum espaço livre, definir a variável de ambiente TMPDIRpara apontar para esse diretório e depois executar ed. (Divulgação: não tenho certeza se isso vai funcionar, mas não pode doer.)

Depois de edtrabalhar, você pode automatizar isso fazendo

nome do arquivo ed << EOF
g / myregex / d
W
q
EOF

em um script. Ou , como sugerido por don_crissti.printf '%s\n' 'g/myregex/d' w q | ed -s filename

G-Man diz que 'restabelece Monica'
fonte
Hummm. O mesmo pode ser feito (com edou com ex), de modo que a memória seja usada em vez de um sistema de arquivos separado? Isso é o que eu estava realmente indo para (e a razão de eu não ter aceitado uma resposta.)
Wildcard
Hmm. Isso pode ser mais complicado do que eu percebi. Estudei a fonte de edmuitos anos atrás. Ainda existiam computadores de 16 bits, nos quais os processos eram limitados a um espaço de endereço de 64K (!); Portanto, a ideia de um editor que lê o arquivo inteiro na memória não é de partida. Desde então, é claro, a memória aumentou - mas também discos e arquivos. Como os discos são tão grandes, as pessoas não sentem necessidade de lidar com a contingência de /tmpficar sem espaço. Eu apenas dei uma olhada rápida no código fonte de uma versão recente ede ainda parece ... (continua)
G-Man diz 'Reinstate Monica'
(Continua) ... para implementar o "buffer de edição" como um arquivo temporário, incondicionalmente - e não consigo encontrar nenhuma indicação de que qualquer versão do ed( exou vi) ofereça uma opção para manter o buffer na memória.  Por outro lado, Edição de Texto com ed e vi - Capítulo 11: Processamento de Texto - Parte II: Explorando os Segredos Profissionais do Red Hat Linux - Red Hat Linux 9 - Os sistemas Linux dizem que edo buffer de edição reside na memória,… (continua )
G-Man diz 'Reinstate Monica'
(Continuação)… e o UNIX Document Processing and Typesetting de Balasubramaniam Srinivasan diz a mesma coisa vi(que é o mesmo programa que ex). Eu acredito que eles estão apenas usando palavras desleixadas e imprecisas - mas, se estiverem na Internet (ou impressas), deve ser verdade, certo? Você paga o seu dinheiro e faz a sua escolha.
G-Man diz 'Reinstate Monica'
Mas enfim, adicionei uma nova resposta.
G-Man diz 'Reinstate Monica'
1

Você pode truncar o arquivo com bastante facilidade se conseguir obter a contagem de bytes para o deslocamento e suas linhas ocorrerem desde o 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 você ${TMPDIR:-/tmp}estiver em outro sistema de arquivos, talvez:

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

Porque (a maioria) os shells colocam seus documentos aqui em um arquivo temporário excluído. É perfeitamente seguro, desde que o <<FILEdescritor seja mantido do início ao fim e ${TMPDIR:-/tmp}tenha o 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. Estes escudos são tipicamente ashderivados como busybox, dash, BSD sh- zsh, bash, kshe o shell Bourne, no entanto, todos os arquivos de utilização de trabalho temporário.

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


Se /tmpnão for viável, desde 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 sedprocesso antes de tentar truncar o arquivo de entrada / saída.

Uma solução mais direcionada - e eficiente - pode ser:

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

... porque não incomodaria as linhas de buffer que você pretendia 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
mikeserv
fonte
Confesso que não tinha lido sua resposta em detalhes antes, porque ela começa com soluções impraticáveis ​​(para mim) que envolvem contagem de bytes (diferente entre cada um dos muitos servidores) e /tmpque estão no mesmo sistema de arquivos. Eu gosto da sua sedversão dupla . Eu acho que uma combinação de Barmar de e sua resposta provavelmente seria melhor, algo como: myvar="$(sed '/myregex/d' < file)" && [ -n "$myvar" ] && echo "$myvar" > file ; unset myvar (Para este caso eu não me importo sobre a preservação de novas linhas à direita.)
Wildcard
2
@ Wildcard - que poderia ser. mas você não deve usar o shell como um banco de dados. o sed| catO item acima nunca abre a saída, a menos sedque já tenha armazenado em buffer o arquivo inteiro e esteja pronto para começar a gravar tudo na saída. Se ele tentar armazenar em buffer o arquivo e falhar - readnão será bem-sucedido porque encontra o EOF no |canal antes de ler sua primeira nova linha e, por isso, cat >out nunca acontece até a hora de gravá-lo completamente da memória. um estouro ou algo assim simplesmente falha. também todo o pipeline retorna sempre sucesso ou fracasso. armazená-lo em um var é apenas mais arriscado.
mikeserv
@Wildcard - se eu realmente o quisesse em uma variável também, acho que faria isso: file=$(sed '/regex/!H;$!d;x' <file | read v && tee file) && cmp - file <<<"$file" || shiteassim, o arquivo de saída e o var seriam gravados simultaneamente, o que faria um backup eficaz ou um , ou seja, o único motivo pelo qual você deseja complicar as coisas além do necessário.
mikeserv
@ MikeServ: Estou lidando com o mesmo problema que o OP agora e acho sua solução realmente útil. Mas não entendo o uso read scripte read vna sua resposta. Se você puder elaborar mais sobre isso, serei muito apreciado, obrigado!
sylye 26/09/16
1
@ sylye - $scripté o sedscript que você usaria para direcionar para qualquer parte do seu arquivo que você desejasse; é o script que fornece o resultado final que você deseja no fluxo. vé apenas um espaço reservado para uma linha vazia. em um bashshell, não é necessário porque bashusará automaticamente a $REPLYvariável shell em seu lugar, se você não especificar um, mas POSIXly sempre deverá fazê-lo. Fico feliz que você ache útil, a propósito. Boa sorte com isso. im mikeserv @ gmail se você precisar de algo em profundidade. i deve ter um computador novo em alguns dias
mikeserv
0

Esta resposta empresta idéias dessa outra resposta e dessa outra resposta, mas se baseia nelas, criando uma resposta mais aplicável em geral:

num_bytes = $ (sed '/ myregex / d' / caminho / para / arquivo / nome do arquivo | wc -c)
sed '/ myregex / d' / caminho / para / arquivo / nome do arquivo 1 <> / caminho / para / arquivo / nome do arquivo 
dd se = / dev / null de = / caminho / para / arquivo / nome do arquivo bs = "$ num_bytes" = 1

A primeira linha executa o sedcomando com a saída gravada na saída padrão (e não em um arquivo); especificamente, a um tubo wcpara contar os caracteres. A segunda linha também executa o sedcomando com a saída gravada na saída padrão, que, nesse caso, é redirecionada para o arquivo de entrada no modo de substituição de leitura / gravação (sem truncamento), discutido aqui . Isso é algo perigoso de se fazer; é seguro somente quando o comando filter nunca aumenta a quantidade de dados (texto); ou seja, para cada n bytes que lê, ele grava n ou menos bytes. Isso é verdade, é claro, para o sed '/myregex/d'comando; 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 o faria.)

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 sobrescrito com 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.\nrestantes no final.

Por fim, o ddcomando procura o final dos novos dados limpos (byte 25 neste exemplo) e remove o restante do arquivo; isto é, trunca o arquivo nesse ponto.


Se, por qualquer motivo, o 1<>truque não funcionar, você poderá

sed '/ myregex / d' / caminho / para / arquivo / nome do arquivo | dd de = / caminho / para / arquivo / nome do arquivo conv = notrunc

Além disso, observe que, enquanto tudo o que você está fazendo é remover linhas, tudo o que você precisa é grep -v myregex(conforme indicado por Barmar ).

G-Man diz que 'restabelece Monica'
fonte
-3

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

Chiranjeeb
fonte
1
Oi! Seria melhor explicar com o máximo de detalhes relevantes como sua solução funciona e responde à pergunta.
dhag
2
Esta é uma péssima resposta. (a) Ele falhará em um sistema de arquivos completo, assim como meu comando original; (b) Se tivesse êxito, esvaziaria o arquivo TODO, em vez de apenas as linhas correspondentes ao meu regex.
Curinga