Видалення останнього символу нового рядка з вводу fgets ()


235

Я намагаюся отримати деякі дані від користувача та надіслати їх іншій функції в 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та надіслати належний рядок.


21
if (!fgets(Name, sizeof Name, stdin))(принаймні, не використовуйте два заперечення! і! =)

4
@ Roger Pate "не використовуйте два заперечення" -> hmmm, якщо ми копаємо глибоко "не" і "заперечення" - це обидва заперечення . ;-). Можливо "Використовуйте if (fgets(Name, sizeof Name, stdin)) {.
chux

3
@chux, я впевнений, ти мав на увазіif (fgets(Name, sizeof Name, stdin) == NULL ) {
R Sahu

Відповіді:


154

Трохи некрасивий спосіб:

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

Трохи дивний спосіб:

strtok(Name, "\n");

Зауважте, що strtokфункція не працює, як очікувалося, якщо користувач вводить порожній рядок (тобто натискає лише Enter). Це залишає \nперсонажа неушкодженим.

Є, звичайно, і інші.


7
Будь-яка бібліотека часу виконання C, яка відома потоком (тобто, більшість, яка спрямована на багатопотокову платформу), strtok()буде безпечною для потоків (вона використовуватиме локальне сховище потоку для стану "міждзвінка"). Однак, як правило, краще використовувати нестандартний (але досить поширений) strtok_r()варіант.
Майкл Берр

2
Дивіться мою відповідь щодо повністю безпечного та поточного варіанту, схожого на ваш strtokпідхід (і він працює з порожніми введеннями). Насправді, хорошим способом реалізації strtokє використання strcspnта strspn.
Тім Час

2
Важливо вирішити інший випадок, якщо ви знаходитесь в середовищі, де існує ризик перенапруги. Мовчазне обрізання вводу може спричинити дуже згубні помилки.
Малькольм Маклін

2
Якщо вам подобаються однолінійки та використовуєте glibc, спробуйте *strchrnul(Name, '\n') = '\0';.
двобітний

Коли strchr(Name, '\n') == NULL, окрім "введення занадто довго для буфера, помилки прапорця", існують інші можливості: Останній текст у stdinне закінчувався '\n'або був прочитаний рідкісний вбудований нульовий символ.
chux

439

Мабуть, найпростіше рішення використовує одну з моїх улюблених маловідомих функцій strcspn():

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

Якщо ви хочете, щоб він також обробляв '\r'(скажімо, якщо потік є двійковим):

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

Функція підраховує кількість символів, поки вона не потрапить на a '\r'або a '\n'(іншими словами, вона знаходить перше '\r'або '\n'). Якщо нічого не вдарить, він зупиняється на '\0'(повертаючи довжину рядка).

Зауважте, що це працює добре, навіть якщо немає нового рядка, оскільки strcspnзупиняється на а '\0'. У цьому випадку весь рядок просто замінюється '\0'на '\0'.


30
Це навіть справляється з рідкісним, bufferніж починається, з '\0'тим, що викликає горе за buffer[strlen(buffer) - 1] = '\0';підхід.
chux

5
@chux: Так, я хотів би, щоб про це знали більше людей strcspn(). Одна з найбільш корисних функцій бібліотеки, ІМО. Я вирішив написати та опублікувати купу поширених хаків на C, як цей сьогодні; strtok_rреалізація з використанням strcspnі strspnбув одним з перших: codepad.org/2lBkZk0w ( Увага: я не можу гарантувати , що це без помилок, вона була написана поспіхом і , ймовірно, деякі з них). Я не знаю, де я їх ще опублікую, але я маю намір зробити це в дусі знаменитих "біт-твінг-хаків".
Тім Час

4
Розглядали способи надійної обробкиfgets() . Це strcspn()здається єдиним правильним однолінійним. strlenшвидше - хоча і не так просто.
chux

6
@sidbushes: Питання, як у заголовку, так і в змісті, задає питання про введення нового рядка з fgets()введення . Що завжди також є першим новим рядком.
Тім Час

9
@sidbushes: Я розумію, звідки ви беретесь, але я не можу відповідати за результати пошуку Google для конкретних термінів. Поговоріть з Google, а не я.
Тім Час

83
size_t ln = strlen(name) - 1;
if (*name && name[ln] == '\n') 
    name[ln] = '\0';

8
Ймовірно, викине виняток, якщо рядок порожній, чи не так? Як індекс поза діапазоном.
Едвард Оламісан

1
@EdwardOlamisan, рядок ніколи не буде порожньою.
Джеймс Морріс

5
@James Morris У незвичних випадках fgets(buf, size, ....)-> strlen(buf) == 0. 1) fgets()читається як перший charа '\0'. 2) size == 13) fgets()повертає, NULLто bufвміст може бути будь-чим. (Код ОП робить тест на NULL, хоча) Запропонуйте:size_t ln = strlen(name); if (ln > 0 && name[ln-1] == '\n') name[--ln] = '\0';
chux - Відновіть Моніку

2
Що робити, якщо рядок порожній? lnбуло б -1, окрім того, що факт size_tне підписаний, таким чином записуючи до випадкової пам'яті. Я думаю, ви хочете використовувати ssize_tта перевірити, lnчи> 0.
abligh

2
@ legends2k: Пошук значення часу компіляції (особливо нульового значення, як і в strlen) може бути реалізований набагато ефективніше, ніж звичайний пошук за допомогою char-by-char. З цієї причини я вважаю це рішення кращим, ніж те, strchrчи strcspnзасноване на ньому.
ANT

17

Нижче наведено швидкий підхід до видалення потенціалу '\n'з рядка, збереженого fgets().
Він використовує 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';
  }

Тепер використовуйте bufferі lenза потребою.

Цей метод має побічну перевагу lenзначення для наступного коду. Це може бути швидше, ніж strchr(Name, '\n'). Ref YMMV, але обидва методи працюють.


buffer, з оригіналу fgets()не міститиметься "\n"за певних обставин:
A) Рядок був занадто довгим, bufferтому лише charпопередній запис '\n'зберігається в buffer. Непрочитані символи залишаються в потоці.
Б) Останній рядок у файлі не закінчувався символом a '\n'.

Якщо вхід '\0'десь вставив нульові символи , довжина, про яку повідомляє, strlen()не міститиме '\n'місця розташування.


Деякі інші відповіді на питання:

  1. strtok(buffer, "\n");не вдалося видалити, '\n'коли bufferє "\n". З цієї відповіді - змінено після цієї відповіді, щоб попередити про це обмеження.

  2. Наступний збій в рідкісних випадках , коли перше charчитання на fgets()це '\0'. Це відбувається, коли введення починається з вбудованого '\0'. Тоді buffer[len -1]стає buffer[SIZE_MAX]доступ до пам'яті, безумовно, поза законним діапазоном buffer. Щось хакер може спробувати знайти у глупо читанні текстових файлів UTF16. Це був стан відповіді, коли ця відповідь була написана. Пізніше не-ОП відредагував його, щоб включити код на зразок перевірки цієї відповіді "".

    size_t len = strlen(buffer);
    if (buffer[len - 1] == '\n') {  // FAILS when len == 0
      buffer[len -1] = '\0';
    }
  3. sprintf(buffer,"%s",buffer);не визначена поведінка: Реф . Крім того, це не економить жодного провідного, відокремлюваного чи зворотного пробілів. Тепер видалено .

  4. [Редагувати через гарну пізнішу відповідь ] У buffer[strcspn(buffer, "\n")] = 0;порівнянні з strlen()підходом у 1 вкладиша немає інших проблем, крім продуктивності . Продуктивність в обрізанні, як правило, не є проблемою. Код - це введення / виведення - чорна діра часу процесора. Якщо наступний код потребує довжини рядка або має високу продуктивність, використовуйте цей strlen()підхід. Інакше strcspn()це прекрасна альтернатива.


Дякуємо за корисну відповідь. Чи можемо ми використовуватись, strlen(buffer)коли розмір буфера динамічно розподіляється за допомогою malloc?
rrz0

@ Rrz0 buffer = malloc(allocation_size); length = strlen(buffer);поганий - дані в пам'яті, на які вказує, bufferневідомі. buffer = malloc(allocation_size_4_or_more); strcpy(buffer, "abc"); length = strlen(buffer);в порядку
chux - Відновіть Моніку

спасибі за це !! Я беру курс CS, і це було дуже корисно для одного із завдань. я зарахував вашу відповідь у вихідний код.
Натаніель Хойт

8

Пряме, щоб видалити '\ 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';
}

1
Зауважте, що безпечніше використовувати strnlenзамість цього strlen.
Майк Мертсок

3
У коментарі до першої відповіді у зв’язаному запитанні зазначено "Зверніть увагу, що strlen (), strcmp () та strdup () є безпечними. Альтернативи" n "надають вам додаткову функціональність."
Етьєнн

4
@esker ні, не буде. вставлення ангіни nне збільшує безпеку, в цьому випадку це фактично зробить код більш небезпечним. Аналогічно з strncpy, жахливо небезпечною функцією. Повідомлення, з яким ви пов’язали, є поганою порадою.
ММ

3
Це виходить з ладу для порожнього рядка ( ""). Також не strlen()повертається . size_tint
алк

4
це небезпечно для порожнього рядка, він запише в індексі -1. Не використовуйте це.
Жан-Франсуа Фабре

3

Для одиночного обрізання '\ 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';
  }
}

1
Навіщо гніздитися, ifколи можна просто написати одну умову, використовуючи &&? Ця whileпетля має дивну структуру; це просто може бути while (length > 0 && string[length-1] == '\n') { --length; string[length] = '\0'; }.
Мельпомена

@melpomene дякую за пропозицію. Оновіть код.
BEPP

1
Я б припустити , що перша функція більш природно визначається як: size_t length = strlen(string); if (length > 0 && string[length-1] == '\n') { string[length-1] = '\0'; }. Це також краще відображає друге визначення (просто використання ifзамість while).
мельпомена

@elpomene спасибі Це має сенс. Я оновив код.
BEPP

1

Мій спосіб новачків ;-) Будь ласка, повідомте мене, якщо це правильно. Здається, це працює у всіх моїх випадках:

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

1

Крок для видалення символу нового рядка, можливо, найбільш очевидним способом:

  1. Визначте довжину рядка всередині NAMEза допомогою strlen()заголовка string.h. Зверніть увагу, що strlen()закінчення не рахується \0.
size_t sl = strlen(NAME);

  1. Подивіться, чи рядок починається з або містить лише один \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';
}

Зверніть увагу, якщо ви натиснули лише Enter на fgets()запит рядка (вміст рядка складався лише з символу нової рядки), після чого рядок NAMEбуде порожнім рядком.


  1. Ми можемо поєднати крок 2. і 3. разом лише в одній ifзаяві, використовуючи логічний оператор &&:
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і беруть рядок до цього нульового символу.

Окрім питання самостійно. Намагайтеся уникати подвійних заперечень , які роблять ваш код unclearer: if (!(fgets(Name, sizeof Name, stdin) != NULL) {}. Можна просто зробити if (fgets(Name, sizeof Name, stdin) == NULL) {}.


Не впевнений, чому ви хочете це зробити. Сенс видалення нових рядків не в тому, щоб скасувати нульові рядки; це видалити нові рядки. Заміна a \nна a \0в кінці рядка - це спосіб "видалити" новий рядок. Але заміна \nсимволів у рядку принципово змінює рядок. Не рідкість наявність рядків із навмисними кількома символами нового рядка, і це ефективно відрізає кінці цих рядків. Щоб видалити такі нові рядки, вміст масиву потрібно змістити вліво, щоб перезаписати \n.
ex nihilo

@exnihilo Як хтось може ввести рядок із кількома новинками всередині, використовуючи fgets()?
RobertS підтримує Моніку Селліо

Ну, ви можете об'єднати рядки, отримані кількома дзвінками до fgets(). Але я не розумію вашого заперечення: ви пропонуєте код для обробки декількох нових рядків.
ex nihilo

@exnihilo Ви маєте рацію, я буду переосмислити стратегію. Я хотів би додати дуже суворий, але можливий спосіб отримати бажаний результат.
RobertS підтримує Моніку Селліо

@exnihilo повністю відредагував мою відповідь і дотримувався основного підходу, використовуючи strlenі т.д. 2. Надається як функціональне, так і контекстне рішення. 3. Підказка уникати подвійних виразів заперечення.
RobertS підтримує Моніку Селліо

0

Тім Час один вкладиш дивовижний для рядків, отриманих викликом до 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;
}

Для тих, хто шукає еквівалента Perl chomp в C, я думаю, це саме так (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;
}

1
"тому що ви знаєте, що вони містять єдиний новий рядок в кінці." -> Він працює навіть тоді, коли його немає '\n'(або якщо рядок є "").
chux

У відповідь на ваш перший коментар чу, моя відповідь зберігає це. Мені довелося закидати, strrcspnколи його немає \n.
Філіп А.

Навіщо використовувати goto end;замість return len;?
chqrlie

@chqrlie Мені потрібно було вийти з цієї неелегантної дворівневої петлі, в яку я потрапив. Шкода була заподіяна. Чому б не гото?
Філіп А.

У gotoвашому коді є два види s: марний, gotoякий можна замінити returnвисловлюванням і зворотний, gotoякий вважається злим. Використання strchrдопомагає реалізовувати strrspnі strrcspnпростішим способом: size_t strrspn(const char *s, const char *accept) { size_t len = strlen(s); while (len > 0 && strchr(accept, s[len - 1])) { len--; } return len; }іsize_t strrcspn(const char *s, const char *reject) { size_t len = strlen(s); while (len > 0 && !strchr(reject, s[len - 1])) { len--; } return len; }
chqrlie

0

Якщо використання 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;
}

Примітка : В [ питання безпеки ] з getlineне слід нехтувати , хоча.


-1

Функція нижче - це частина бібліотеки обробки рядків, яку я підтримую в 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++;і зробити bad, tokenі d const char *. Також чому б не використовувати strchrзамість zChrSearch? *srcне може бути '\0'у вашій zStrrmvфункції.
chqrlie

Дякую @chqrlie! оновив код, щоб відобразити ваші пропозиції ..... zstring розпочався як цікавий проект з метою створення бібліотеки струнних маніпуляцій без використання будь-яких стандартних функцій бібліотеки, отже, я не використовувавstrchr
fnisi

1
Написання " бібліотеки обробних рядків без використання будь-яких стандартних функцій бібліотеки " - приємна вправа, але навіщо говорити іншим людям користуватися нею? Якщо що-небудь, це буде повільніше і менш перевірене, ніж будь-яка стандартна бібліотека.
Мельпомена

Це робить іншу роботу, ніж те, про що задається питання. Це, ймовірно, можна використовувати для позбавлення від єдиного нового рядка, але це відчуває себе як надмір.
Джонатан Леффлер

-1
 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';

оскільки ви будете використовувати одинарні, а не подвійні лапки. Ось посилання про сингли проти подвійних цитат, якщо ви хочете дізнатися більше


2
було б краще, якщо ви поясните та відредагуєте формат свого коду.
Анх Фам

Зазвичай краще пояснити рішення, а не просто розміщувати кілька рядків анонімного коду. Ви можете прочитати Як написати гарну відповідь , а також Пояснити відповіді на основі коду .
Массіміліано Краус

1
Мені шкода, що це був мій перший внесок тут. Я це виправлю. Дякую за відгук
Matheus Martins Jerônimo

3
Неефективна: for(int i = 0; i < strlen(Name); i++ )зателефонує strlen(Name)багато разів (зміни циклу Name[]), тому з довжиною Nце O(N*N)рішення. strlen(Name)Для створення рішення O (N) `потрібен лише 1 виклик , якщо він є. Незрозуміло, чому int iвикористовується замість size_t i. Подумайтеfor(size_t i = 0; i < Name[i]; i++ )
chux

@chux Більше схожий наfor (size_t i = 0; Name[i]; i++) { if (Name[i] == '\n') { Name[i] = '\0'; break; } }
melpomene

-1

Спробуйте це:

        int remove_cr_lf(char *str)
        {
          int len =0;


          len = strlen(str);

          for(int i=0;i<5;i++)
          {
            if (len>0)
            if (str[len-1] == '\n')
            {
              str[len-1] = 0;
              len--;
            }

            if (len>0)
            if (str[len-1] == '\r')
            {
              str[len-1] = 0;
              len--;
            }
          }

          return 0;
        }

1
len = strlen(str)може переповнювати: strlenповертає size_t, ні int. Що з дивними if (len>0) if (...)умовами? Ви про це не знаєте &&? Якщо ви збираєтесь видалити декілька кінцевих екземплярів CR / LF, навіщо обмежувати себе 5? Чому б не зняти їх усіх? Чому функція має intтип повернення, коли вона завжди повертається 0? Чому б просто не повернутися void?
мельпомена
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.