[vox-tech] a better scanf?? (C-programming question)
Ken Herron
vox-tech@lists.lugod.org
Fri, 18 Apr 2003 14:32:15 -0700
--On Friday, April 18, 2003 04:48:50 -0700 Andy Campbell
<atcampbell@ucdavis.edu> wrote:
>> Non-blocking with select/poll/busy-looping, reading into a
>> buffer, followed by sscanf when a return key is found will certainly
>> handle the second case... examples if that's what you want.
>> Otherwise you will have to go with some curses or raw mode input
>> to get character by character input from a user, and opens a whole
>> can of worms...
>>
>> TTFN,
>> Mike
>
> I am trying to implement this but am unsuccessfull. Could you show me
> where some examples are?? Thank you!!
When you're developing for unix, you have two different sets of functions
for doing file I/O:
* The unix operating system provides a set of functions which use file
descriptors: open(), close(), read(), write(), etc. These functions are
generally provided by the operating system kernel.
* The C language standard defines a different set of functions that use
file handles (not descriptors): fopen(), fclose(), printf(), etc. These
are commonly called stdio or "standard I/O". These functions are normally
implemented as library functions (i.e. code running outside of the
kernel, just like the functions you write) which call the
operating-system routines in order to perform actual I/O.
stdin, stdout, and stderr are file handles, not descriptors. They're
strictly used with the stdio functions, not the OS functions. Further,
they're generally already open when your program starts up so it isn't
necessary to open() or fopen() them.
Now let's talk about ttys. If your program is interacting with a human
than it's probably through a tty device. This is a unix OS construct that
provides things like the backspace key and killing a program by typing
^C. If you want to do non-blocking reads from a tty you have to
understand a bit about how ttys work.
ttys have three operating modes: line-at-a-time (or "cooked"), cbreak, or
raw. In raw mode, every character typed by the user is immediately
available to the program. cbreak mode is much like raw mode, but some
characters receive special handling, e.g. ^C. In cooked mode, the tty
driver buffers data typed by the user (and handles line-editing such as
backspace, ^U, etc.) until the user hits return or ^D. When the user hits
one of these, the entire line of input becomes available for the program
to read.
You generally don't use stdio routines to do nonblocking input. The stdio
library doesn't provide any standard means to see if data is waiting to
be read without blocking. For example, getchar() always returns either a
character or an EOF indication. There's no getchar return value that
means "not EOF but not valid, either". Similarly, when you use fgets() to
read a line, fgets will block until it reads an entire line. Even reading
from a tty in cbreak mode, with characters dribbling in as the user types
them, fgets() won't return control until it gets that newline character.
So if you want to do non-blocking reads you have to use the OS routines
to do it, rather than the stdio routines. The os routine is called
read(), but it also blocks until data becomes available. There are two
ways around that:
1) Use select() or poll() to test the file descriptor first; perform the
read() only if select/poll indicates you can do it without blocking.
2) Set the file descriptor to nonblocking at open() time or by using
fcntl(). Then, if you call read() and no data is waiting, then read()
will return -1 and errno will be set to EWOULDBLOCK.
Either way, every time you read in some data, you'll need to save it in a
buffer somewhere until you've read a newline (or whatever marks the end
of the input). Then you can call sscanf() to parse the buffer contents if
you like.
Since you'll be using read() and select()/poll(), you'll need to know
what file descriptor to use. You can do any of three things:
1) call fileno(stdin). fileno() returns the file descriptor associated
with a file handle.
2) Just use 0. File descriptors are just numbers. 0 is standard input, 1,
is standard output, and 2 is standard error.
3) Forget about using stdin. Call open("/dev/tty", O_RDWR) instead. This
avoids the question of what to do if standard input isn't actually a tty
device for some reason (e.g., because the user redirected input when he
launched the program).
By now you're probably wishing there were an easier way. There are at
least three alternatives to doing all of this:
1) Use curses/ncurses. Curses is a library for doing fancy operations on
terminals, such as positioning the cursor or printing boldface text.
Among its features, it includes a set of functions for doing nonblocking
character input.
2) Use separate processes. Call fork() and let the child process perform
the operations that shouldn't be interrupted while the parent process
interacts with the user.
3) Use separate threads. A thread is your execution context, i.e. the
part of the program being executed at any given time and its local
variables. Under the classic program model that you're probably used to,
a running program only has one thread, but it's possible to start
additional threads. The benefit is that a single thread can perform an
operation that blocks (for example, using stdio to read a line from the
user) without preventing other threads from executing.
Threads have a reputation for being difficult to work with. It's true
there's a bit of a learning curve, but personally I've never had much
trouble with them. They're definitely the wave of the future; java
contains sophisticated thread support, and most complex GUI-driven
programs are multithreaded. If you aspire to do serious programming
you'll probably end up learning them anyway; better to do it now before
you develop a lot of bad programming habits.
--
Kenneth Herron Kherron@newsguy.com 916-366-7338