Existe uma maneira de modificar um arquivo no local?

50

Eu tenho um arquivo razoavelmente grande (35Gb), e eu gostaria de filtrar este arquivo in situ (ou seja, eu não tenho espaço em disco suficiente para outro arquivo), especificamente eu quero grep e ignorar alguns padrões - existe uma maneira de fazer isso sem usar outro arquivo?

Digamos que eu queira filtrar todas as linhas que contêm foo: , por exemplo ...

    
por Nim 11.04.2011 / 11:53

11 respostas

40

No nível de chamada do sistema, isso deve ser possível. Um programa pode abrir seu arquivo de destino para escrever sem truncá-lo e começar a escrever o que ele lê de stdin. Ao ler EOF, o arquivo de saída pode ser truncado.

Como você está filtrando linhas da entrada, a posição de gravação do arquivo de saída deve ser sempre menor que a posição de leitura. Isso significa que você não deve corromper sua entrada com a nova saída.

No entanto, encontrar um programa que faz isso é o problema. dd(1) tem a opção conv=notrunc que não trunca o arquivo de saída ao abrir, mas também não trunca no final, deixando o conteúdo do arquivo original após o conteúdo do grep (com um comando como grep pattern bigfile | dd of=bigfile conv=notrunc )

Como é muito simples a partir de uma perspectiva de chamada do sistema, eu escrevi um pequeno programa e o testei em um sistema de arquivos de loopback completo pequeno (1MiB). Ele fez o que você queria, mas você realmente quer testar isso com alguns outros arquivos primeiro. Sempre será arriscado substituir um arquivo.

overwrite.c

/* This code is placed in the public domain by camh */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)
{
        int outfd;
        char buf[1024];
        int nread;
        off_t file_length;

        if (argc != 2) {
                fprintf(stderr, "usage: %s <output_file>\n", argv[0]);
                exit(1);
        }
        if ((outfd = open(argv[1], O_WRONLY)) == -1) {
                perror("Could not open output file");
                exit(2);
        }
        while ((nread = read(0, buf, sizeof(buf))) > 0) {
                if (write(outfd, buf, nread) == -1) {
                        perror("Could not write to output file");
                        exit(4);
                }
        }
        if (nread == -1) {
                perror("Could not read from stdin");
                exit(3);
        }
        if ((file_length = lseek(outfd, 0, SEEK_CUR)) == (off_t)-1) {
                perror("Could not get file position");
                exit(5);
        }
        if (ftruncate(outfd, file_length) == -1) {
                perror("Could not truncate file");
                exit(6);
        }
        close(outfd);
        exit(0);
}

Você usaria como:

grep pattern bigfile | overwrite bigfile

Estou principalmente postando isso para os outros comentarem antes de você tentar. Talvez alguém saiba de um programa que faz algo semelhante que é mais testado.

    
por 11.04.2011 / 14:01
19

Você pode usar sed para editar arquivos no lugar (mas isso cria um arquivo temporário intermediário):

Para remover todas as linhas que contêm foo :

sed -i '/foo/d' myfile

Para manter todas as linhas contendo foo :

sed -i '/foo/!d' myfile
    
por 11.04.2011 / 13:49
19

Assumirei que seu comando de filtro é o que chamarei de filtro de encolhimento de prefixo , que tem a propriedade de que o byte N na saída nunca é gravado antes de ter lido pelo menos N bytes de entrada. grep tem essa propriedade (contanto que seja apenas filtragem e não fazer outras coisas, como adicionar números de linha para correspondências). Com esse filtro, você pode substituir a entrada à medida que avança. Claro, você precisa ter certeza de não cometer nenhum erro, já que a parte sobrescrita no início do arquivo será perdida para sempre.

A maioria das ferramentas unix oferece apenas a opção de anexar a um arquivo ou truncá-lo, sem possibilidade de sobrescrevê-lo. A única exceção na caixa de ferramentas padrão é dd , que pode ser instruída a não truncar seu arquivo de saída . Portanto, o plano é filtrar o comando em dd conv=notrunc . Isso não altera o tamanho do arquivo, então também pegamos o tamanho do novo conteúdo e truncamos o arquivo para esse tamanho (novamente com dd ). Observe que essa tarefa é inerentemente não robusta - se ocorrer um erro, você estará sozinho.

export LC_ALL=C
n=$({ grep -v foo <big_file |
      tee /dev/fd/3 |
      dd of=big_file conv=notrunc; } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek=$n

Você pode escrever Perl equivalente. Aqui está uma implementação rápida que não tenta ser eficiente. Claro, você pode querer fazer sua filtragem inicial diretamente nesse idioma também.

grep -v foo <big_file | perl -e '
  close STDOUT;
  open STDOUT, "+<", $ARGV[0] or die;
  while (<STDIN>) {print}
  truncate STDOUT, tell STDOUT or die
' big_file
    
por 11.04.2011 / 23:22
16

Com qualquer shell parecido com o Bourne:

{
  cat < bigfile | grep -v to-exclude
  perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile

Por algum motivo, parece que as pessoas tendem a esquecer o padrão de 40 anos e operador de redirecionamento de leitura + gravação.

Abrimos bigfile no modo de leitura + gravação e (o que é mais importante aqui) sem truncamento em stdout , enquanto bigfile está aberto (separadamente) em cat stdin . Depois de grep ter terminado, e se tiver removido algumas linhas, stdout agora aponta para algum lugar dentro de bigfile , precisamos nos livrar do que está além desse ponto. Daí o comando perl que trunca o arquivo ( truncate STDOUT ) na posição atual (conforme retornado por tell STDOUT ).

(o cat é para o GNU grep que, do contrário, reclama se stdin e stdout apontarem para o mesmo arquivo).

¹ Bem, enquanto <> esteve no shell Bourne desde o início no final dos anos setenta, era inicialmente não documentado e não corretamente implementado . Não estava na implementação original de ash de 1989 e, embora seja um operador de redirecionamento POSIX sh (desde o início dos anos 90 como POSIX sh é baseado em ksh88 que sempre o teve), não foi adicionado ao FreeBSD sh , por exemplo, até 2000, então portably 15 anos de idade é provavelmente mais preciso. Observe também que o descritor de arquivo padrão quando não especificado é <> em todos os shells, exceto que em ksh93 ele mudou de 0 para 1 em ksh93t + em 2010 (quebrando compatibilidade com versões anteriores e conformidade POSIX)

    
por 19.05.2015 / 18:59
9

Embora esta seja uma pergunta antiga, parece-me que é uma questão perene, e uma solução mais geral e mais clara está disponível do que foi sugerido até agora. Crédito para onde o crédito é devido: não tenho certeza se teria pensado nisso sem considerar a menção de Stéphane Chazelas do operador <> update.

Abrir um arquivo para atualização em um shell Bourne é de utilidade limitada. O shell não oferece uma maneira de procurar em um arquivo e nenhuma maneira de definir seu novo tamanho (se menor que o antigo). Mas isso é facilmente solucionado, tão facilmente que me surpreende que não esteja entre os utilitários padrão em /usr/bin .

Isso funciona:

$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T; 
     1  foo

Como isso (gorjeta para Stéphane):

$ { grep foo T && ftruncate; } 1<>T  && nl T; 
     1  foo

(Estou usando o GNU grep. Talvez algo tenha mudado desde que ele escreveu sua resposta.)

Exceto, você não tem / usr / bin / ftruncate . Para algumas dezenas de linhas de C, você pode ver abaixo. Este utilitário ftruncate trunca um descritor de arquivo arbitrário para um comprimento arbitrário, padronizando a saída padrão e a posição atual.

O comando acima (primeiro exemplo)

  • abre o descritor de arquivos 4 em T para atualização. Assim como com open (2), abrir o arquivo desta forma posiciona o deslocamento atual em 0.
  • O grep processa T normalmente, e o shell redireciona sua saída para T via o descritor 4.
  • ftruncate chama ftruncate (2) no descritor 4, definindo o comprimento para o valor do offset atual (exatamente onde grep o deixou).

A subshell então sai, fechando o descritor 4. Aqui está ftruncate :

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main( int argc, char *argv[] ) {
  off_t i, fd=1, len=0;
  off_t *addrs[2] = { &fd, &len };

  for( i=0; i < argc-1; i++ ) {
    if( sscanf(argv[i+1], "%lu", addrs[i]) < 1 ) {
      err(EXIT_FAILURE, "could not parse %s as number", argv[i+1]);
    }
  }

  if( argc < 3 && (len = lseek(fd, 0, SEEK_CUR)) == -1 ) {
    err(EXIT_FAILURE, "could not ftell fd %d as number", (int)fd);
  }


  if( 0 != ftruncate((int)fd, len) ) {
    err(EXIT_FAILURE, argc > 1? argv[1] : "stdout");
  }

  return EXIT_SUCCESS;
}

N.B., ftruncate (2) não é portável quando usado dessa maneira. Para uma generalidade absoluta, leia o último byte escrito, reabra o arquivo O_WRONLY, procure, escreva o byte e feche.

Dado que a questão tem 5 anos, vou dizer que esta solução não é óbvia. Ele tira proveito de exec para abrir um novo descritor, e o operador <> , ambos os quais são arcanos. Eu não consigo pensar em um utilitário padrão que manipula um inode pelo descritor de arquivo. (A sintaxe poderia ser ftruncate >&4 , mas não tenho certeza de que houve uma melhoria.) É consideravelmente mais curta do que a resposta competente e exploratória de camh. É apenas um pouco mais claro do que o de Stéphane, IMO, a menos que você goste do Perl mais do que eu. Espero que alguém ache útil.

Uma maneira diferente de fazer a mesma coisa seria uma versão executável de lseek (2) que relata o deslocamento atual; a saída poderia ser usada para / usr / bin / truncate , que alguns Linuxi fornecem.

    
por 30.04.2016 / 23:55
5

ed é provavelmente a escolha certa para editar um arquivo no local:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS
    
por 11.04.2011 / 17:50
5

Você pode usar um descritor de arquivo de leitura / gravação bash para abrir seu arquivo (para sobrescrevê-lo no local), então sed e truncate ... mas é claro, nunca permita que suas alterações sejam maior que a quantidade de dados lidos até o momento.

Aqui está o script (usa: variável bash $ BASHPID)

# Create a test file
  echo "going abc"  >junk
  echo "going def" >>junk
  echo "# ORIGINAL file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )
#
# Assign file to fd 3, and open it r/w
  exec 3<> junk  
#
# Choose a unique filename to hold the new file size  and the pid 
# of the semi-asynchrounous process to which 'tee' streams the new file..  
  [[ ! -d "/tmp/$USER" ]] && mkdir "/tmp/$USER" 
  f_pid_size="/tmp/$USER/pid_size.$(date '+%N')" # %N is a GNU extension: nanoseconds
  [[ -f "$f_pid_size" ]] && { echo "ERROR: Work file already exists: '$f_pid_size'" ;exit 1 ; }
#
# run 'sed' output to 'tee' ... 
#  to modify the file in-situ, and to count the bytes  
  <junk sed -e "s/going //" |tee >(echo -n "$BASHPID " >"$f_pid_size" ;wc -c >>"$f_pid_size") >&3
#
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The byte-counting process is not a child-process, 
# so 'wait' doesn't work... but wait we must...  
  pid_size=($(cat "$f_pid_size")) ;pid=${pid_size[0]}  
  # $f_pid_size may initially contain only the pid... 
  # get the size when pid termination is assured
  while [[ "$pid" != "" ]] ; do
    if ! kill -0 "$pid" 2>/dev/null; then
       pid=""  # pid has terminated. get the byte count
       pid_size=($(cat "$f_pid_size")) ;size=${pid_size[1]}
    fi
  done
  rm "$f_pid_size"
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#
  exec 3>&- # close fd 3.
  newsize=$(cat newsize)
  echo "# MODIFIED file (before truncating)";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
 truncate -s $newsize junk
 echo "# NEW (truncated) file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
exit

Aqui está a saída de teste

# ORIGINAL file
going abc
going def
# 2 lines, 20 bytes

# MODIFIED file (before truncating)
abc
def
c
going def
# 4 lines, 20 bytes

# NEW (truncated) file
abc
def
# 2 lines, 8 bytes
    
por 14.04.2011 / 15:17
3

Eu mapeava o arquivo na memória, fazia tudo no local usando ponteiros char * para a memória nua, depois desmapeava o arquivo e o truncava.

    
por 12.04.2011 / 03:20
2

Não exatamente in-situ , mas isso pode ser útil em circunstâncias semelhantes.
Se o espaço em disco for um problema, comprima o arquivo primeiro (já que é texto, isso causará uma grande redução) e use sed (ou grep, ou qualquer outro) da maneira usual no meio de um pipeline de descompactação / compactação.

# Reduce size from ~35Gb to ~6Gb
$ gzip MyFile

# Edit file, creating another ~6Gb file
$ gzip -dc <MyFile.gz | sed -e '/foo/d' | gzip -c >MyEditedFile.gz
    
por 24.03.2017 / 17:41
0

Para o benefício de qualquer pessoa pesquisando esta questão, a resposta correta é parar procurando por recursos de shell obscuros que correm o risco de corromper seu arquivo para ganhos insignificantes de desempenho e usar alguma variação desse padrão:

grep "foo" file > file.new && mv file.new file

Apenas na situação extremamente incomum que isto por alguma razão não é viável, você deve considerar seriamente qualquer outra resposta nesta página (embora certamente seja interessante ler). Eu admitirei que o enigma do OP de não ter espaço em disco para criar um segundo arquivo é exatamente essa situação. Embora, mesmo assim, existam outras opções disponíveis, por ex. como fornecido por @Ed Randall e @Basile Starynkevitch.

    
por 06.04.2018 / 16:27
-3

echo -e "$(grep pattern bigfile)" >bigfile

    
por 15.12.2013 / 12:03