I'm working on a small c project. I need to read lines from stdin, but
because gets is known to be an undefined function I've been given the
code below to use instead. It works fine but I'd like to understand what
its doing and I just find it really confusing.
That is because this code is confusing.
Can anyone explain to me what's going on here. Thanks.
#include <string.h>
char *safegets(char *buf, int sz)
{
char *r,*p;
return (buf==NULL || sz<1) ? NULL :
sz==1 ? *buf='\0', buf :
!(r=fgets(buf,sz,stdin)) ? r :
(p = strchr(buf, '\n')) ? *p=0, r : r;
}
Okay, first a top-level view: Someone who thinks he's a lot smarter than
he actually is is trying to show off. I used to write code like this,
maybe fifteen or twenty years ago.
Okay, let's start by expanding this a bit:
char *safegets(char *buf, int size)
{
char *temp1, *temp2;
return (buf == NULL || size < 1) ? NULL :
size == 1 ? (*buf = '\0', buf) :
!(temp1 = fgets(buf, size, stdin)) ? temp1 :
(temp2 = strchr(buf, '\n')) ? (*temp2 = 0, temp1) :
temp1;
}
Wow, that's awful.
Quick intro:
return x ? y : z
is basically the same as
if (x) {
return y;
} else {
return z;
}
?: is like an if/then, except you use it as part of an expression,
instead of on statements. So you could do something like
abs_of_x = (x < 0) ? (-1 * x) : (x);
and that's the same effect as
if (x < 0) {
abs_of_x = (-1 * x);
} else {
abs_of_x = x;
}
Let's convert that to if/else:
char *safegets(char *buf, int size);
{
char *temp1, *temp2;
if (buf == NULL || size < 1) {
return NULL;
} else if (size == 1) {
*buf = '\0';
return buf;
} else if (!(temp1 = fgets(buf, size, stdin))) {
return temp1;
} else if (temp2 = strchr(buf, '\n')) {
*temp2 = 0;
return temp1;
} else {
return temp1;
}
}
Okay, this is still crap. I would not approve this code in a tech
review but it's getting legible. Lemme give it some comments:
/* read at most size characters into buf, resulting in a
* null-terminated string. If buf is null or size is not
* at least 1, returns null. If fgets() fails, returns
* null. Otherwise, returns buf, with the first newline
* (if any) replaced with a null byte. Since fgets() in
* theory stops with the newline, that just trims a trailing
* newline.
*/
char *safegets(char *buf, int size);
{
char *temp1, *temp2;
if (buf == NULL || size < 1) {
/* if buf is NULL, or we've said that less than
* one byte is available, return a NULL pointer;
* you give me invalid inputs, I give you invalid
* outputs.
*/
return NULL;
} else if (size == 1) {
/* if we have exactly one byte, it has to be
* the null terminator.
*/
*buf = '\0';
return buf;
} else if (!(temp1 = fgets(buf, size, stdin))) {
/* assign the results of fgets() into a
* new value named temp1. If it's "false"
* (a null pointer), return that newly
* generated null pointer.
*/
return temp1;
} else if (temp2 = strchr(buf, '\n')) {
/* if we find a newline in the string, replace
* it with a null byte. Return the pointer
* returned by fgets. Which is identical to buf,
* mind you.
*/
*temp2 = 0;
return temp1;
} else {
/* there was no newline, but we got a string,
* return it unmodified.
*/
return temp1;
}
}
Ugh. Not very consistent, but at least it makes sense.
char *safegets(char *buf, int size);
{
char *temp1, *temp2;
if (buf == NULL || size < 1) {
return NULL;
} else if (size == 1) {
*buf = '\0';
return buf;
}
temp1 = fgets(buf, size, stdin);
if (!temp1) {
/* fgets failed */
return NULL;
}
temp2 = strchr(buf, '\n');
if (temp2) {
/* found a newline, trim it */
*temp2 = '\0';
}
return buf;
}
That's a little clearer. Still, it's pretty kludgy. Let's see
if we can't make it a little friendlier. At this point, it's more
clear what the goal is: The goal is to write a wrapper around fgets()
which strips the trailing newline. For historical reasons, "gets()"
trims the terminating newline, fgets() doesn't.
Here's a first pass:
char *safegets(char *buf, int size) {
int c;
int count = 1;
char *s = buf;
if (s && size > 0) {
while ((c = getchar()) != EOF &&
c != '\n' &&
count++ < size) {
*s++ = c;
}
*s++ = '\0';
return buf;
} else {
/* invalid arguments */
return NULL;
}
}
Here's my test program:
#include <stdio.h>
char *safegets(char *buf, int size) {
int c;
int count = 1;
char *s = buf;
if (s && size > 0) {
while ((c = getchar()) != EOF &&
c != '\n' &&
count++ < size) {
*s++ = c;
}
*s++ = '\0';
return buf;
} else {
/* invalid arguments */
return NULL;
}
}
int
main(void) {
char buf[10] = { "xxxxxxxxx" };
int i;
printf("doing safegets, length 6:\n");
safegets(buf, 6);
for (i = 0; i < 8; ++i) {
printf("\t[%d]: 0x%02x [%c]\n",
i,
(unsigned char) buf
,
isprint((unsigned char) buf) ?
buf : '.');
}
printf("[5] or earlier should be 0x00 [.], following should be [x].\n");
return 0;
}
This passed the obvious edge cases:
* values less than 5 characters long
* exactly 5 characters plus a newline
* newline with following characters
* more than 6 characters
-s