【问题标题】:Removing trailing newline character from fgets() input从 fgets() 输入中删除尾随换行符
【发布时间】:2022-01-05 21:06:15
【问题描述】:

我正在尝试从用户那里获取一些数据并将其发送到 gcc 中的另一个函数。代码是这样的。

printf("Enter your Name: ");
if (!(fgets(Name, sizeof Name, stdin) != NULL)) {
    fprintf(stderr, "Error reading Name.\n");
    exit(1);
}

但是,我发现它最后有一个换行符\n。所以如果我输入John,它最终会发送John\n。如何删除 \n 并发送正确的字符串。

【问题讨论】:

  • if (!fgets(Name, sizeof Name, stdin))(至少不要使用两个否定,! 和 !=)
  • @Roger Pate “不要使用两个否定” --> 嗯,如果我们深入挖掘,“不要”和“否定”都是 否定。 ;-)。也许“使用if (fgets(Name, sizeof Name, stdin)) {
  • @chux,我确定你的意思是if (fgets(Name, sizeof Name, stdin) == NULL ) {
  • @RSahu True: 讨厌 !:

标签: c string gcc newline fgets


【解决方案1】:

也许最简单的解决方案使用我最喜欢的鲜为人知的函数之一,strcspn()

buffer[strcspn(buffer, "\n")] = 0;

如果你想让它也处理'\r'(比如说,如果流是二进制的):

buffer[strcspn(buffer, "\r\n")] = 0; // works for LF, CR, CRLF, LFCR, ...

该函数计算字符数,直到找到'\r''\n'(换句话说,它找到第一个'\r''\n')。如果它没有命中任何东西,它会停在'\0'(返回字符串的长度)。

请注意,即使没有换行符,这也可以正常工作,因为strcspn 会在'\0' 处停止。在这种情况下,整行只是简单地将'\0' 替换为'\0'

【讨论】:

  • 这甚至可以处理罕见的buffer,而不是'\0' 开始,这对buffer[strlen(buffer) - 1] = '\0'; 方法造成了悲痛。
  • @chux:是的,我希望更多的人知道strcspn()。库中更有用的功能之一,IMO。我决定在今天编写并发布一堆像这样的常见 C hack;使用strcspnstrspnstrtok_r 实现是第一个:codepad.org/2lBkZk0w警告:我不能保证它没有错误;它是仓促编写的,可能有很少)。不过,我不知道我会在哪里发布它们,但我打算本着著名的“bit twiddling hacks”的精神来制作它。
  • 研究了稳健 trim fgets()的方法。这个strcspn() 似乎是only 正确的单线。 strlen 更快 - 虽然不是那么简单。
  • @sidbushes:这个问题在标题和内容中都询问了尾随换行符来自fgets() 输入。这也是第一个换行符。
  • @sidbushes:我了解您来自哪里,但我无法对特定字词的 Google 搜索结果负责。与 Google 交谈,而不是我。
【解决方案2】:

优雅的方式:

Name[strcspn(Name, "\n")] = 0;

略丑的方式:

char *pos;
if ((pos=strchr(Name, '\n')) != NULL)
    *pos = '\0';
else
    /* input too long for buffer, flag error */

略显奇怪的方式:

strtok(Name, "\n");

请注意,如果用户输入空字符串(即仅按 Enter),strtok 函数将无法按预期工作。它使\n 字符保持不变。

当然还有其他的。

【讨论】:

  • 任何线程感知的 C 运行时库(也就是说,大多数针对多线程平台的),strtok() 将是线程安全的(它将使用线程本地存储来存储 '通话间状态)。也就是说,使用非标准(但足够常见)strtok_r() 变体通常会更好。
  • 查看我对完全线程安全和可重入变体的回答,类似于您的 strtok 方法(它适用于空输入)。其实实现strtok的一个好办法就是使用strcspnstrspn
  • 如果您所处的环境存在排长风险,请务必处理 else 情况。静默截断输入会导致非常严重的错误。
  • 如果你喜欢单行并且正在使用 glibc,试试*strchrnul(Name, '\n') = '\0';
  • strchr(Name, '\n') == NULL 时,除了“缓冲区输入太长,标志错误”之外,还存在其他可能:stdin 中的最后一个文本没有以'\n' 结尾或罕见的嵌入空字符被读取。
【解决方案3】:
size_t ln = strlen(name) - 1;
if (*name && name[ln] == '\n') 
    name[ln] = '\0';

【讨论】:

  • 如果字符串为空,可能会抛出异常,不是吗?像索引超出范围。
  • @EdwardOlamisan,但是字符串永远不会为空。
  • @James Morris 在不寻常的情况下fgets(buf, size, ....) --> strlen(buf) == 0。 1) fgets() 读作第一个 char'\0'。 2) size == 1 3) fgets() 返回 NULL 然后 buf 内容可以是任何东西。 (虽然 OP 的代码确实测试了 NULL)建议:size_t ln = strlen(name); if (ln > 0 && name[ln-1] == '\n') name[--ln] = '\0';
  • 字符串为空怎么办? ln 将是 -1,除非 size_t 是无符号的,因此写入随机内存。我认为您想使用ssize_t 并检查ln 是否>0。
  • @legends2k:搜索编译时值(尤其是 strlen 中的零值)可以比普通的逐字符搜索更有效地实现。出于这个原因,我认为这个解决方案比基于 strchrstrcspn 的解决方案更好。
【解决方案4】:

下面是从fgets()保存的字符串中删除潜在'\n'的快速方法。
它使用strlen(),进行了2次测试。

char buffer[100];
if (fgets(buffer, sizeof buffer, stdin) != NULL) {

  size_t len = strlen(buffer);
  if (len > 0 && buffer[len-1] == '\n') {
    buffer[--len] = '\0';
  }

现在根据需要使用bufferlen

此方法的附带好处是为后续代码提供len 值。它可以轻松地比strchr(Name, '\n') 更快。 Ref YMMV,但两种方法都可以。


buffer,从原来的fgets()在某些情况下不会包含在"\n"中:
A) 对于buffer,行太长,所以只有'\n' 之前的char 保存在buffer 中。未读字符仍保留在流中。
B) 文件中的最后一行没有以'\n' 结尾。

如果输入在某处嵌入了空字符'\0',则strlen() 报告的长度将不包括'\n' 位置。


其他一些答案的问题:

  1. buffer"\n" 时,strtok(buffer, "\n"); 无法删除'\n'。从这个answer - 在这个答案之后修改以警告这个限制。

  2. fgets() 读取的第一个char'\0' 时,以下情况很少发生。当输入以嵌入的'\0' 开头时会发生这种情况。然后buffer[len -1] 变成buffer[SIZE_MAX] 访问内存肯定在buffer 的合法范围之外。黑客可能会在愚蠢地读取 UTF16 文本文件时尝试或发现某些东西。这是编写此答案时answer 的状态。后来,非 OP 对其进行了编辑,以包含类似此答案的代码,以检查 ""

    size_t len = strlen(buffer);
    if (buffer[len - 1] == '\n') {  // FAILS when len == 0
      buffer[len -1] = '\0';
    }
    
  3. sprintf(buffer,"%s",buffer); 是未定义的行为:Ref。此外,它不保存任何前导、分隔或尾随空格。现在deleted

  4. [稍后编辑answer] 与strlen() 方法相比,除了性能之外,1 班轮buffer[strcspn(buffer, "\n")] = 0; 没有问题。考虑到代码正在执行 I/O - CPU 时间的黑洞,修剪中的性能通常不是问题。如果下面的代码需要字符串的长度或高度注重性能,请使用此strlen() 方法。否则strcspn() 是一个不错的选择。

【讨论】:

  • 感谢您的帮助。当使用malloc动态分配缓冲区大小时,我们可以使用strlen(buffer)吗?
  • @Rrz0 buffer = malloc(allocation_size); length = strlen(buffer); 错误 - buffer 指向的内存中的数据未知。 buffer = malloc(allocation_size_4_or_more); strcpy(buffer, "abc"); length = strlen(buffer); 可以
【解决方案5】:

如果每一行都有'\n',则直接从fgets输出中删除'\n'

line[strlen(line) - 1] = '\0';

否则:

void remove_newline_ch(char *line)
{
    int new_line = strlen(line) -1;
    if (line[new_line] == '\n')
        line[new_line] = '\0';
}

【讨论】:

  • 请注意,使用strnlen 代替strlen 将是safer
  • 对链接问题中第一个答案的评论指出“请注意 strlen()、strcmp() 和 strdup() 是安全的。'n' 替代方案为您提供了额外的功能。”
  • @esker 不,它不会。插入n 并不会神奇地提高安全性,在这种情况下,它实际上会使代码更加危险。与strncpy 类似,这是一个非常不安全的函数。您链接到的帖子是不好的建议。
  • 这对于空字符串 ("") 来说非常失败。同样strlen() 返回size_t 而不是int
  • 这对于空字符串是不安全的,它将写入索引 -1。不要使用这个。
【解决方案6】:

对于单个 '\n' 修剪,

void remove_new_line(char* string)
{
    size_t length = strlen(string);
    if((length > 0) && (string[length-1] == '\n'))
    {
        string[length-1] ='\0';
    }
}

用于多个 '\n' 修剪,

void remove_multi_new_line(char* string)
{
  size_t length = strlen(string);
  while((length>0) && (string[length-1] == '\n'))
  {
      --length;
      string[length] ='\0';
  }
}

【讨论】:

  • 当您可以简单地使用&& 编写一个条件时,为什么要嵌套ifwhile 循环有一个奇怪的结构;它可能只是while (length > 0 && string[length-1] == '\n') { --length; string[length] = '\0'; }
  • @melpomene 感谢您的建议。更新代码。
  • 我建议将第一个函数更自然地定义为:size_t length = strlen(string); if (length > 0 && string[length-1] == '\n') { string[length-1] = '\0'; }。这也更好地反映了第二个定义(仅使用if 而不是while)。
  • @elpomene 谢谢。这说得通。我更新了代码。
【解决方案7】:

我的新手方式 ;-) 请让我知道这是否正确。它似乎适用于我所有的情况:

#define IPT_SIZE 5

int findNULL(char* arr)
{
    for (int i = 0; i < strlen(arr); i++)
    {
        if (*(arr+i) == '\n')
        {
            return i;
        }
    }
    return 0;
}

int main()
{
    char *input = malloc(IPT_SIZE + 1 * sizeof(char)), buff;
    int counter = 0;

    //prompt user for the input:
    printf("input string no longer than %i characters: ", IPT_SIZE);
    do
    {
        fgets(input, 1000, stdin);
        *(input + findNULL(input)) = '\0';
        if (strlen(input) > IPT_SIZE)
        {
            printf("error! the given string is too large. try again...\n");
            counter++;
        }
        //if the counter exceeds 3, exit the program (custom function):
        errorMsgExit(counter, 3); 
    }
    while (strlen(input) > IPT_SIZE);

//rest of the program follows

free(input)
return 0;
}

【讨论】:

    【解决方案8】:

    以最明显的方式删除换行符的步骤:

    1. 使用strlen(),标头string.h 确定NAME 中字符串的长度。请注意,strlen() 不计入终止的\0
    size_t sl = strlen(NAME);
    

    1. 查看字符串是否以\0 字符开头或仅包含一个\0 字符(空字符串)。在这种情况下,sl 将是 0,因为正如我上面所说的 strlen() 不计算 \0 并在第一次出现时停止:
    if(sl == 0)
    {
       // Skip the newline replacement process.
    }
    

    1. 检查正确字符串的最后一个字符是否为换行符'\n'。如果是这种情况,请将\n 替换为\0。请注意,索引计数从0 开始,因此我们需要执行NAME[sl - 1]
    if(NAME[sl - 1] == '\n')
    {
       NAME[sl - 1] = '\0';
    }
    

    请注意,如果您仅在fgets() 字符串请求时按 Enter(字符串内容仅由换行符组成),则NAME 中的字符串此后将为空字符串。


    1. 我们可以使用逻辑运算符 &amp;&amp; 将步骤 2. 和 3. 组合在一个 if-statement 中:
    if(sl > 0 && NAME[sl - 1] == '\n')
    {
       NAME[sl - 1] = '\0';
    }
    

    1. 完成的代码:
    size_t sl = strlen(NAME);
    if(sl > 0 && NAME[sl - 1] == '\n')
    {
       NAME[sl - 1] = '\0';
    }
    

    如果您更喜欢通过处理fgets 输出字符串而不每次都重新输入来使用此技术的函数,这里是fgets_newline_kill

    void fgets_newline_kill(char a[])
    {
        size_t sl = strlen(a);
    
        if(sl > 0 && a[sl - 1] == '\n')
        {
           a[sl - 1] = '\0';
        }
    }
    

    在您提供的示例中,它将是:

    printf("Enter your Name: ");
    
    if (fgets(Name, sizeof Name, stdin) == NULL) {
        fprintf(stderr, "Error reading Name.\n");
        exit(1);
    }
    else {
        fgets_newline_kill(NAME);
    }
    

    请注意,如果输入字符串中嵌入了\0s,则此方法不起作用。如果是这种情况,strlen() 只会返回第一个 \0 之前的字符数。但这并不是一种很常见的方法,因为大多数字符串读取函数通常会在第一个 \0 处停止,并将字符串读取到那个空字符。

    除了问题本身。尽量避免使您的代码更不清楚的双重否定:if (!(fgets(Name, sizeof Name, stdin) != NULL) {}。你可以简单地做if (fgets(Name, sizeof Name, stdin) == NULL) {}

    【讨论】:

    • 不确定您为什么要这样做。删除换行符的目的不是以空结尾的字符串;它是删除换行符。在字符串的end 处用\0 替换\n 是“删除”换行符的一种方式。但是替换字符串中的\n 字符会从根本上改变字符串。有意使用多个换行符的字符串并不少见,这将有效地切断这些字符串的末端。要删除这样的换行符,数组内容需要向左移动以覆盖\n
    • @exnihilo 如何使用fgets() 输入包含多个换行符的字符串?
    • 好吧,你可以连接多次调用fgets()获得的字符串。但我不明白你的反对意见:你是提出处理多个换行符的代码。
    • @exnihilo 你说得对,我会过度考虑策略。我只是想添加一种非常苛刻但可能的方法来获得所需的结果。
    • @exnihilo 完全编辑了我的答案,并使用strlen 等遵循主要方法。不重复的理由:1.逐步解释代码。 2. 作为功能和基于上下文的解决方案提供。 3. 提示避免双重否定表达式。
    【解决方案9】:

    一般来说,与其修剪不需要的数据,不如避免一开始就写入。如果您不希望缓冲区中有换行符,请不要使用 fgets。请改用getcfgetcscanf。也许是这样的:

    #include <stdio.h>
    #include <stdlib.h>
    int
    main(void)
    {
            char Name[256];
            char fmt[32];
            if( snprintf(fmt, sizeof fmt, "%%%zd[^\n]", sizeof Name - 1) >= (int)sizeof fmt ){
                    fprintf(stderr, "Unable to write format\n");
                    return EXIT_FAILURE;
            }
            if( scanf(fmt, Name) == 1 ) {
                    printf("Name = %s\n", Name);
            }
            return 0;
    }
    

    请注意,这种特殊方法会使换行符不被读取,因此您可能希望使用像 "%255[^\n]%*c" 这样的格式字符串来丢弃它(例如,sprintf(fmt, "%%%zd[^\n]%%*c", sizeof Name - 1);),或者可能在 scanf 后面加上 getchar()

    【讨论】:

    • 您是否意识到上面的代码 sn-p 容易受到缓冲区溢出的影响? sprintf 不检查缓冲区的大小!
    • @Sapphire_Brick 确实不是。格式字符串的长度将是 7 + name 长度的 base 10 表示中的位数。如果该长度大于 24,您将遇到其他问题。如果您想安全并使用snprintf,当然可以,但这适用于明显大于 PB 的缓冲区。
    • 为了溢出缓冲区,您需要创建一个大约 8 yotta 字节的自动数组,因为在 Name 超过 2^83 字节之前您不会溢出缓冲区在尺寸方面。实际上,这不是问题。但是,是的,snprintf 应该始终优先于 sprintf。代码已编辑。
    【解决方案10】:

    如果使用getline 是一个选项——不要忽视它的安全问题,并且如果你想用括号括起来——你可以避免使用字符串函数,因为getline 返回字符数。类似下面的东西

    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
        char *fname, *lname;
        size_t size = 32, nchar; // Max size of strings and number of characters read
        fname = malloc(size * sizeof *fname);
        lname = malloc(size * sizeof *lname);
        if (NULL == fname || NULL == lname)
        {
            printf("Error in memory allocation.");
            exit(1);
        }
        printf("Enter first name ");
        nchar = getline(&fname, &size, stdin);
        if (nchar == -1) // getline return -1 on failure to read a line.
        {
            printf("Line couldn't be read..");
            // This if block could be repeated for next getline too
            exit(1);
        }
        printf("Number of characters read :%zu\n", nchar);
        fname[nchar - 1] = '\0';
        printf("Enter last name ");
        nchar = getline(&lname, &size, stdin);
        printf("Number of characters read :%zu\n", nchar);
        lname[nchar - 1] = '\0';
        printf("Name entered %s %s\n", fname, lname);
        return 0;
    }
    

    注意[ security issues ]getline 不应该被忽略。

    【讨论】:

      【解决方案11】:

      这是我的解决方案。很简单。

      // Delete new line
      // char preDelete[256]  include "\n" as newline after fgets
      
      char deletedWords[256];
      int iLeng = strlen(preDelete);
      int iFinal = 0;
      for (int i = 0; i < iLeng; i++) {
          if (preDelete[i] == '\n') {
      
          }
          else {
              deletedWords[iFinal]  = preDelete[i];
              iFinal++;
          }
          if (i == iLeng -1 ) {
              deletedWords[iFinal] = '\0';
          }
      }
      

      【讨论】:

        【解决方案12】:

        Tim Čas one liner 对于通过调用 fgets 获得的字符串来说是惊人的,因为你知道它们在末尾包含一个换行符。

        如果您在不同的上下文中并且想要处理可能包含多个换行符的字符串,您可能正在寻找 strrspn。它不是 POSIX,这意味着您不会在所有 Unices 上找到它。我为自己的需要写了一个。

        /* Returns the length of the segment leading to the last 
           characters of s in accept. */
        size_t strrspn (const char *s, const char *accept)
        {
          const char *ch;
          size_t len = strlen(s);
        
        more: 
          if (len > 0) {
            for (ch = accept ; *ch != 0 ; ch++) {
              if (s[len - 1] == *ch) {
                len--;
                goto more;
              }
            }
          }
          return len;
        }
        

        对于那些在 C 中寻找 Perl chomp 等效项的人,我想就是这样(chomp 只删除尾随的换行符)。

        line[strrspn(string, "\r\n")] = 0;
        

        strrcspn 函数:

        /* Returns the length of the segment leading to the last 
           character of reject in s. */
        size_t strrcspn (const char *s, const char *reject)
        {
          const char *ch;
          size_t len = strlen(s);
          size_t origlen = len;
        
          while (len > 0) {
            for (ch = reject ; *ch != 0 ; ch++) {
              if (s[len - 1] == *ch) {
                return len;
              }
            }
            len--;
          }
          return origlen;
        }
        

        【讨论】:

        • "因为你知道它们最后包含一个换行符。" --> 甚至在没有'\n'(或者如果字符串是"")的情况下也可以工作。
        • 针对您的第一条评论 chux,我的回答保留了这一点。当没有\n 时,我不得不在strrcspn 中抛出resetlen。
        • 为什么使用goto end;而不是return len;
        • @chqrlie 我需要摆脱我陷入的这个不雅的 2 级循环。伤害已经造成。为什么不是 goto?
        • 您的代码中有两种gotos:可以用return 语句替换的无用goto 和被认为是邪恶的倒退goto。使用strchr 有助于以更简单的方式实现strrspnstrrcspnsize_t strrspn(const char *s, const char *accept) { size_t len = strlen(s); while (len &gt; 0 &amp;&amp; strchr(accept, s[len - 1])) { len--; } return len; }size_t strrcspn(const char *s, const char *reject) { size_t len = strlen(s); while (len &gt; 0 &amp;&amp; !strchr(reject, s[len - 1])) { len--; } return len; }
        【解决方案13】:

        下面的函数是我在 Github 上维护的字符串处理库的一部分。它从字符串中删除不需要的字符,正是您想要的

        int zstring_search_chr(const char *token,char s){
            if (!token || s=='\0')
                return 0;
        
            for (;*token; token++)
                if (*token == s)
                    return 1;
        
            return 0;
        }
        
        char *zstring_remove_chr(char *str,const char *bad) {
            char *src = str , *dst = str;
            while(*src)
                if(zstring_search_chr(bad,*src))
                    src++;
                else
                    *dst++ = *src++;  /* assign first, then incement */
        
            *dst='\0';
                return str;
        }
        

        一个例子可以是

        Example Usage
              char s[]="this is a trial string to test the function.";
              char const *d=" .";
              printf("%s\n",zstring_remove_chr(s,d));
        
          Example Output
              thisisatrialstringtotestthefunction
        

        您可能想检查其他可用功能,甚至为项目做出贡献 :) https://github.com/fnoyanisi/zString

        【讨论】:

        • 您应该删除*src++; 中的* 并生成badtokend const char *。另外为什么不使用strchr 而不是zChrSearch*src 不能是 '\0' 在您的 zStrrmv 函数中。
        • 谢谢@chqrlie!更新了代码以反映您的建议..... zstring 最初是一个有趣的项目,目的是在不使用任何标准库函数的情况下创建字符串操作库,因此我没有使用 strchr
        • 编写一个“不使用任何标准库函数的字符串操作库”是一个很好的练习,但为什么要告诉其他人使用它呢?如果有的话,它将比任何标准库都更慢且测试更少。
        • 这与问题所问的工作不同。它可能可以用来摆脱唯一的换行符,但感觉有点矫枉过正。
        【解决方案14】:
         for(int i = 0; i < strlen(Name); i++ )
        {
            if(Name[i] == '\n') Name[i] = '\0';
        }
        

        你应该试一试。这段代码基本上循环遍历字符串,直到找到'\n'。找到时,'\n' 将被空字符终止符 '\0'

        替换

        请注意,您在此行中比较的是字符而不是字符串,则无需使用 strcmp():

        if(Name[i] == '\n') Name[i] = '\0';
        

        因为您将使用单引号而不是双引号。 Here's 如果您想了解更多信息,请提供有关单引号和双引号的链接

        【讨论】:

        • 低效:for(int i = 0; i &lt; strlen(Name); i++ ) 将多次调用strlen(Name)(循环更改Name[]),因此长度为N,这是O(N*N) 解决方案。只需 1 次调用 strlen(Name)(如果有)即可提供 O(N)` 解决方案。不清楚为什么使用int i 而不是size_t i。考虑for(size_t i = 0; i &lt; Name[i]; i++ )
        • @chux 更像for (size_t i = 0; Name[i]; i++) { if (Name[i] == '\n') { Name[i] = '\0'; break; } }
        • @melpomene 是的,这将是直接和好的。然而,如果break 不存在,i++ 将出现,而后面的Name[i] 将为 0,从而停止循环。您的好主意的优点是i 是循环后的字符串长度。
        • @melpomene 我现在看到了。是的for(size_t i = 0; i &lt; Name[i]; i++ ) 应该是for(size_t i = 0; Name[i]; i++ )
        猜你喜欢
        相关资源
        最近更新 更多