Existe uma maneira de modificar um arquivo no local?

54

Eu tenho um arquivo bastante grande (35Gb) e gostaria de filtrar esse arquivo in situ (ou seja, não tenho espaço em disco suficiente para outro arquivo), especificamente 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 ...

Nim
fonte
3
@Tshepang: Eu acho que ele quer escrever de volta para o mesmo arquivo.
Faheem Mitha
5
"in situ" é uma frase em latim que significa "no local". Literalmente, "em posição".
Faheem Mitha
3
Nesse caso, a pergunta deve ser mais clara, algo como existe uma maneira de modificar um arquivo no local ?
tshepang
5
@Tshepang, "in situ" é uma frase bastante comum usada em inglês para descrever exatamente isso - eu pensei que o título era bastante autoexplicativo ... @Gilles, achei muito mais fácil esperar por mais espaço em disco! ;)
Nim
2
@ Nim: Bem, acho que no local é mais comum do que no local .
tshepang

Respostas:

41

No nível da chamada do sistema, isso deve ser possível. Um programa pode abrir seu arquivo de destino para gravação sem truncá-lo e começar a escrever o que lê do 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 sempre deve ser 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 faça isso é o problema. dd(1)tem a opção conv=notruncque não trunca o arquivo de saída em aberto, mas também não trunca no final, deixando o conteúdo original do arquivo após o conteúdo grep (com um comando como grep pattern bigfile | dd of=bigfile conv=notrunc)

Como é muito simples do ponto de vista de chamada do sistema, escrevi um pequeno programa e o testei em um pequeno sistema de arquivos de loopback completo (1MiB). Ele fez o que você queria, mas você realmente deseja testar isso com alguns outros arquivos primeiro. Sempre será arriscado sobrescrever 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ê o usaria como:

grep pattern bigfile | overwrite bigfile

Estou postando isso principalmente para que outros comentem antes de tentar. Talvez alguém conheça um programa que faça algo semelhante e que seja mais testado.

camh
fonte
Eu queria ver se conseguiria fugir sem escrever algo para isso! :) Acho que isso vai dar certo! Obrigado!
Nim
2
+1 para C; parece funcionar, mas vejo um problema em potencial: o arquivo está sendo lido do lado esquerdo no momento em que o direito está gravando no mesmo arquivo e, a menos que você coordene os dois processos, você terá problemas de substituição potencialmente iguais blocos. Talvez seja melhor para a integridade do arquivo usar um tamanho de bloco menor, pois a maioria das ferramentas principais provavelmente usará o 8192. Isso pode tornar o programa mais lento o suficiente para evitar conflitos (mas não pode garantir). Talvez leia porções maiores na memória (não todas) e escreva em blocos menores. Também pode adicionar um nanosleep (2) / usleep (3).
Arcege
4
@Arcege: A escrita não é feita em blocos. Se seu processo de leitura tiver lido 2 bytes e seu processo de gravação gravar 1 byte, apenas o primeiro byte será alterado e o processo de leitura poderá continuar lendo no byte 3 com o conteúdo original nesse ponto inalterado. Como grepnão produzirá mais dados do que lê, a posição de gravação deve estar sempre atrás da posição de leitura. Mesmo se você estiver escrevendo na mesma proporção que a leitura, ainda estará ok. Tente rot13 com isso em vez de grep e novamente. md5sum antes e depois e você verá o mesmo.
Camh
6
Agradável. Esta pode ser uma adição valiosa aos maisutilis de Joey Hess . Você pode usardd , mas é complicado.
Gilles 'SO- stop be evil' ''
'grep padrão bigfile | overwrite bigfile '- eu fiz isso funcionar sem erros, mas o que eu não entendo é - não é o requisito para substituir o que está no padrão por algum outro texto? não deveria ser algo como: 'grep pattern bigfile | Substituir / substituir texto / bigfile '
Alexander Mills
20

Você pode usar sedpara editar arquivos no local (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
dogbane
fonte
interessante, esse arquivo temporário precisará ter o mesmo tamanho que o original?
Nim
3
Sim, então isso provavelmente não é bom.
Pjc50
17
Não é isso que o OP está solicitando, pois cria um segundo arquivo.
precisa saber é o seguinte
11
Esta solução falhará em um sistema de arquivos somente leitura, onde "somente leitura" significa que você $HOME poderá ser gravado, mas /tmpserá somente leitura (por padrão). Por exemplo, se você possui o Ubuntu e inicializou no Console de recuperação, esse é geralmente o caso. Além disso, o operador aqui documentado <<<também não funcionará lá, pois precisa /tmpser r / w porque também gravará um arquivo temporário nele. (cf. esta questão inclui uma stracesaída)
syntaxerror
sim, isso também não funcionará para mim, todos os comandos sed que eu tentei substituirão o arquivo atual por um novo arquivo a (apesar do sinalizador --in-place).
Alexander Mills
19

Assumirei que seu comando filter é o que chamarei de filtro encolhimento de prefixo , que tem a propriedade de que o byte N na saída nunca é gravado antes de ler pelo menos N bytes de entrada. greptem essa propriedade (desde que apenas filtre e não faça outras coisas, como adicionar números de linha para correspondências). Com esse filtro, você pode substituir a entrada à medida que avança. Obviamente, você deve ter certeza de não cometer nenhum erro, pois a parte substituída 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 é a ddque pode ser solicitada a não truncar seu arquivo de saída. Portanto, o plano é filtrar o comando dd conv=notrunc. Isso não altera o tamanho do arquivo, então também capturamos o tamanho do novo conteúdo e truncamos o arquivo para esse tamanho (novamente com dd). Observe que esta tarefa é inerentemente não robusta - se ocorrer um erro, você estará por sua conta.

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 áspero. Aqui está uma implementação rápida que não tenta ser eficiente. Obviamente, você também pode fazer sua filtragem inicial diretamente nesse idioma.

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
Gilles 'SO- parar de ser mau'
fonte
16

Com qualquer casca semelhante a Bourne:

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

Por alguma razão, parece que as pessoas tendem a esquecer aquele operador de redirecionamento de leitura e gravação padrão de 40 anos de idade¹ .

Abrimos bigfileem modo + write leitura e (o que mais importa aqui), sem cortes no stdoutenquanto bigfileestá aberto (separadamente) em cat's stdin. Após o greptérmino, e se ele removeu algumas linhas, stdoutagora aponta para algum lugar dentro bigfile, precisamos nos livrar do que está além desse ponto. Daí o perlcomando que trunca o arquivo ( truncate STDOUT) na posição atual (retornada por tell STDOUT).

( caté para o GNU grepque reclama se stdin e stdout apontam para o mesmo arquivo).


¹ Bem, embora <>esteja no shell Bourne desde o início no final dos anos setenta, ele foi inicialmente não documentado e não foi implementado adequadamente . Ele não estava na implementação original de ash1989 e, embora seja um shoperador de redirecionamento POSIX (desde o início dos anos 90, como o POSIX shse baseia no ksh88que sempre o possuía), não foi adicionado ao FreeBSD shpor exemplo até 2000, de maneira portável por 15 anos. velho é provavelmente mais preciso. Observe também que o descritor de arquivo padrão, quando não especificado, está <>em todos os shells, exceto que ksh93foi alterado de 0 para 1 no ksh93t + em 2010 (quebrando a compatibilidade com versões anteriores e a conformidade com POSIX)

Stéphane Chazelas
fonte
2
Você pode explicar o perl -e 'truncate STDOUT, tell STDOUT'? Funciona para mim sem incluir isso. Alguma maneira de conseguir a mesma coisa sem usar Perl?
Aaron Blenkush
11
@AaronBlenkush, veja editar.
Stéphane Chazelas
11
Absolutamente brilhante - obrigado. Eu estava lá então, mas não me lembro disso .... Uma referência para o padrão "36 anos" seria divertida, pois não é mencionada em en.wikipedia.org/wiki/Bourne_shell . E para que foi usado? Vejo uma referência a uma correção de bug no SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). que é uma dica.
Nelmcb
2
@nealmcb, veja editar.
Stéphane Chazelas
@ StéphaneChazelas Como sua solução se compara a esta resposta ? Aparentemente, faz a mesma coisa, mas parece mais simples.
akhan 3/01
9

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

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, nem como definir seu novo tamanho (se menor que o antigo). Mas isso é facilmente remediado, tão facilmente que me surpreende que não esteja entre os utilitários padrão /usr/bin.

Isso funciona:

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

Como faz isso (dica de chapéu 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 que você não possui / usr / bin / ftruncate . Para algumas dezenas de linhas de C, você pode ver abaixo. Esse 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 (1º exemplo)

  • abre o descritor de arquivo 4 Tpara atualização. Assim como em open (2), abrir o arquivo dessa maneira posiciona o deslocamento atual em 0.
  • O grep processa Tnormalmente e o shell redireciona sua saída para o Tdescritor 4.
  • ftruncate chama ftruncate (2) no descritor 4, configurando o comprimento para o valor do deslocamento atual (exatamente onde grep o deixou).

O 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;
}

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

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

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

James K. Lowden
fonte
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
Glenn Jackman
fonte
Eu gosto da idéia, mas a menos que diferentes edversões comportar de maneira diferente ..... isto é de man ed(GNU Ed 1.4) ...If invoked with a file argument, then a copy of file is read into the editor's buffer. Changes are made to this copy and not directly to file itself.
Peter.O
@fred, se você estiver sugerindo que salvar as alterações não afetará o arquivo nomeado, você está incorreto. Interpreto essa citação para dizer que suas alterações não são refletidas até que você as salve. Eu admito que ednão é uma solução gool para editar arquivos de 35 GB, já que o arquivo é lido em um buffer.
Glenn Jackman
2
Eu estava pensando que isso significava que o arquivo completo seria carregado no buffer .. mas talvez apenas as seções neeeds sejam carregadas no buffer .. Estou curioso sobre o assunto há algum tempo ... pensei nisso. poderia fazer edição in situ ... Vou apenas tentar um arquivo grande ... Se funcionar, é uma solução razoável, mas enquanto escrevo, estou começando a pensar que isso pode ser o que inspirou o sed ( livre de trabalhar com grandes blocos de dados ... Notei que 'ed' pode realmente aceitar entrada de fluxo de um script (prefixado com !), para que possa ter mais alguns truques interessantes na manga.
Peter.O
Tenho certeza de que a operação de gravação edtrunca o arquivo e o reescreve. Portanto, isso não alterará os dados no disco no local, conforme o OP desejar. Além disso, não funcionará se o arquivo for muito grande para ser carregado na memória.
Nick Matteo
5

Você pode usar uma festança de leitura / gravação descritor de arquivo para abrir o arquivo (para substituí-lo in-situ), em seguida, sede truncate... mas é claro, não nunca permitir que as alterações sejam maiores do que a quantidade de dados lido até agora .

Aqui está o script (usa: bash variable $ 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 do 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
Peter.O
fonte
3

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

bmcnett
fonte
3
+1, mas apenas porque a ampla disponibilidade de CPUs e sistemas operacionais de 64 bits torna possível fazer isso com um arquivo de 35 GB agora. Os que ainda estão em sistemas de 32 bits (a grande maioria até da audiência deste site, eu suspeito) não poderão usar esta solução.
Warren Young
2

Não exatamente no local, 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 o que for) 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
Ed Randall
fonte
2
Mas certamente o gzip está gravando a versão compactada no disco antes de substituí-la pela versão compactada; portanto, você precisa de pelo menos esse espaço extra, ao contrário das outras opções. Mas é mais seguro, se você tem o espaço (o que eu não ....)
nealmcb
Esta é uma solução inteligente que pode ser ainda mais optimizado para executar apenas uma compressão em vez de dois:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen
0

Para o benefício de qualquer pessoa pesquisando essa questão no Google, a resposta correta é parar de procurar por recursos obscuros do shell que correm o risco de danificar seu arquivo para obter ganhos insignificantes de desempenho e, em vez disso, use alguma variação desse padrão:

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

Somente na situação extremamente incomum que, por algum motivo, não é viável, você deve considerar seriamente qualquer uma das outras respostas nesta página (embora elas certamente sejam interessantes de ler). Admito que o dilema 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 exemplo, conforme fornecidas por @Ed Randall e @Basile Starynkevitch.

Todd Owen
fonte
11
Posso entender mal, mas não tem nada a ver com o que o OP originalmente pediu. aka edição embutida do bigfile sem ter espaço em disco suficiente para arquivo temporário.
Kiwy #
@Kiwy É uma resposta dirigida a outros telespectadores desta pergunta (dos quais foram quase 15.000 até agora). A pergunta "Existe uma maneira de modificar um arquivo no local?" tem relevância mais ampla que o caso de uso específico do OP.
Todd Owen
-3

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

user54620
fonte
3
Isso não funciona se o arquivo for grande e os greppeddados excederem o comprimento permitido pela linha de comando. em seguida, corrompe os dados
Anthon