18

I have written a program which takes a file as input and whenever it finds a line with length > 80, it adds \ and \n to that file to make it 80 chars in width max.

The problem is that I have used fseek to insert \ and \n whenever the length exceeds 80, so it overrides two characters of that line which exceeds length 80. Is there a way using which I can insert text without overriding the existing text?

Here is my code:-

#include<stdio.h>
#include<string.h>

int main(int argc, char *argv[])
{
  FILE *fp1,*fp2;
  int prev=0,now=0;
  char ch;
  int flag=0;
  long cur;
  fp1=fopen(argv[1],"r+");
  if(fp1==NULL){
    printf("Unable to open the file to read. Program will exit.");
    exit(0);
  }
  else{
    while((ch=fgetc(fp1))!=EOF){
      if(ch!=' ' && ch!='\n'){
        now=now+1;
      }
      else{
        if(now>=80){
            fseek(fp1,cur,SEEK_SET);
            fputc('\\',fp1);
            fputc('\n',fp1);
            now=0;
            continue;
        }
        if(ch=='\n'){
          flag=0;
          now=0;
          continue;
          }
        else{
          prev=now;
          cur=ftell(fp1);
        }
        now=now+1;
      }
    }
  }
  fclose(fp1);
  return 0;
}

To run it, you need to do following:-

user@ubuntu$ cc xyz.c
user@ubuntu$ ./a.out file_to_check.txt
Rahul
  • 2,515
  • 3
  • 25
  • 29
  • 3
    No, there is no way to insert data into the middle of a file. You need to handle "shifting" the data yourself, and/or write to a new file. – Mat Jan 27 '12 at 12:10
  • 3
    @Mat: I would flag your comment as "Not a comment", since it's an answer :) – Armen Tsirunyan Jan 27 '12 at 12:10
  • 2
    possible duplicate of [How do I insert and delete some characters in the middle of a file?](http://stackoverflow.com/questions/2431073/how-do-i-insert-and-delete-some-characters-in-the-middle-of-a-file) – Mat Jan 27 '12 at 12:11
  • @ArmenTsirunyan: that answer's been made dozens of times... – Mat Jan 27 '12 at 12:11

6 Answers6

28

While there are a couple of techniques to do it in-place, you're working with a text file and want to perform insertions. Operating systems typically don't support text file insertions as a file system primitive and there's no reason they should do that.

The best way to do that kind of thing is to open your file for reading, open a new file for writing, copy the part of the file before the insertion point, insert the data, copy the rest, and then move the new file over the old one.

This is a common technique and it has a purpose. If anything goes wrong (e.g. with your system), you still have the original file and can repeat the transaction later. If you start two instances of the process and use a specific pattern, the second instance is able to detect that the transaction has already been started. With exclusive file access, it can even detect whether the transaction was interrupted or is still running.

That way is much less error prone than any of the techniques performed directly on the original file and is used by all of those traditional tools like sed even if you ask them to work in-place (sed -i). Another bonus is that you can always rename the original file to one with a backup suffix before overwriting it (sed offers such an option as well).

The same technique is often used for configuration files even if your program is writing an entirely new version and doesn't use the original file for that. It hasn't been long since many internet magazines claimed that ext4 accidentally truncates configuration files to zero length. This was exactly because some applications kept the configuration files open and truncated while the system was forcedly shut down. Those application often tampered with the original configuration files before they had the data ready and then even kept them open without syncing them, which made the window for data corruption much larger.

TL;DR version:

When you value your data, don't destroy it before you have the replacement data ready.

Pavel Šimerda
  • 5,783
  • 1
  • 31
  • 31
8

No, there's no way to insert characters into an existing file. You will need to use a second file to do that.

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • 1
    This is not completely true. You can seek(), fread(), seek() and fwrite() again. Of course therefore the tail has to fit into the memory. – ckruse Jan 27 '12 at 12:16
  • 1
    @ckruse: That, and you also risk destroying the original file if the `fwrite()` doesn't complete for some reason. I therefore do not consider this approach to be a feasible solution. – NPE Jan 27 '12 at 12:17
  • 3
    The tail does not have to fit in memory. You can seek to the end and shift characters from the end backwards. If you don't care about it being pathologically slow, a single-char buffer will suffice. In practice, you want a buffer of at least a few KB, but you definitely don't need to store the whole tail in memory. – R.. GitHub STOP HELPING ICE Jan 27 '12 at 13:46
  • 3
    You only need as much memory as the bytes you are inserting; that's how much must be read ahead to allow inserting data without destroying it. – Ioan Jan 27 '12 at 16:02
  • 1
    Actually, @R.. 's one byte of buffer was correct if you can accept pathological slowness and risk of destructive failure: You can inject one character from the payload at a time. – MickLH Feb 21 '15 at 20:44
  • @MickLH - Well the risk of destructive failure should stop anyone in their tracks. And reducing the buffer size only increases the time window for failures. – Stephen C Sep 22 '17 at 23:59
  • 1
    @StephenC The risk of destructive failure can be rigorously mitigated by the use of transaction journaling, so the point stands that *this answer is wrong*. You absolutely can insert data into a file without the use of a second file, and there is no unremovable risk of destruction attached to doing so. – MickLH Sep 26 '17 at 15:50
4

This is the function I use for this kind of thing:

int finsert (FILE* file, const char *buffer) {

    long int insert_pos = ftell(file);
    if (insert_pos < 0) return insert_pos;

    // Grow from the bottom
    int seek_ret = fseek(file, 0, SEEK_END);
    if (seek_ret) return seek_ret;
    long int total_left_to_move = ftell(file);
    if (total_left_to_move < 0) return total_left_to_move;

    char move_buffer[1024];
    long int ammount_to_grow = strlen(buffer);
    if (ammount_to_grow >= sizeof(move_buffer)) return -1;

    total_left_to_move -= insert_pos;

    for(;;) {
        u16 ammount_to_move = sizeof(move_buffer);
        if (total_left_to_move < ammount_to_move) ammount_to_move = total_left_to_move;

        long int read_pos = insert_pos + total_left_to_move - ammount_to_move;

        seek_ret = fseek(file, read_pos, SEEK_SET);
        if (seek_ret) return seek_ret;
        fread(move_buffer, ammount_to_move, 1, file);
        if (ferror(file)) return ferror(file);

        seek_ret = fseek(file, read_pos + ammount_to_grow, SEEK_SET);
        if (seek_ret) return seek_ret;
        fwrite(move_buffer, ammount_to_move, 1, file);
        if (ferror(file)) return ferror(file);

        total_left_to_move -= ammount_to_move;

        if (!total_left_to_move) break;

    }

    seek_ret = fseek(file, insert_pos, SEEK_SET);
    if (seek_ret) return seek_ret;
    fwrite(buffer, ammount_to_grow, 1, file);
    if (ferror(file)) return ferror(file);

    return 0;
}

Use it like this:

FILE * file= fopen("test.data", "r+");
ASSERT(file);

const char *to_insert = "INSERT";

fseek(file, 3, SEEK_SET);
finsert(file, to_insert);

ASSERT(ferror(file) == 0);
fclose(file);

This (as others here have mentioned) can theoretically corrupt a file if there is an error, but here is some code to actually do it... Doing it in-place like this is usually fine, but you should backup the file if you are worried about it...

Dave Butler
  • 1,646
  • 1
  • 12
  • 18
1

No, there is no way. You have to create a new file or move the contents of the file 2 characters backwards.

ckruse
  • 9,642
  • 1
  • 25
  • 25
0

another implementation use tmpfile()

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

FILE *tmp_buf;
int finsert(FILE *f, const char* msg){
    fseek(tmp_buf, 0, SEEK_SET);
    fpos_t f_pos;
    assert (fgetpos(f, &f_pos)==0);

    char buf[50];
    while(fgets(buf, 50, f))
        fputs(buf, tmp_buf);

    long tmp_buf_pos = ftell(tmp_buf);

    fsetpos(f, &f_pos);
    fputs(msg, f);
    fseek(tmp_buf, 0, SEEK_SET);

    while(--tmp_buf_pos>=0)
        fputc(fgetc(tmp_buf), f);

    return ferror(f);
}

int main()
{
    FILE *f = fopen("result.txt", "wb+");
    assert (f!=NULL);
    fputs("some text", f);
    tmp_buf = tmpfile();
    assert (tmp_buf!=NULL);
    assert(finsert(f, "another text")==0);
    fclose (f);
    perror("");
}

tested in Cygwin64

ianfun
  • 411
  • 5
  • 10
0

You can load the file as chunks (in your case is 80 characters) and then append two character (new line) and write the content into anohter file.

Ahmed
  • 7,148
  • 12
  • 57
  • 96