【问题标题】:Attempting to 'insert' or 'add' into a text file - one small problem尝试“插入”或“添加”到文本文件中 - 一个小问题
【发布时间】:2020-09-05 17:22:47
【问题描述】:

提供代码 M.R.E。

症状是'简单':

  1. 尝试hello\nhello\nhelloEOF 并插入'_'@ln=2col=1(或者更确切地说是任何line>1 && col==1)程序好奇地做对了,但吃掉了@ 987654327@ ,导致第 2 行与第 1 行 融合 - hello_hello\nhelloEOF ,而不是 hello\n_hello\nhelloEOF
  2. 尝试在文件hello\nhello\nhelloEOF 的最后一行中使用一个字符并插入'_'@ln=3col=5(或者更确切地说是在由EOF 终止的行中的任何字符col>1)导致它跳过了该行的最后一个 actual 字符:hello\nhello\nhell_EOF 而不是 hello\nhello\nhell_oEOF
  3. 尝试使用最后一行中的第一个字符(col==1 在由 EOF 终止的行中)会导致抛出错误消息 (\n Invalid Index.)。

就我的直觉而言,我认为在处理线路终止 ('\n',EOF) 及其计数方式以及一般的循环计数器时存在缺陷,但我不明白.

想法/算法:

  • 从用户处获取源文件
  • 从用户处获取索引(行、列)
  • 从源文件复制到 tmp 文件直到(用户的索引 -1 col)。
  • 现在,允许用户将字符串写入 tmp 文件的缓冲区。
  • 一旦用户终止输入,从用户索引复制到 tmp 直到 EOF
  • 重命名/删除+重命名 tmp 作为用户源文件的名称。

下面的代码做了所有真正的谈话:

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

/* Likely error region in chknmove(), chkncpy() and/or main() ;
   All other functions used are also provided */

long long lncnt(FILE *lcount){
/* counts the number of lines in a file with fgets() */
    rewind(lcount); /* ensures lines counted from beginning */
    long long lines=0; char line[501];
    while((fgets(line,501,lcount))!=NULL){
        lines++;
    }
    rewind(lcount); /* leaves fptr @ 0,0 rather than EOF */ 
    return lines; 
}
int chknmove(FILE *tomove, long long line, long long col){
/* Function to check & move FILE* to certain line/col */
    rewind(tomove); /* make sure fptr is at beginning of file */
    int f=0; /* set check-variable to 0 for successful */
    if(line<1 || col<1)
        f = -1; /* Ln,col cannot be -ve or even 0 -> 1st col is @ 1ln,1col in any non-empty file */
    else{
        long long i,q; i=1;q=0; /* i = lines seen, q = cols seen */
        /* i init to 1 for ease of comprehension v/s testing 'line-1' */
        while(i<line || q<col){ /* loop must iterate until i==line && q==col */
            int ch = fgetc(tomove); /* int to store EOF */
            if(ch==EOF){
                f=-1; break; /* if file ends before reaching index, failed. */
            }
            else if(ch=='\n') {
                if(i==line){
                    f=-1; break;/* if line ends before reaching col in ln, failed. */
                }
                i++; q=0; /* else , increase i by 1 and set q (col) to 0 */
            }
            else
                q++; /* any other character is 'normal' , just increment q (col) by 1 */
        }
    }
    if(f==0){ 
        fseek(tomove,-1,SEEK_CUR); 
        /* since index must be checked, loop reaches it -> fseek(-1) causes *next* r/w to fptr to be on the index . */
        return 0;
    }
    else{
        /* f != 0 : moving has failed. */
        printf("\n Invalid Index.\n");
        return -1;
    }
}
int chkncpy(FILE *source, FILE *dest,long long beginln, long long begincol, long long endln, long long endcol){
    rewind(source); int f=0;
    if(beginln<1 || begincol<1 || endln<beginln || endcol<1 || (beginln==endln && endcol<begincol))
        f=-1; /* line/col must be >= 1 in non-empty file, copying done top to bottom (begin-index <= end-index ) */
    else{
        if(chknmove(source,beginln,begincol)==0){
        /* loop begins if begin-index is valid */
            long long i,q; i=beginln;q=begincol; /* i = lines, q = cols */
            while(i<endln || q<endcol){
                /* while end-index is not reached : !(i==endln && q==endcol) */
                int ch = fgetc(source);
                if(ch==EOF) {f=-1; break;} /* File ends b/w begin-index & end-index - failed. */
                else if(ch=='\n') {
                    if(i==endln) {f=-1; break;} /* endln ends before reaching endcol - failed */
                    i++; q=0;
                    fputc(ch,dest); /* valid '\n' put in dest file */
                }
                else{
                    q++; fputc(ch,dest); /* valid char put in dest file */
                }
            }
        }
        else f=-1; /* if begin-index is invalid, failed. */
    }
    if(f==-1){
        /* if f != 0 : chkncpy() failed */
        printf("\n Invalid Index.\n");
        return -1;
    }
    else /* if f remains == 0 */ return 0;
}
long long lastlncol(FILE *fyl){
    rewind(fyl); /* makes sure fptr @ beginning */
    chknmove(fyl,lncnt(fyl),1); /* move to last line */
    long long cnt=0; /* cnt stores no. of chars in last line */
    while(1){
        int g = fgetc(fyl);
        if (g==EOF)
            break; /* EOF not counted */
        else if(g=='\n'){
                cnt++;break; /* \n is counted as a char, but also as EOL */
            }
        else
            cnt++; /* all other chars counted */
    }
    rewind(fyl); /* leaves file at its beginning */
    return cnt;
}
long long lcc(FILE *fyl,long long line){
    rewind(fyl); int f = chknmove(fyl,line,1); /* moves fptr to line */
    long long cnt=0;
    if(f==0){
        while(1){
            int g = fgetc(fyl);
            if (g==EOF)
                break;
            else if(g=='\n'){
                cnt++;break;
            }
            else
                cnt++;
        }
        rewind(fyl);
        return cnt;
    }
    else
        return -1;    
}
void writer(FILE *write, int ctrl){
    printf("\n Terminate Input with \"/end/\".\n\n   Type below :\n\n");
    char in[501]; /* str that stores input line-by-line */
    char *p; int o;
    while(1){
        fgets(in,501,stdin); /* takes line from user */
        if((p=strstr(in,"/end/"))!=0){ 
            /* if line has "/end/", all chars till /end/ are written to file and input loop breaks */
            o = p-in;
            fprintf(write,"%.*s",o,in);
            break;
        }
        else{
            /* writes line to file */
            fputs(in,write);
        }
    }
    if(ctrl==0){
        /* in some cases, file must not be closed yet , hence ctrl is taken */
        int s = fclose(write);
        if(s==EOF) printf("\n Error ");
        else printf("\n Success.\n");
    }
}
void eat() /* clears stdin */
{
    int eat;while ((eat = getchar()) != '\n' && eat != EOF);
}

int main(){
    /* main to add/insert to file @ given index */
    char fadd[501];/* filename str */ 
    printf("\n Filename : "); scanf("%500[^\n]",fadd);
    FILE * add = fopen(fadd,"r");
    if(add==NULL)
        perror("\n Error "); /* if file does not pre-exist */
    else{
        long long line, col; char sep; printf("\n Index : "); scanf("%lld%c%lld",&line,&sep,&col); /* takes index in ln-char-col form */
        eat(); /* clears stdin */
        FILE * tmp=fopen("Temp.Ctt","w"); /* opens a tmp file to write */
        if(tmp==NULL)
            perror("\n Error "); /* failed - tmp file could not be created to write */
        else{
            int f; /* success var */
            if(line==1 && col>1){
                /* if user wants to insert @ a col>1 in line 1 */
                f = chkncpy(add,tmp,1,1,line,col); /* all below calls intend to write till 1 char before the char @ given index */
            }
            else if(line>1 && col>1){
                /* if user wants to insert @ a col>1 in any line>1*/
                f = chkncpy(add,tmp,1,1,line,col-1);
            }
            else if(line>1 && col==1){ //ERRORS - ignores the '\n' of line-1
                /* if user wants to insert @ a 1st col in line>1 */
                f = chkncpy(add,tmp,1,1,line-1,lcc(add,line-1));
            }
            else if(line==1 && col==1){
               /* if user wants to insert @ 1,1 (no moving/copying needed) */
                f=0;
            }
            else{
                printf("\n Invalid Index.\n");f=-1;
            }
            if(f==0){
                writer(tmp,1); 
                /* calls function to allow user to write to fptr , with ctrl != 0, so writer() *won't* fclose(tmp) */
                chkncpy(add,tmp,line,col,lncnt(add),lastlncol(add));
                /* copies all characters from index till EOF */
                int ok = fclose(tmp); fclose(add);
                if(ok==EOF){
                    /* if closing tmp was unsuccessful, the file on disk may be corrupted/incomplete, so must be removed */ 
                    remove("Temp.Ctt");perror("\n Error ");

                }
                else{
                    if(rename("Temp.Ctt",fadd)==0)
                        printf("\n Success.\n");
                    else{
                        /* on Windows & some other non-POSIX systems, file cannot be renamed to pre-existing filename , hence delete original */
                        remove(fadd);
                        if(rename("Temp.Ctt",fadd)==0)
                            printf("\n Success.\n");
                        else{
                            /* if rename still unsuccessful, throw an error, remove tmp file and give up */
                            remove("Temp.Ctt");
                            perror("\n Error ");
                        }  
                    }
                }

            }
        }
    }
}

如果有任何遗漏的细节或无意的错误,请发表评论,我会更正。

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • @SamuelLiew 如果评论线程看起来有点大,除了版主干预之外,我们有没有办法将它移动到我们自己聊天?

标签: c file file-io file-pointer


【解决方案1】:

你的代码太复杂了:

  • 应将复制文件开头的不同情况合并到一个函数调用中,该函数调用将数据复制到line 行上col 列处的字节并排除。

  • 然后你从用户输入中复制。

  • 最后复制输入文件的其余部分。

  • 代码假定行和列从1 开始编号。这可能很明显,但您应该指定这一点,因为它可能对每个人都不是显而易见的。

  • 这种风格很难阅读:您应该在二元运算符周围、关键字之后、,; 以及{ 之前使用水平空格。将多个语句塞进一行也不是一个好主意。

这是一个简化版本。通常,大部分代码用于错误处理:

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

void flush_line(FILE *fp) { /* read the rest of the current line */
    int c;
    while ((c = getc(fp)) != '\n' && c != EOF)
        continue;
}

int copy_file(FILE *from, FILE *to) {
    int c;
    while ((c = getc(from)) != EOF) {
        if (putc(c, to) == EOF)
            return EOF;
    }
    return 0;
}

int copy_lines(FILE *from, int line1, int col1, FILE *to) {
    int c, line = 1, col = 1;
    for (;;) {
        if (line >= line1 || (line == line1 && col >= col1))
            break;
        if ((c = getc(from)) == EOF)
            break;
        if (putc(c, to) == EOF)
            return EOF;
        col++;
        if (c == '\n') {
            line++;
            col = 1;
        }
    }
    return 0;
}

int writer(FILE *write) {
    char in[501]; /* str that stores input line-by-line */
    char *p;

    printf("\n Terminate Input with \"/end/\".\n\n   Type below :\n\n");
    while (fgets(in, 501, stdin)) {
        if ((p = strstr(in, "/end/")) != NULL) {
            /* if line has "/end/", all chars till /end/ are written to file and input loop breaks */
            int o = p - in;
            if (fprintf(write, "%.*s", o, in) < 0)
                return EOF;
            break;
        } else {
            /* writes line to file */
            if (fputs(in, write) < 0)
                return EOF;
        }
    }
    return 0;
}

int main() {
    /* main to add/insert to file @ given index */
    const char *temp_filename = "Temp.Ctt";
    char fadd[501];/* filename str */

    printf("\n Filename : ");
    if (scanf("%500[^\n]", fadd) != 1)
        return 1;
    flush_line(stdin);

    FILE *add = fopen(fadd, "r");
    if (add == NULL) {
        perror("\n Cannot open input file");
        return 1;
    }
    int line, col;
    /* read the index: 1 based line and column numbers */
    printf("\n Index : ");
    if (scanf("%d%*c%d", &line, &col) != 2) {
        fprintf(stderr, "invalid input\n");
        return 1;
    }
    flush_line(stdin);
    FILE *tmp = fopen(temp_filename, "w");
    if (tmp == NULL) {
        perror("\n Cannot create temporary file");
        fclose(add);
        return 1;
    }
    if (copy_lines(add, line, col, tmp)) {
        perror("\n Error copying beginning of file");
        fclose(add);
        fclose(tmp);
        remove(temp_filename);
        return 1;
    }
    if (writer(tmp)) {
        perror("\n Error writing user input");
        fclose(add);
        fclose(tmp);
        remove(temp_filename);
        return 1;
    }
    if (copy_file(add, tmp)) {
        perror("\n Error copying remaining file contents");
        fclose(add);
        fclose(tmp);
        remove(temp_filename);
        return 1;
    }
    fclose(add);
    if (fclose(tmp) < 0) {
        /* if closing tmp was unsuccessful, the file on disk may be corrupted/incomplete, so must be removed */
        perror("\n Error closing temporary file");
        remove(temp_filename);
        return 1;
    }
    if (rename(temp_filename, fadd) == 0) {
        printf("\n Success.\n");
        return 0;
    }
    /* on Windows & some other non-POSIX systems, file cannot be renamed to pre-existing filename , hence delete original */
    if (remove(fadd)) {
        perror("\n Cannot remove input file");
        remove(temp_filename);
        return 1;
    }        
    if (rename(temp_filename, fadd) == 0) {
        printf("\n Success.\n");
        return 1;
    }
    /* if rename still unsuccessful, try and copy contents */
    tmp = fopen(temp_filename, "r");
    if (tmp == NULL) {
        perror("\n Cannot re-open temporary file, output is in Temp.Ctt");
        return 1;
    }
    add = fopen(fadd, "w");
    if (add == NULL) {
        perror("\n Cannot re-open input file, output is in Temp.Ctt");
        fclose(tmp);
        return 1;
    }
    if (copy_file(tmp, add)) {
        perror("\n Error copying temporary file to input file");
        fclose(add);
        fclose(tmp);
        return 1;
    }
    fclose(tmp);
    if (fclose(add) < 0) {
        perror("\n Error closing input file");
        return 1;
    }
    /* throw an error, remove tmp file and give up */
    remove(temp_filename);
    printf("\n Success.\n");
    return 0;
}

【讨论】:

  • 好吧,这似乎解决了我的代码中的大部分丑陋问题——除了:它将\n 视为一个重置点,这意味着我不能'append'到一行(可能无法写信给line &gt; 1 col==1),这也是这里发生的相同问题(stackoverflow.com/a/63730935/13863346) - 我们需要也将\n计为一个字符,并且重置@ next 行的 first char !关于如何做到这一点的任何建议?
  • @user13863346:索引为插入点:1,1表示在第1行的开头插入文本。如果第1行有8个字符ABCDEFGH,则在索引处插入1,9追加到行1,即在第一行的H和换行符\n之间插入用户输入的文本。在1,10 处插入与2,02,1 的效果相同。可以更改代码以使1,10 的行为与1,9 相同,并修改测试:if (c == '\n') { if (line == line1) { ungetc(c, fp); break; } else { line++; col = 1; }}
  • 非常感谢您的回答。我既不能确认也不能否认我的答案是“太复杂”的指控;)我已经修复了我的代码,并将其作为第二个答案发布 - 对我来说,我更正的代码还可以,无论如何是一个稍微大一点的项目的一部分,所以\_(0_0)_/。也许如果你有时间,看看它!再次感谢。
  • @user13863346:研究我的copy_lines 函数,它更简单并且可以处理所有情况。您可以轻松修改它以拒绝 invalid 索引值。
【解决方案2】:

这是我的自我回答chknmove()chkncpy() 的问题导致“故障”,这些问题已在下面修复。

下面是可以编译的,并且注释和间隔很大,因此实际上比封装在chknmove()chkncpy()中的主要思想更大——所以这些有被放在代码的顶部。

希望这个问题/答案在未来对其他人有用。

#include <stdio.h> // For file & console I/O
#include <string.h> // For strstr() in writer()

int chknmove(FILE *tomove, long long line, long long col){
    /* Moves FILE* to given ((line,col) -1 char) , such that next char read from FILE* will be @ (line,col)
     Checks validity of index as it moves : if col not in line || EOF encountered, returns -1. */

    rewind(tomove); // rewind file 'just in case'

    int f = 0 ; // control variable which stores state (succeeded/failed) of chknmove()

    if (line < 1 || col < 1) 
    {
        f=-1; 
        printf("\n Illegal Index.\n");
        // Follows 1-based line/col index : -ve values are illegal
        return -1;
    }
    else {
        long long i,q; i = q = 0; // i = lines encountered ; q = chars encountered in line i ; both are 0-based

        while(i < line){
            int chk = fgetc(tomove); // 
            if(chk == EOF){
                printf("\nInvalid %s - beyond EOF.\n", (i == line -1 ) ? "Column" : "Line"); 
                f = -1; break;
            }
            else if(chk == '\n'){
                if(i==line-1 && q == col-1) 
                /*  1. This allows for user to directly point to the '\n' char , allowing him to append to line 
                    2.(line/col - 1) : since i & q are 0-based   */
                    break;
                else{
                    if(i == line-1 ){
                        // except if target index was the '\n' , reading beyond newline @ target line is invalid, since '\n' terminates line
                        printf("\nInvalid column for line %lld.\n",line); 
                        f=-1; break;
                    }
                    i++; q=0; // if not @ target line , reset and continue 
                }
            }
            else if(i == line-1  && q == col-1 ) // if dest index reached, break .
                break;
            else // if non-EOF , non-\n char encountered, increment q and continue.
                q++;
        }

        if(f==0){
            fseek(tomove,-1,SEEK_CUR); // So that the after returning/exiting chknmove() , the char read from FILE* is @ line,col
            return 0;
        }
        else
            return -1;
    }
}
int chkncpy(FILE* source, FILE *dest, long long beginln, long long begincol, long long endln, long long endcol) {
    /* Copies everything from FILE *source to FILE *dest within begining index and terminating index , if they're valid
        Returns -1 if they're invalid.*/

    if (beginln < 1 || begincol < 1 || endln < beginln || endcol < ((endln == beginln) ? begincol : 1))
        // -ve indexes and reading/writing backwards is illegal
        return -1;

    long long i, q; // i -> lines && q -> chars 
    int f=0;
    if(chknmove(source,beginln,begincol)==0){ 
        // checked if begining index is valid and if so, moved to it.

        i=beginln; q=begincol; // i & q have same base as line & col , so 1-based

        while(1){
            int ch = fgetc(source);
            if(ch==EOF){
                printf("\nInvalid Terminating Index.\n");
                f=-1; break;
            }
            else if(ch=='\n'){
                if(i==endln && q==endcol){
                    fputc(ch,dest);
                    break;
                }
                else{
                    if(i==endln){
                        printf("Invalid column for line %lld.\n",endln);
                        f=-1; break;
                    }
                    i++; q=1; // q set to 1 -> q is 1-based !
                    fputc(ch,dest);
                }
            }
            else if(i==endln && q==endcol){
                fputc(ch,dest); break;
            }

            else{
                q++; fputc(ch,dest);
            }
        }
    }
    else
        f=-1;
    
    if(f==0)    return 0;
    else    return -1;
}
long long lcc(FILE *fyl,long long line){ 
    // L.C.C. == line char count , i.e, count of chars in a line (including the \n).

     int f = chknmove(fyl,line,1); /* attempt moving to line, store returned val */

    long long cnt=0; // cnt -> number of chars found 
    if(f==0){ // if line exists , then :
        while(1){
            int g = fgetc(fyl);
            if (g==EOF) // EOF checked in case line is last line
                break;
            else if(g=='\n'){ // '\n' is EOL , hence it is counted and then loop is terminated .
                cnt++; break;
            }
            else
                cnt++;
        }
        rewind(fyl);
        return cnt;
    }
    else
        return -1; // if line doesn't exist, return -1    
}
int clone(FILE *wfrm,FILE *wto){
    // clones wfrom ("Write From ") onto wto ("Write To") until EOF is encountered.

    while(1){
        int a =fgetc(wfrm);
        if(a==EOF)
            break;
        else
            fputc(a,wto);                    
    }

    return 0;
}
void writer(FILE *write){
    // Allows basic console line-level I/O for writing to FILE * 

    printf("\n Terminate Input with \"/end/\".\n\n\tType below :\n\n");
    char in[501]; /* str that stores input line-by-line */
    char *p; int o;
    while(1){
        fgets(in,501,stdin); /* takes line from user */
        if((p=strstr(in,"/end/"))!=0){ 
            /* if line has "/end/", all chars till /end/ are written to file and input loop breaks */
            o = p-in;
            fprintf(write,"%.*s",o,in);
            break;
        }
        else{
            /* writes line to file */
            fputs(in,write);
        }
    }
}
void eat() /* clears stdin */
{
    int eat;while ((eat = getchar()) != '\n' && eat != EOF);
}
int main(){
    /* main to add/insert to file @ given index */

    char fadd[501]=""; // String to store fname
    printf("\n Filename : "); scanf("%500[^\n]",fadd); eat(); // Take fname, clear stdin .
    FILE * add = fopen(fadd,"r"); // open file

    if(add==NULL)
        perror("\n Error "); 
    else{ 
        // If File is loaded for reading successfully

        long long line, col; char sep; // line, col and seperating char make up the index
        printf("\n Index : "); scanf("%lld%c%lld",&line,&sep,&col); eat(); // take index, clear stdin

        FILE * tmp=fopen("Temp.Ctt","w"); // open a temporary file
        if(tmp==NULL)
            perror("\n Error ");
        else{
            int f;
            if(line>=1 && col>1){ // copy till the line , col-1
                f = chkncpy(add,tmp,1,1,line,col-1);
            }
            else if(line>1 && col==1){ // copy till line-1 , last char  
                f = chkncpy(add,tmp,1,1,line-1,lcc(add,line-1));
            }
            else if(line==1 && col==1){ // no moving/copying necessary at all 
                f=0;
            }
            else{
                printf("\n Invalid Index.\n");f=-1;
            }
            if(f==0){ // if Index was not invalid 

                writer(tmp); // let user write to temp file 
                clone(add,tmp); //clones the rest of add to tmp - both are *not* fclosed
                int ok = fclose(tmp); fclose(add);
                if(ok==EOF){
                    /* if closing tmp was unsuccessful, the file on disk may be corrupted/incomplete, so must be removed */
                    remove("Temp.Ctt");perror("\n Error ");

                }
                else{
                    if(rename("Temp.Ctt",fadd)==0)
                        printf("\n Success.\n");
                    else{
                        /* on Windows & some other non-POSIX systems, file cannot be renamed to pre-existing filename , hence delete original */
                        remove(fadd);
                        if(rename("Temp.Ctt",fadd)==0)
                            printf("\n Success.\n");
                        else{
                            /* if rename still unsuccessful, throw an error, point user to temp file and give up */
                            perror("\n Error ");
                            printf("\n %s lost. File-buffer exists as %s in cwd.\n",fadd,"Temp.Ctt");
                        }  
                    }
                }

            }
            else
                remove("Temp.Ctt");
        }
    }
    return 0;
}

欢迎任何可以改善答案的建设性批评。如有任何不慎的错误或遗漏的细节,请发表评论,我会尽快回复。

【讨论】:

  • @chqrlie 我不确定您所说的 在尝试从文件中读取下一个字节之前是否已达到索引 它是一个循环 - “之前”和'after' 因迭代而异 .... 至于在 1 字节文件中插入 1,2,它会做它应该做的事情:1)将第一个字节复制到 tmp,将用户输入写入 "\end\",然后重命名/remove + 重命名 tmp 太原始了。它没有失败,为什么要失败?
  • @chqrlie 你是说只有EOF 的空文件吗?
  • for(;;)循环体中,first测试当前位置是否为index:if (i == line-1 &amp;&amp; q == column-1) return 0;then读取下一个字节,更新计数器,并拒绝无效的索引值。
  • 我真的很好奇,为什么,@chqrlie .. 我错过了什么?
  • 你是说只有EOF的空文件吗?不,我的意思是一个包含单个字节的文件,比如A,文件大小是1正如lsdir 或发现者所报告的那样。 EOF 不是字节,它不是文件的一部分,它是fgetc() 返回的特殊值,表示由于文件结束或读取错误而无法读取更多字节。
猜你喜欢
  • 2021-05-04
  • 1970-01-01
  • 2021-08-20
  • 2018-04-04
  • 2014-09-15
  • 1970-01-01
  • 1970-01-01
  • 2017-08-27
  • 1970-01-01
相关资源
最近更新 更多