A common problem that arises is obtaining text from user input. I know this is addressed in the FAQ. But a couple of recent threads prompted me to revisit this a little. With that, here are some thoughts.- The standard library function scanf is more complex than many want to realize. (I like Chris Torek's lengthy explanations; here is one example.)
- The standard library function gets is notorious for being unsafe. A very real issue with it is buffer overflow error.
- The standard library function fgets is safe but can be annoying due to the fact that it may retain the new-line character in the string.
Inspired by libclc (but preferring this forum), I'd like to implement a function that is a "bulletproof" version of gets. I also think it makes a good discussion of things that can go wrong in this "simple" process of obtaining text from the user.
Here is some code to start with.
Code:
/*
* Description:
* Define the functions mygets, a safe version of gets, and myprompt.
* Test the functions when the macro TEST is defined.
*/
#include <stdio.h>
#include <string.h>
/*
* Synopsis:
* char *mygets(char *buffer, int size);
*
* Description:
* The mygets function reads at most one less than the number of
* characters specified by 'size' from the input stream pointed to by
* stdin, into the array pointed to by 'buffer'. No additional characters
* are read after a new-line character or after end-of-file. A null
* character is written immediately after the last character read into
* the array. If present, the new-line character is overwritten with a
* null character.
*
* Returns:
* The mygets function returns 'buffer' if successful. If end-of-file is
* encountered and no characters have been read into the array, the
* contents of the array remain unchanged and a null pointer is returned.
* If a read error occurs during the operation, the array contents are
* indeterminate and a null pointer is returned.
*
* Remarks:
* The standard library function gets is notorious for being unsafe.
* Because you cannot specify the size of the buffer into which the data
* will be placed, it can never be used safely. The standard library
* function fgets, on the other hand, is safe but can be annoying due
* to the fact that it retains the new-line character when fewer than
* 'size' - 1 characters are obtained. This function attempts to combine
* safety with convenience.
*/
char *mygets(char *buffer, int size)
{
/*
* Ensure that we won't use a null pointer with standard functions.
*/
if ( buffer != NULL )
{
/*
* Use fgets to obtain text from the stdin and put it in 'buffer'.
* It stops reading when it reads either 'size' - 1 characters
* or a newline character '\n', whichever comes first. It retains
* the newline character, if present.
*/
if ( fgets(buffer, size, stdin) != NULL )
{
/*
* If a new-line character is present in 'buffer', replace it with
* a null character. Use the function strchr to search for the
* new-line character. (There are other methods like strtok.)
*/
char *newline = strchr(buffer, '\n');
if ( newline != NULL ) /* fewer than 'size' - 1 characters */
{
/*
* A new-line character was found, and is located where
* 'newline' points. Replace the new-line character '\n'
* with a null character '\0'.
*/
*newline = '\0';
/*
* Note: Since a new-line character was found, fewer than
* 'size' - 1 characters were present in the stdin.
*/
}
else /* No new-line characters was found */
{
/*
* Since a new-line character was not found, 'size' or more
* characters (too much for 'buffer') were present in the stdin.
*/
for ( ;; )
{
/*
* Consume the 'extra' characters remaining in the stdin.
* This is done by reading the character and discarding
* it (by not placing it in 'buffer' which is already full).
*/
int ch = getchar();
if ( ch == EOF )
{
/*
* End-of-file was encountered or an error occurred.
* Live up to our promise to return a null pointer.
*/
return NULL;
}
if ( ch == '\n' )
{
/*
* The new-line character was found. Stop consuming
* characters from the stdin.
*/
break;
}
}
}
}
else
{
/*
* End-of-file was encountered or an error occurred with fgets.
* Live up to our promise to return a null pointer.
*/
return NULL;
}
}
/*
* Return a pointer to 'buffer' as promised. If a null pointer was
* passed to this function, it will be returned.
*/
return buffer;
}
/*
* Synopsis:
* int myprompt(const char *prompt);
*
* Description:
* The myprompt function writes the string pointed to by 'prompt' to the
* output stream pointed to by stdout. The terminating null character is
* not written. The output stream pointed to by stdout is flushed, which
* causes any unwritten data for that stream to be delivered to the host
* environment to be written to the stdout.
*
* Returns:
* The myprompt function returns EOF if a write error occurs, otherwise
* it returns zero. If an error occurs attempting to flush the stream
* pointed to by stdout, the error indicator for the stdout is set.
*
* Remarks:
* When prompting for user input, flushing the stdout encourages the host
* to display the prompt. The function myprompt combines presenting the
* prompt text with flushing the output buffer.
*/
int myprompt(const char *prompt)
{
/*
* Ensure that we won't use a null pointer with standard functions.
*/
if ( prompt != NULL )
{
/*
* Write the string pointed to by 'prompt' to the stdout.
*/
if ( fputs(prompt, stdout) < 0 )
{
return EOF;
}
/*
* Flush the stdout.
*/
if ( fflush(stdout) < 0 )
{
return EOF;
}
}
return 0;
}
#define TEST
#if defined(TEST)
/*
* Description:
* Test the functions mygets and myprompt defined in this module.
*/
int main(void)
{
char text[10] = {0};
while ( *text != '0' )
{
if ( myprompt("Enter text (\"0\" to exit): ") != EOF )
{
if ( mygets(text, sizeof(text)) != NULL )
{
printf("text = \"%s\"\n", text);
}
else
{
break;
}
}
}
return 0;
}
/* my output
C:\Test>test
Enter text ("0" to exit): here is my text
text = "here is m"
Enter text ("0" to exit): okay
text = "okay"
Enter text ("0" to exit): 1234567890
text = "123456789"
Enter text ("0" to exit): 123456789
text = "123456789"
Enter text ("0" to exit): 12345678
text = "12345678"
Enter text ("0" to exit): 0
text = "0"
C:\Test>test
Enter text ("0" to exit): okay^Z..
C:\Test>test
Enter text ("0" to exit): here is some text^Z...
C:\Test>
*/
#endif
What are recommendations for improvement?
- Is there anything I might have overlooked?
- What might need to be added?
- What could be removed?
- Are there any other comments?